N-Gram Lens breaks every search term into one-, two- and three-word phrases and aggregates cost, clicks and conversions per phrase, so patterns that are invisible term-by-term become obvious. It covers Search, Shopping and DSA from search_term_view and Performance Max from campaign_search_term_view, and it knows which terms are already blocked by your negatives.

How it works

Campaigns with impressions in the window are collected (filtered by campaign_contains / campaign_not_contains and status). Search-type campaigns are read at ad-group level so ad-group negatives apply; PMax campaigns at campaign level with cost. Existing negatives at ad-group, campaign, shared-list (by ID) and account level are matched with exact / phrase / broad semantics; matched queries are counted as excluded. Terms are tokenised in the build language with optional stopwords, n-grams of min_ngram…max_ngram words are counted once per query, and thresholds trim the noise. Negative candidates are phrases with clicks but no or expensive conversions, each with the evidence (“account rate predicts ~N conv.”); expansion candidates convert better than the account.

What it writes to the Sheet

  • Summary — campaigns in scope, sources, queries analysed and excluded by negatives, n-gram sizes, account totals, counts of negative and expansion candidates, cost sitting on negative candidates, notes and any ⚠ warnings (read limit, account negatives unreadable).
  • Account n-grams and Campaign n-grams — phrase, n, queries, source, impressions, clicks, CTR, cost, CPC, conversions, value, conv. rate, CPA, ROAS; campaign table segmented per campaign.
  • Word count — metrics by number of words in the query.
  • Suggestions — negative candidates (paste as phrase negatives) and expansion candidates, each with a “Why” column. Nothing is ever applied automatically.

Setup

  1. Create a new, empty Google Sheet with the Google account you will authorise the script with and copy its URL. Do not reuse a Sheet from another script — each Sheet is bound to one script and one Google Ads account.
  2. In Google Ads open Tools → Scripts, create a new script, paste the whole code and give it a name.
  3. Set CONFIG.spreadsheet_url to the Sheet URL. If you run the script from a manager (MCC) account, also set CONFIG.account_id to the client account ID (e.g. 123-456-7890); without it the script stops with a clear error.
  4. Optional: fill CONFIG.email (comma-separated recipients) and/or CONFIG.slack_webhook. Keep the webhook in the code only — it is never written to the Sheet.
  5. Authorise the script, run Preview once to check the log, then Run. The first run creates the tabs, the header rows and the _settings tab.
  6. Schedule it: weekly, Monday at 04:00 account time or later.
  7. Optional: download the Excel dashboard (button above), open its Read me tab and paste the Sheet tabs as described — the column layout matches the script output one-to-one.

Parameters

ParameterDescriptionDefault
spreadsheet_urlURL of the empty Google Sheet the script writes to.'YOUR_SPREADSHEET_URL'
account_idClient account ID; required only when running from a manager (MCC) account.''
daysLength of the analysis window in days.30
min_ngram / max_ngramSmallest and largest n-gram size (1 = single words, 3 = three-word phrases).1 / 3
include_search / include_pmaxInclude Search/Shopping/DSA and/or Performance Max campaigns.true / true
campaign_containsOnly campaigns whose name contains this text.''
campaign_not_containsExclude campaigns whose name contains this text.''
enabled_campaigns_onlytrue = enabled campaigns only; false = enabled and paused.true
min_query_impressionsMinimum impressions for a search term to be read.1
check_negativesMatch terms against existing negatives (ad group, campaign, shared list, account level).true
languageTokeniser language; defaults to the build language (en / tr / de).L.text_lang
stopwordsExtra words to ignore when building n-grams.[]
segment_by_campaignWrite the per-campaign n-gram table.true
max_rows_writtenRow cap per table; when exceeded a truncation note is added.20000
thresholdsMinimum query count / impressions / clicks / cost for an n-gram to be listed.1 / 20 / 0 / 0
suggestionsnegative_min_clicks, negative_max_cpa_multiple, expansion_min_conversions, list_size.30 / 3 / 3 / 50
All values live in the CONFIG block at the top of the script; keys that also exist in the _settings tab can be changed there.

The _settings tab

On the first run the script creates a _settings tab (Key / Value / Notes). Values there override the matching CONFIG entries, so day-to-day tuning happens in the Sheet without editing code. Numbers must be written without thousands separators and with at most two decimals (0.5 or 0,5). The rows script and account mark the Sheet’s owner: if another script or another account opens the same Sheet, it stops before writing anything. Internal tabs starting with an underscore (_settings, _history, _snapshot, _state, _baseline) are language-independent, so switching between the EN/TR/DE builds keeps the stored state.

Notifications

This script has no e-mail or Slack option; the Sheet and the dashboard are the output.

Notes and limits

  • Broad negatives match any word order, phrase negatives contiguous words, exact negatives the whole query — the same semantics Google applies.
  • If a query hits the Ads Scripts read limit the Summary shows a ⚠ line; narrow the window or filters.
  • Whether search_term_view returns rows for Shopping campaigns on your account should be checked once against the UI search-terms report.
  • Every report tab carries a small attribution cell (H1:K1, warning-only protection) and the code header keeps a copyright line; both may be removed by anyone who edits the source.