Search Query Optimization - Single Account

Reports icon

This script compares search term performance in the Search term view report with thresholds you supply to generate lists of positive and negative (exact) keywords.

How it works

To familiarize yourself with how reporting works in Google Ads scripts, check out the dedicated reporting guide and the reporting samples page.

This solution runs a search query report to find search terms that may be undesirable if they don't have a high enough click-through rate, and adds them as negative keywords so that they won't trigger your ad.

The script also looks for keywords that have a high click-through rate as well as a low cost per conversion, and adds them as positive keywords.

Setup

  • Create a new script with the source code below.
  • When previewing, the script will most likely be able to complete within the 30 second preview window. If not, try changing the condition DURING LAST_7_DAYS to DURING YESTERDAY.
  • After the script completes in preview, you're presented with a list of keywords that would be added as negative (and in some cases positive) keywords.

Source code

// Copyright 2015, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name Search Query Report
 *
 * @overview The Search Query Report script uses the Search Query Performance
 *     Report to find undesired search terms and add them as negative (or
 *     positive) exact keywords. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/search-query
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.0
 *
 * @changelog
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.0.1
 *   - Upgrade to API version v201609.
 * - version 1.0
 *   - Released initial version.
 */

// Minimum number of impressions to consider "enough data"
const IMPRESSIONS_THRESHOLD = 100;
// Cost-per-click (in account currency) we consider an expensive keyword.
const AVERAGE_CPC_THRESHOLD = 1; // $1
// Threshold we use to decide if a keyword is a good performer or bad.
const CTR_THRESHOLD = 0.5; // 0.5%
// If ctr is above threshold AND our conversion cost isn’t too high,
// it’ll become a positive keyword.
const COST_PER_CONVERSION_THRESHOLD = 10; // $10

/**
 * Configuration to be used for running reports.
 */
const REPORTING_OPTIONS = {
  // Comment out the following line to default to the latest reporting version.
  apiVersion: 'v11'
};

function main() {
  const report = AdsApp.report(
      `SELECT search_term_view.search_term, ` +
      `metrics.clicks, ` +
      `metrics.cost_micros, ` +
      `metrics.ctr, ` +
      `metrics.conversions_from_interactions_rate, ` +
      `metrics.cost_per_conversion, ` +
      `metrics.conversions, ` +
      `campaign.id, ` +
      `ad_group.id ` +
      `FROM search_term_view ` +
      `WHERE metrics.conversions > -1 ` +
      `AND metrics.impressions > ${IMPRESSIONS_THRESHOLD} ` +
      `AND metrics.average_cpc > ${AVERAGE_CPC_THRESHOLD} ` +
      `AND segments.date DURING LAST_7_DAYS`, REPORTING_OPTIONS);

  const rows = report.rows();
  const negativeKeywords = {};
  const positiveKeywords = {};
  const allAdGroupIds = {};
  // Iterate through search query and decide whether to
  // add them as positive or negative keywords (or ignore).
  for (const row of rows) {
    if (parseFloat(row['metrics.ctr*100%']) < CTR_THRESHOLD) {
      addToMultiMap(negativeKeywords, row['ad_group.id'],
                    row['search_term_view.search_term']);
      allAdGroupIds[row['ad_group.id']] = true;
    } else if (parseFloat(row['metrics.cost_per_conversion']) <
        COST_PER_CONVERSION_THRESHOLD ||
        !(parseFloat(row['metrics.cost_per_conversion']))) {
        addToMultiMap(positiveKeywords, row['ad_group.id'],
                    row['search_term_view.search_term']);
        allAdGroupIds[row['ad_group.id']] = true;
    }
  }

  // Copy all the adGroupIds from the object into an array.
  const adGroupIdList = [];
  for (const adGroupId in allAdGroupIds) {
    adGroupIdList.push(adGroupId);
  }

  // Add the keywords as negative or positive to the applicable ad groups.
  const adGroups = AdsApp.adGroups().withIds(adGroupIdList).get();
  for (const adGroup of adGroups) {
    if (negativeKeywords[adGroup.getId()]) {
      for (const negativeKeyword of negativeKeywords[adGroup.getId()]) {
        adGroup.createNegativeKeyword(`[${negativeKeyword}]`);
      }
    }
    else if (positiveKeywords[adGroup.getId()]) {
      for (const positiveKeyword of positiveKeywords[adGroup.getId()]) {
        adGroup.newKeywordBuilder()
            .withText(`[${positiveKeyword}]`)
            .build();
      }
    }
  }
}

function addToMultiMap(map, key, value) {
  if (!map[key]) {
    map[key] = [];
  }
  map[key].push(value);
}