Search Term Optimizer for Google Ads: Free Script with Google Sheet Integration

Improve your PPC results using a Search Term Optimizer for Google Ads. Learn how to reduce wasted spend and find winning keywords.

Search term optimizer, run from Google Sheets

Running a successful Google Ads campaign often comes down to one thing: understanding what your users are searching for. This is where a Search Term Optimizer for Google Ads becomes essential. If you are spending money on paid search but not regularly analyzing your search term reports, you are likely wasting budget on irrelevant or underperforming queries. The right tool can help you identify costly keywords with no conversions and quickly turn that insight into action. In this article, we will explore how using a Search Term Optimizer can supercharge your campaign performance and give you practical Google Ads optimization tips to make the most of your ad spend.

How the Search Term Optimizer Works

The Search Term Optimizer is a combination of a Google Ads Script and a connected Google Sheet. It pulls in your account’s search term data and flags queries that exceed a certain cost threshold without bringing in conversions. You can customize the thresholds to suit your business goals, which means the tool adapts to different industries, budgets, and campaign sizes.

Once the data is inside the Google Sheet, the script evaluates each search term based on your criteria. If a query spends more than your set limit and doesn’t convert, it gets flagged as a negative keyword suggestion. This lets you take action faster, with more confidence and less manual work.

Setting Up the Search Term Optimizer

There are two parts that you should follow as below:

Part 1: How to Install the Script in Google Ads

Step 1: Go to your Google Ads account

Step 2: Navigate to → Tools & Settings → Bulk Actions → Scripts

Step 3: Click the + button to create a new script

Step 4: Copy the full script code and paste into the editor

Step 5: Click “”Authorize”” and approve permissions

Step 6: Set your Sheet URL at this line: const SHEET_URL = ‘PASTE_YOUR_SHEET_URL_HERE’;

Step 7: Click Save, then click Run

The script will fetch recent search terms and update your Google Sheet automatically.

Part 2: How to Use the Google Sheet

Important: When first opening the Sheet, the Config tab will likely be empty.
You must first run the script from Google Ads at least once to generate the configuration options automatically.

In the Config tab:

Step 1: Select campaign from cell B7

Step 2: Choose action in B8 – options include: (Fetch Search Terms, Apply Negatives and Add as Keyword) – Note: You can only run one action at a time

Step 3: Set date range in cells B9 – B10

Step 4: Set performance thresholds in B11 – B12 (Cost Threshold & Conversion Threshold) and when adjusting the Cost (B11) or Conversions (B12) thresholds, the script will automatically re-evaluate all search terms.

Step 5: Go back to Google Ads Script editor and click Run

In the Search Terms tab:

Step 1: Review the list of search queries.

The Status column will show:

  • Already Negative – if the term was previously excluded.
  • Matches Existing Keyword – if it matches an active targeting keyword.
  • (blank) – if it’s a new search term.

Step 2: In the Suggested Action column, choose one:

  • Add as Negative
  • Add as Positive

Step 3: Select the Match Type for your chosen action.

Step 4: Go back to the Google Ads Script editor and click Run.

The script will:

  • Apply the selected actions.
  • Update the Status column.

Why Google Ads Search Term Scripts and Sheets Make a Powerful Combo

One reason this optimizer is so effective is because it leverages the power of automation. Google Ads Scripts can access real-time account data, while Google Sheets provides an easy, shareable workspace. Together, they let you analyze, visualize, and take action without needing any expensive tools or platforms.

Even if you’re new to Google Ads Scripts, the setup is really beginner-friendly. The script is well-documented, and the instructions are clear. Once installed, you can rerun the script whenever you want fresh data, or even set it to run on a schedule.

Using Sheets also means you can collaborate with team members or clients. Everyone can see the flagged terms, make notes, and keep track of changes. This transparency helps improve communication and decision-making in your PPC efforts.

Final thought

