// Search Term N-Gram Negative Miner — Google Ads script
// Mines the search-term report into 1- and 2-word n-grams, aggregates
// cost / conversions per n-gram, and writes negative-keyword candidates
// (high spend, zero conversions) to a Google Sheet for review.
var SPREADSHEET_URL = 'PASTE_YOUR_SHEET_URL';
var LOOKBACK_DAYS = 90;
var MIN_COST = 50; // flag n-grams that spent at least this (account currency)
var MAX_NGRAM = 2; // 1 = unigrams only, 2 = unigrams + bigrams
function main() {
var tz = AdsApp.currentAccount().getTimeZone();
var to = new Date();
var from = new Date(to.getTime() - LOOKBACK_DAYS * 864e5);
var range = Utilities.formatDate(from, tz, 'yyyyMMdd') + ',' +
Utilities.formatDate(to, tz, 'yyyyMMdd');
var report = AdsApp.report(
'SELECT Query, Cost, Conversions, Clicks ' +
'FROM SEARCH_QUERY_PERFORMANCE_REPORT ' +
'WHERE CampaignStatus = ENABLED ' +
'DURING ' + range
);
var grams = {};
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var cost = parseFloat(row['Cost']) || 0;
var conv = parseFloat(row['Conversions']) || 0;
var clk = parseInt(row['Clicks'], 10) || 0;
var words = String(row['Query']).toLowerCase().split(/s+/);
for (var n = 1; n <= MAX_NGRAM; n++) {
for (var i = 0; i + n <= words.length; i++) {
var g = words.slice(i, i + n).join(' ');
if (!grams[g]) { grams[g] = { cost: 0, conv: 0, clicks: 0 }; }
grams[g].cost += cost;
grams[g].conv += conv;
grams[g].clicks += clk;
}
}
}
var out = [['N-gram', 'Cost', 'Conversions', 'Clicks']];
Object.keys(grams).forEach(function (g) {
var s = grams[g];
if (s.cost >= MIN_COST && s.conv === 0) {
out.push([g, s.cost.toFixed(2), s.conv, s.clicks]);
}
});
out.sort(function (a, b) { return (b[1] - a[1]) || 0; });
var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getActiveSheet();
sheet.clearContents();
sheet.getRange(1, 1, out.length, 4).setValues(out);
Logger.log((out.length - 1) + ' negative candidates written to the sheet.');
}
Single search terms lie: one query rarely spends enough to prove anything. N-grams aggregate the words inside your search terms, so a money-burning pattern like “free” or “diy repair” becomes visible across hundreds of queries at once. This script mines your last 90 days of search terms into 1- and 2-word n-grams and writes every high-spend, zero-conversion candidate to a Google Sheet for review — it never adds negatives by itself.
How it works
The script reads the search-query performance report for enabled campaigns, tokenizes every query, and rolls cost, conversions and clicks up to each unigram and bigram. Anything that spent at least MIN_COST with zero conversions is sorted by cost and pushed to your spreadsheet. You review the sheet and add the real losers as negative keywords — human judgement stays in the loop.
Setup
Make a copy of the Sheets template (button above) or create a blank Google Sheet, and copy its URL.
In Google Ads, go to Tools > Bulk actions > Scripts and create a new script.
Paste the code, set SPREADSHEET_URL, LOOKBACK_DAYS and MIN_COST, then authorize.
Preview once to validate the output, then schedule it to run Weekly.
Parameters
Parameter
Description
Default
SPREADSHEET_URL
Google Sheet the candidates are written to
—
LOOKBACK_DAYS
Rolling window of search-term data
90
MIN_COST
Minimum spend before an n-gram is flagged
50
MAX_NGRAM
1 = unigrams only, 2 = unigrams + bigrams
2
All four parameters live at the top of the script.
Requirements
A Google Ads account with search campaigns and a Google Sheet the script can write to.