As of February 18, 2021 Google is getting rid of Broad Match Modifier...which just breaks my heart. I threw together a script and sheet to adapt to this by creating a Phrase Match variation of every BMM keyword and labelling the legacy BMM keywords and their new (or even pre-existing) Phrase Match counterparts to help monitor performance and search queries as the transition is carried out.
Below you'll find a spreadsheet template for formatting keywords across accounts in your MCC so that you can simply drop the new Phrase Match keywords into Google Ads Editor for multiple accounts to get everything in place in just a few minutes. It will also apply the labels to any new or pre-existing BMM/Phrase counterparts to help monitor performance.
After that, you'll also find a reporting sheet template for reviewing performance for these keywords as the transition gets carried out in order to better make the decision of which keywords to keep and which to pause. I've also appended some anonymized insights from some accounts I've worked on.
Make a copy of the sheet template using the button above.
On the Settings tab, you can make the decision for how you want to import these keywords into the sheet by selecting to "Pre-Filter" or "Post-Filter" the accounts you're pulling data for.
Pre-Filter will allow you to pick and choose which accounts you want to import data for before you drop the data into the sheet. This will be the best option for larger MCCs where you're more likely to surpass the time limit on Google Ads Scripts. To select this option, simply select Pre-Filter from the dropdown in cell B3 and then enter the Account IDs that you'd like to import in the blue box in cell D3. Follow formatting instructions in the sheet.
Post-Filter will allow you to just filter the accounts included in the Export tab after you've imported the data for all accounts. Just useful for doing groups of client accounts at a time or more carefully setting up keywords for a client or two at a time. After you run the script below, you can check and uncheck the accounts you want to include using the checkboxes in Column B.
Once you've chosen how to filter (or not filter) the accounts, copy the script below, create a new script in your MCC, and paste it into the Script Editor.
Change MY_GOOGLE_SHEET_URL in Line 6 to the URL of the spreadsheet that you created above.
If desired, you can change the Date Range from LAST_30_DAYS. It will only make a difference in that it will filter out any accounts without Impressions in the last 30 days (see lines 44 and 45).
Save, Authorize, and Run the script. It should drop all BMM keywords from the accounts you've selected into the spreadsheet you created above. Note: this script will not actively make any changes to your accounts. It is just structured to be a reporting script. You'll make changes in steps 7 and 8.
function main() {
//***START CUSTOM VARIABLES***//
//URL of Google Sheet to push data to
var spreadsheetUrl = 'MY_GOOGLE_SHEET_URL';
//see documentation for preset dates under DateRangeLiteral: https://developers.google.com/adwords/api/docs/guides/awql#formal_grammar
var dateRange = 'LAST_30_DAYS';
//***END CUSTOM VARIABLES***//
//name of tab on sheet where data will be delivered
var sheetTabName = 'Import';
//report type – see documentation here: https://developers.google.com/adwords/api/docs/appendix/reports/account-performance-report
var reportType = 'KEYWORDS_PERFORMANCE_REPORT';
//select metrics from report above – see link from documentation – make sure all terms are comma separated with a space afterwards
var metrics = 'ExternalCustomerId, CustomerDescriptiveName, CampaignName, AdGroupName, Criteria, KeywordMatchType, CpcBid, Labels';
//campaignArray is for pushing active campaigns only into the filter
var campaignArray = [];
//pre-filter accounts setting from the Settings sheet of the Spreadsheet listed above
var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
var settingsSheet = spreadsheet.getSheetByName('Settings');
var filterSetting = settingsSheet.getRange('B3').getValue();
var accountsToFilter = settingsSheet.getRange('D3').getValue();
if (filterSetting == 'Pre-Filter') {
var accountFilter = 'ExternalCustomerId IN ['+accountsToFilter+']';
Logger.log('Accounts will be pre-filtered by sheet settings. Accounts: ' + accountsToFilter);
}
else {
var accountFilter = 'ExternalCustomerId NOT_IN [0000000000,1111111111]';
Logger.log('Accounts are not being pre-filtered by the spreadsheet settings.');
}
//account iterator – currently gets all accounts, but can be adjusted to filter for specifics using this documentation:
//https://developers.google.com/google-ads/scripts/docs/examples/ads-manager-scripts
var accountIterator = MccApp.accounts().withCondition('Impressions > 0').withCondition(accountFilter).forDateRange(dateRange).get();
var campaignAccountIterator = MccApp.accounts().withCondition('Impressions > 0').withCondition(accountFilter).forDateRange(dateRange).get();
var mccAccount = AdWordsApp.currentAccount();
var sheet = spreadsheet.getSheetByName(sheetTabName);
//pull campaigns to check and push into array
while (campaignAccountIterator.hasNext()) {
var accountCampaign = campaignAccountIterator.next();
MccApp.select(accountCampaign);
var campaignReport = AdWordsApp.report(
'SELECT CampaignId ' +
'FROM CAMPAIGN_PERFORMANCE_REPORT ' +
'WHERE ServingStatus = "SERVING" ' +
'DURING ' + dateRange);
var campaignRows = campaignReport.rows();
while (campaignRows.hasNext()) {
var campaignRow = campaignRows.next();
for (var i = 0; i < 1; i++) {
campaignArray.push(campaignRow['CampaignId']);
}
}
}
//clear sheet & add header using metrics variable
sheet.clearContents();
var headers = metrics.split(', ');
sheet.appendRow(headers);
//pull report and append rows of data
while (accountIterator.hasNext()) {
var account = accountIterator.next();
MccApp.select(account);
var report = AdWordsApp.report(
'SELECT ' + metrics + ' ' +
'FROM ' + reportType + ' ' +
'WHERE CampaignStatus = "ENABLED" AND CampaignId IN ['+campaignArray+'] AND AdGroupStatus = "ENABLED" AND Status = "ENABLED" AND Criteria CONTAINS "+" AND KeywordMatchType = "BROAD" ' +
'DURING ' + dateRange);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var rowArray = [];
for (var i = 0; i < headers.length; i++) {
rowArray.push("'"+row[headers[i]]);
}
sheet.appendRow(rowArray);
}
}
//timestamp sheet
var now = new Date();
var timeZone = AdsApp.currentAccount().getTimeZone();
var dateString = Utilities.formatDate(now, timeZone, 'MMMM dd, yyyy h:mm a');
settingsSheet.getRange('F1').setValue(dateString);
}
7. After the script has run, you can review the proposed structure in the Export tab of your Google Sheet. If you've already filtered out your accounts, you can simply open up Google Ads Editor and open the accounts you'd like to import changes for (the Google Sheet is structured to allow for multiple accounts to be edited in one single upload).
8. Select all contents from the Export tab of your Google sheet. Go to the Keywords section of the Editor for all accounts, select Make Multiple Changes, and paste in the contents from the Google Sheet's Export tab. Review changes and post if everything looks good.
9. You should end up with new Phrase Match variations of your existing BMM keywords. BMM Keywords will be left in place but assigned the label: Legacy BMM. Your new Phrase Match keywords (and any pre-existing Phrase Match Keywords that are already counterparts for the BMM keywords you had running) will be assigned the label: Phrase BMM Replacement. The same Max CPC will be assigned to the Phrase Match keywords as the pre-existing BMM keywords. Review these changes and make sure that doesn't cause any issues. Note that any labels on existing BMM keywords will also be applied to the new Phrase Match keywords so make any adjustments as needed here.
Make a copy of the reporting sheet using the link above.
Go to the Settings tab and update the Account IDs that you'd like to pre-filter results for (i.e. which accounts would you like to download data to the sheet for from your MCC? If you have a larger MCC, I'd recommend pre-filtering to keep the sheet relatively usable!)
After pre-filtering, simply add the script below to your MCC and set it to run however frequently you'd like. Simply replace the MY_SPREADSHEET_URL with the link to the new spreadsheet you've created using the link above. You can also customize the Start Date for reporting at var startDate. See note above startDate in script regarding the End Date for the reporting period.
After the data has been downloaded to the sheet, you should be able to now update your Account Selector on the Performance Comparison tab to filter the page's results for Phrase Match vs. BMM to a specific account. Leave this filter blank to review total results from your MCC. You can select multiple accounts by typing RegEx into this selector (i.e. enter: Account A|Account C|Account D to show data on this sheet for Accounts A, C, and D).
Use filters in Row 14 to check Keyword-specific performance (comparing BMM and Phrase match iterations of the same keyword) and to even filter out performance by specific types of queries using the RegEx query filter.
function main() {
//***START CUSTOM VARIABLES***//
//URL of Google Sheet to push data to
var spreadsheetUrl = 'MY_SPREADSHEET_URL';
//set start date for data pull
//default end date will be "today," but you can edit that by resetting the "endDate" variable below
var startDate = '20210112';
//***END CUSTOM VARIABLES***//
//report type – see documentation here: https://developers.google.com/adwords/api/docs/appendix/reports/account-performance-report
var keywordReportType = 'KEYWORDS_PERFORMANCE_REPORT';
//select metrics from report above – see link from documentation – make sure all terms are comma separated with a space afterwards
var keywordMetrics = 'ExternalCustomerId, CustomerDescriptiveName, CampaignName, AdGroupName, Id, Criteria, KeywordMatchType, Labels, Impressions, Clicks, Cost, Conversions, SearchImpressionShare, SearchRankLostImpressionShare, SearchAbsoluteTopImpressionShare, SearchTopImpressionShare';
//report type – see documentation here: https://developers.google.com/adwords/api/docs/appendix/reports/account-performance-report
var queryReportType = 'SEARCH_QUERY_PERFORMANCE_REPORT';
//select metrics from report above – see link from documentation – make sure all terms are comma separated with a space afterwards
var queryMetrics = 'ExternalCustomerId, CustomerDescriptiveName, CampaignName, AdGroupName, KeywordId, Query, QueryTargetingStatus, KeywordTextMatchingQuery, QueryMatchTypeWithVariant, Impressions, Clicks, Cost, Conversions, AbsoluteTopImpressionPercentage, TopImpressionPercentage';
//set end date to today
var MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
var now = new Date();
var endDate = new Date(now.getTime() - 0 * MILLIS_PER_DAY);
var to = Utilities.formatDate(endDate, "GMT-5", "yyyyMMdd");
Logger.log('Start Date: '+startDate+' End Date: '+to);
var dateRange = startDate+','+to;
//spreadsheet variables
var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
var settingsSheet = spreadsheet.getSheetByName('Settings');
var filterSetting = settingsSheet.getRange('B3').getValue();
Logger.log(filterSetting);
var accountsToFilter = settingsSheet.getRange('B4').getValue();
var keywordSheet = spreadsheet.getSheetByName('Keyword Performance');
var querySheet = spreadsheet.getSheetByName('Search Query Performance');
//check for account filters
if (filterSetting == true) {
var accountFilter = 'ExternalCustomerId IN ['+accountsToFilter+']';
Logger.log('Accounts will be pre-filtered by sheet settings. Accounts: ' + accountsToFilter);
}
else {
var accountFilter = 'ExternalCustomerId NOT_IN [0000000000,1111111111]';
Logger.log('Accounts are not being pre-filtered by the spreadsheet settings.');
}
//account iterator – currently gets all accounts, but can be adjusted to filter for specifics using this documentation:
//https://developers.google.com/google-ads/scripts/docs/examples/ads-manager-scripts
var accountIterator = MccApp.accounts().withCondition('Impressions > 0').withCondition(accountFilter).forDateRange(dateRange).get();
var queryAccountIterator = MccApp.accounts().withCondition('Impressions > 0').withCondition(accountFilter).forDateRange(dateRange).get();
var mccAccount = AdWordsApp.currentAccount();
//clear sheet & add header using metrics variable
keywordSheet.clearContents();
var headers = keywordMetrics.split(', ');
keywordSheet.appendRow(headers);
//pull report and append rows of data
while (accountIterator.hasNext()) {
var account = accountIterator.next();
MccApp.select(account);
//try getting keyword labels
try {
var bmmLabel = AdWordsApp.labels()
.withCondition('Name = "Legacy BMM"')
.get().next();
}
catch(e){
continue;
}
try {
var phraseLabel = AdWordsApp.labels()
.withCondition('Name = "Phrase BMM Replacement"')
.get().next();
}
catch(e){
continue;
}
var report = AdWordsApp.report(
'SELECT ' + keywordMetrics + ' ' +
'FROM ' + keywordReportType + ' ' +
'WHERE Labels CONTAINS_ANY ['+bmmLabel.getId()+','+phraseLabel.getId()+'] ' +
'DURING ' + dateRange);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var rowArray = [];
for (var i = 0; i < headers.length; i++) {
rowArray.push("'"+row[headers[i]]);
}
keywordSheet.appendRow(rowArray);
}
}
var keywordIdArray = [];
var keywordCount = keywordSheet.getRange('F2:F').getValues().filter(String).length + 2;
for (var j = 2; j < keywordCount; j++){
keywordIdArray.push('"'+keywordSheet.getRange('F'+j).getValue()+'"');
}
//clear sheet & add header using metrics variable
querySheet.clearContents();
var queryHeaders = queryMetrics.split(', ');
querySheet.appendRow(queryHeaders);
//pull report and append rows of data
while (queryAccountIterator.hasNext()) {
var queryAccount = queryAccountIterator.next();
MccApp.select(queryAccount);
var queryReport = AdWordsApp.report(
'SELECT ' + queryMetrics + ' ' +
'FROM ' + queryReportType + ' ' +
'WHERE KeywordTextMatchingQuery IN [' + keywordIdArray + '] ' +
'DURING ' + dateRange);
var queryRows = queryReport.rows();
while (queryRows.hasNext()) {
var queryRow = queryRows.next();
var queryRowArray = [];
for (var i = 0; i < queryHeaders.length; i++) {
queryRowArray.push("'"+queryRow[queryHeaders[i]]);
}
querySheet.appendRow(queryRowArray);
}
}
//timestamp sheet
var now = new Date();
var timeZone = AdsApp.currentAccount().getTimeZone();
var dateString = Utilities.formatDate(now, timeZone, 'MMMM dd, yyyy h:mm a');
spreadsheet.getSheetByName('Performance Comparison').getRange('M1').setValue(dateString);
}
For most accounts: seeing lower CTRs and higher CPCs on the new Phrase Match variations of these Keywords accompanied by much stronger Conversion Rates and Costs/Conv. Mostly what you'd expect from the transition. Frustrating to lose some more query data due to more restrictive targeting, though.