If you’re serious about improving your Google Search Campaign results, a Search Term Optimizer is a must-have. It saves time, reduces wasted spend, and uncovers valuable keyword insights you might otherwise miss. By combining the power of Google Ads Scripts with a dynamic Google Sheet interface, this tool gives advertisers of all levels an edge in their optimization process.

Remember, Google Ads optimization is not a one-time activity. It requires consistent review, smart automation, and the willingness to adapt. This optimizer helps make that process easier, faster, and more effective.

Start using it today and take your campaigns to the next level.

Finally, I’ve packaged this script along with the Google Sheet into a single file, which you can access here.

OR, you can find the script below—just copy and paste it into your project.

// ===== CONFIG =====
const SHEET_URL = 'PASTE_YOUR_SHEET_URL_HERE';
const SEARCH_SHEET = 'SearchTerms';
const CONFIG_SHEET = 'Config';

// ===== MAIN ENTRY =====
function main() {
  const ss = SpreadsheetApp.openByUrl(SHEET_URL);
  const configSheet = ss.getSheetByName(CONFIG_SHEET);

  if (!configSheet || configSheet.getLastRow() === 0) {
    Logger.log("ℹ️ Config sheet missing or empty. Creating...");
    setupConfigSheet();
    Logger.log("✅ Config sheet ready.");
    return;
  }

  const actionLabel = configSheet.getRange("B8").getValue();

  if (actionLabel === "Fetch Search Terms") {
    fetchSearchTerms();
  } else if (actionLabel === "Apply Negatives") {
    applyNegativesToCampaign();
  } else if (actionLabel === "Add as Keyword") {
    applyPositiveKeywords();
  } else {
    Logger.log("ℹ️ Choose action in B8: Fetch Search Terms, Apply Negatives, or Add as Keyword.");
  }
}

// ===== CONFIG SHEET CREATION =====
function setupConfigSheet() {
  const ss = SpreadsheetApp.openByUrl(SHEET_URL);
  let sheet = ss.getSheetByName(CONFIG_SHEET);
  if (!sheet) sheet = ss.insertSheet(CONFIG_SHEET);
  sheet.clear();

  sheet.getRange("A1").setValue("Step 1: Select campaign (B7)");
  sheet.getRange("A2").setValue("Step 2: Choose action (B8)");
  sheet.getRange("A3").setValue("Step 3: Set date range (B9-B10)");
  sheet.getRange("A4").setValue("Step 4: Set thresholds (B11-B12)");
  sheet.getRange("A5").setValue("Step 5: Run main()");
  sheet.getRange("A1:A5").setFontWeight("bold").setBackground("#f4cccc");

  sheet.getRange("A7").setValue("Campaign Name");
  sheet.getRange("A8").setValue("Action");
  sheet.getRange("A9").setValue("Start Date");
  sheet.getRange("A10").setValue("End Date");
  sheet.getRange("A11").setValue("Cost Threshold");
  sheet.getRange("A12").setValue("Conversion Threshold");

  sheet.getRange("A7:A12").setFontWeight("bold").setBackground("#c9daf8");
  sheet.getRange("B7:B12").setBackground("#d9ead3");

  let campaigns = [];
  const campIter = AdsApp.campaigns().get();
  while (campIter.hasNext()) campaigns.push(campIter.next().getName());

  sheet.getRange("B7").setDataValidation(
    SpreadsheetApp.newDataValidation().requireValueInList(campaigns, true).build()
  );
  sheet.getRange("B8").setDataValidation(
    SpreadsheetApp.newDataValidation()
      .requireValueInList(["Fetch Search Terms", "Apply Negatives", "Add as Keyword"], true)
      .build()
  );

  const today = Utilities.formatDate(new Date(), AdsApp.currentAccount().getTimeZone(), "yyyyMMdd");
  sheet.getRange("B9").setValue(today);
  sheet.getRange("B10").setValue(today);
  sheet.getRange("B11").setValue(50);
  sheet.getRange("B12").setValue(0);
}

// ===== FETCH SEARCH TERMS (UPDATED with ❌ Already Negative Detection and exact match only logic) =====
function fetchSearchTerms() {
  const ss = SpreadsheetApp.openByUrl(SHEET_URL);
  const config = ss.getSheetByName(CONFIG_SHEET);
  let sheet = ss.getSheetByName(SEARCH_SHEET);
  if (!sheet) sheet = ss.insertSheet(SEARCH_SHEET);
  sheet.clear();

  sheet.appendRow([
    "Campaign", "Ad Group", "Search Term",
    "Impr", "Clicks", "Conv", "Cost",
    "Add Negative?", "Add Keyword?", "Match Type", "Status", "Suggest Negative?"
  ]);

  const selectedCampaign = config.getRange("B7").getValue();
  const startDate = config.getRange("B9").getValue();
  const endDate = config.getRange("B10").getValue();
  const costThreshold = parseFloat(config.getRange("B11").getValue());
  const convThreshold = parseFloat(config.getRange("B12").getValue());

  const campaign = AdsApp.campaigns().withCondition(`Name = "${selectedCampaign}"`).get().next();

  // Load campaign-level negative keywords
  let existingNegatives = new Set();
  const campNegIter = campaign.negativeKeywords().get();
  while (campNegIter.hasNext()) {
    existingNegatives.add(normalizeKeyword(campNegIter.next().getText()));
  }

  // Load ad group-level negative keywords
  const adGroupIter = campaign.adGroups().get();
  while (adGroupIter.hasNext()) {
    const adGroup = adGroupIter.next();
    const negs = adGroup.negativeKeywords().get();
    while (negs.hasNext()) {
      existingNegatives.add(normalizeKeyword(negs.next().getText()));
    }
  }

  // Load existing positive keywords (for exact match check)
  let existingPositives = new Set();
  const kwIter = campaign.keywords().get();
  while (kwIter.hasNext()) {
    existingPositives.add(normalizeKeyword(kwIter.next().getText()));
  }

  const query =
    `SELECT campaign.name, ad_group.name, search_term_view.search_term,
            metrics.impressions, metrics.clicks, metrics.conversions, metrics.cost_micros
     FROM search_term_view
     WHERE campaign.name = '${selectedCampaign}'
     AND segments.date BETWEEN '${startDate}' AND '${endDate}'`;

  const report = AdsApp.report(query);
  const rows = report.rows();

  let rowCount = 0;
  while (rows.hasNext()) {
    const row = rows.next();
    const term = row["search_term_view.search_term"];
    const norm = normalizeKeyword(term);
    const cost = parseFloat(row["metrics.cost_micros"]) / 1e6;
    const conv = parseFloat(row["metrics.conversions"]);

    let status = "";
    if (existingNegatives.has(norm)) {
      status = "✅ Already Negative";
    } else if (existingPositives.has(norm)) {
      status = "⚠️ Matches Existing Keyword";
    }

    const suggestNeg = (cost > costThreshold && conv <= convThreshold) ? "YES" : "";

    sheet.appendRow([
      row["campaign.name"], row["ad_group.name"], term,
      row["metrics.impressions"], row["metrics.clicks"], conv, cost,
      "no", "no", "broad", status, suggestNeg
    ]);
    rowCount++;
  }

  sheet.getRange(2, 8, rowCount, 1).setDataValidation(
    SpreadsheetApp.newDataValidation().requireValueInList(["yes", "no"], true).build()
  );
  sheet.getRange(2, 9, rowCount, 1).setDataValidation(
    SpreadsheetApp.newDataValidation().requireValueInList(["yes", "no"], true).build()
  );
  sheet.getRange(2, 10, rowCount, 1).setDataValidation(
    SpreadsheetApp.newDataValidation().requireValueInList(["broad", "phrase", "exact"], true).build()
  );

  for (let r = 2; r <= rowCount + 1; r++) {
    if (sheet.getRange(r, 12).getValue() === "YES") {
      sheet.getRange(r, 1, 1, 12).setBackground("#ffe599");
      sheet.getRange(r, 12).setValue("⚠️ YES");
    }
  }

  Logger.log(`✅ ${rowCount} search terms fetched.`);
}

// ===== APPLY NEGATIVE KEYWORDS (UNCHANGED) =====
function applyNegativesToCampaign() {
  const ss = SpreadsheetApp.openByUrl(SHEET_URL);
  const config = ss.getSheetByName(CONFIG_SHEET);
  const sheet = ss.getSheetByName(SEARCH_SHEET);
  const campaignName = config.getRange("B7").getValue();

  const campaign = AdsApp.campaigns().withCondition(`Name = "${campaignName}"`).get().next();

  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    const [_, adGroupName, term, , , , , addNeg, , matchType, ,] = data[i];
    if (addNeg.toLowerCase() === "yes") {
      const adGroup = campaign.adGroups().withCondition(`Name = "${adGroupName}"`).get().next();

      let negText = term;
      if (matchType === "phrase") negText = `"${term}"`;
      else if (matchType === "exact") negText = `[${term}]`;

      adGroup.createNegativeKeyword(negText);
      Logger.log(`➕ Added negative keyword: ${negText} to ${adGroupName}`);
    }
  }
}

// ===== APPLY POSITIVE KEYWORDS (UNCHANGED) =====
function applyPositiveKeywords() {
  const ss = SpreadsheetApp.openByUrl(SHEET_URL);
  const config = ss.getSheetByName(CONFIG_SHEET);
  const sheet = ss.getSheetByName(SEARCH_SHEET);
  const campaignName = config.getRange("B7").getValue();

  const campaign = AdsApp.campaigns().withCondition(`Name = "${campaignName}"`).get().next();

  const existing = [];
  const kwIter = campaign.keywords().get();
  while (kwIter.hasNext()) {
    existing.push(normalizeKeyword(kwIter.next().getText()));
  }

  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    const [_, adGroupName, term, , , , , , addKey, matchType, ,] = data[i];
    if (addKey.toLowerCase() === "yes") {
      const norm = normalizeKeyword(term);
      if (existing.includes(norm)) continue;

      const adGroup = campaign.adGroups().withCondition(`Name = "${adGroupName}"`).get().next();
      let kwText = term;
      if (matchType === "phrase") kwText = `"${term}"`;
      else if (matchType === "exact") kwText = `[${term}]`;

      adGroup.newKeywordBuilder().withText(kwText).build();
      Logger.log(`✅ Added keyword "${kwText}" to ${adGroupName}`);
    }
  }
}

// ===== NORMALIZATION UTILS (UNCHANGED) =====
function normalizeKeyword(kw) {
  return kw
    .toLowerCase()
    .normalize('NFD') // handle accents like in Vietnamese
    .replace(/[\u0300-\u036f]/g, '') // strip diacritics
    .replace(/[^\p{L}\p{N} ]+/gu, '') // Remove punctuation
    .replace(/\s+/g, ' ') // Normalize spaces
    .trim();
}

FAQ

What is the Search Term Optimizer for Google Ads?

Search Term Optimizer is a Google Ads Script that analyzes your Google Ads search term report to find queries that are costing money without converting. It helps you make smarter decisions about negative keywords.

Is the Search Term Optimizer hard to set up?

Not at all. It uses a Google Sheet and a Google Ads Script that you copy, authorize, and run. The setup takes less than 10 minutes and includes instructions.

Can I change the thresholds for cost and conversions?

Yes, the Config tab in the sheet lets you adjust the cost and conversion thresholds. The script will then re-evaluate all terms based on the new values.

Will it automatically add negative keywords to my account?

No. The script highlights suggestions, but you manually approve which search terms to add as negatives. This keeps you in control.

Does it work for all campaign types?

It works best with Search campaigns but can also provide value in campaigns where search term data is available. It’s a flexible tool that adapts to your needs.

Dien Ho · Founder, PPCBlogPro

Dien writes PPCBlogPro's guides on Google Ads, LinkedIn Ads, Meta Ads, SEO and AI search for B2B and SaaS marketers, and builds Crawlens, a desktop crawler for technical SEO audits.