Monday, 23 Dec 2024

Spell-check your new expanded text ads with this AdWords script

Expanded text ads are here, and advertisers will likely be doing significant ad copy revisions as a result. Luckily, columnist Russell Savage reminds us that you can use AdWords scripts to ensure that your new ads are devoid of spelling errors.

Well, it looks like we’re all going to be writing a lot of new ad copy. The recent release of expanded text ads provides us with 45 additional characters for our AdWords ads, which we can use to give searchers more context to make decisions. But it also provides us with an opportunity for making mistakes.

I always try to leverage AdWords scripts as a way to minimize mistakes, or to catch those mistakes as quickly as possible. With that in mind, I figured I’d share a spell-checking script for verifying that any active ad you write has everything spelled correctly.

For this script, I am leveraging the Bing Spell Check API. Yes, that’s right — I’m using Google to call a Bing API. It just so happens Bing has a pretty awesome API that lets you build the power of Microsoft Word’s spell-check into any application, and all you need to do is register to get a key.

The free account only gives you 5,000 API calls a month, so I tried to design the script to leverage caching as much as possible. It will cache the previous call, as well as a list of misspelled words that it stores on your Google Drive. If any of these words show up in your ad, it will send that data back immediately without calling the API.

/******************************************
* Bing Spellchecker API v1.0
* By: Russ Savage (@russellsavage)
* Usage:
* // You will need a key from
* // https://www.microsoft.com/cognitive-services/en-us/bing-spell-check-api/documentation
* // to use this library.
* var bing = new BingSpellChecker({
* key : ‘xxxxxxxxxxxxxxxxxxxxxxxxx’,
* toIgnore : [‘list’,’of’,’words’,’to’,’ignore’],
* enableCache : true // <- stores data in a file to reduce api calls
* });
* // Example usage:
* var hasSpellingIssues = bing.hasSpellingIssues(‘this is a speling error’);
******************************************/
function BingSpellChecker(config) {
this.BASE_URL = ‘https://api.cognitive.microsoft.com/bing/v5.0/spellcheck’;
this.CACHE_FILE_NAME = ‘spellcheck_cache.json’;
this.key = config.key;
this.toIgnore = config.toIgnore;
this.cache = null;
this.previousText = null;
this.previousResult = null;
this.delay = (config.delay) ? config.delay : 60000/7;
this.timeOfLastCall = null;
this.hitQuota = false;
// Given a set of options, this function calls the API to check the spelling
// options:
// options.text : the text to check
// options.mode : the mode to use, defaults to ‘proof’
// returns a list of misspelled words, or empty list if everything is good.
this.checkSpelling = function(options) {
if(this.toIgnore) {
options.text = options.text.replace(new RegExp(this.toIgnore.join(‘|’),‘gi’), );
}
options.text = options.text.replace(/{.+}/gi, );
options.text = options.text.replace(/[^a-z ]/gi, ).trim();
if(options.text.trim()) {
if(options.text == this.previousText) {
Logger.log(‘INFO: Using previous response.’);
return this.previousResult;
}
if(this.cache) {
var words = options.text.split(/ +/);
for(var i in words) {
Logger.log(‘INFO: checking cache: ‘+words[i]);
if(this.cache.incorrect[words[i]]) {
Logger.log(‘INFO: Using cached response.’);
return [{“offset”:1,“token”:words[i],“type”:“cacheHit”,“suggestions”:[]}];
}
}
}
var url = this.BASE_URL;
var config = {
method : ‘POST’,
headers : {
‘Ocp-Apim-Subscription-Key’ : this.key,
‘Content-Type’ : ‘application/x-www-form-urlencoded’
},
payload : ‘Text=’+encodeURIComponent(options.text),
muteHttpExceptions : true
};
if(options && options.mode) {
url += ‘?mode=’+options.mode;
} else {
url += ‘?mode=proof’;
}
if(this.timeOfLastCall) {
var now = Date.now();
if(now this.timeOfLastCall < this.delay) {
Logger.log(Utilities.formatString(‘INFO: Sleeping for %s milliseconds’,
this.delay (now this.timeOfLastCall)));
Utilities.sleep(this.delay (now this.timeOfLastCall));
}
}
var resp = UrlFetchApp.fetch(url, config);
this.timeOfLastCall = Date.now();
if(resp.getResponseCode() != 200) {
if(resp.getResponseCode() == 403) {
this.hitQuota = true;
}
throw JSON.parse(resp.getContentText()).message;
} else {
var jsonResp = JSON.parse(resp.getContentText());
this.previousText = options.text;
this.previousResult = jsonResp.flaggedTokens;
for(var i in jsonResp.flaggedTokens) {
this.cache.incorrect[jsonResp.flaggedTokens[i].token] = true;
}
return jsonResp.flaggedTokens;
}
} else {
return [];
}
};
// Returns true if there are spelling mistakes in the text toCheck
// toCheck : the phrase to spellcheck
// returns true if there are words misspelled, false otherwise.
this.hasSpellingIssues = function(toCheck) {
var issues = this.checkSpelling({ text : toCheck });
return (issues.length > 0);
};
// Loads the list of misspelled words from Google Drive.
// set config.enableCache to true to enable.
this.loadCache = function() {
var fileIter = DriveApp.getFilesByName(this.CACHE_FILE_NAME);
if(fileIter.hasNext()) {
this.cache = JSON.parse(fileIter.next().getBlob().getDataAsString());
} else {
this.cache = { incorrect : {} };
}
}
if(config.enableCache) {
this.loadCache();
}
// Called when you are finished with everything to store the data back to Google Drive
this.saveCache = function() {
var fileIter = DriveApp.getFilesByName(this.CACHE_FILE_NAME);
if(fileIter.hasNext()) {
fileIter.next().setContent(JSON.stringify(this.cache));
} else {
DriveApp.createFile(this.CACHE_FILE_NAME, JSON.stringify(this.cache));
}
}
}

I’d like to share a quick MCC-level script for checking ad copy as well. I leverage something like this script sort of like a guardian angel. It is always running in the background of my MCC, checking for ads that have issues. When it finds one, it will mark it with a label, which lets me know I need to make some changes. If you wanted to be more robust, you could configure this to send you an email (or a Slack message) whenever it finds an issue.

It’s very possible that this script will run out of time or quota from the Bing API before completing successfully. This is fine, as the number of ads it operates on will continue to shrink over time. Eventually, all your ads will have a green label indicating they have been checked, and any new ads will automatically be checked as they are added.

var CHECKED_LABEL_NAME = “Spellchecked”;
var ISSUE_LABEL_NAME = “Spelling Issue”;
function main() {
var bing = new BingSpellChecker({
key : ‘xxxxxxxxxxxxxxxxxxx’,
toIgnore : [‘adwords’,‘adgroup’,‘russ’],
enableCache : true
});
var accountIter = MccApp.accounts().get();
while(accountIter.hasNext()) {
MccApp.select(accountIter.next());
checkAds(bing);
if(bing.hitQuota) {
break;
}
}
bing.saveCache();
}
function checkAds(bing) {
createLabelIfNeeded(CHECKED_LABEL_NAME,‘Indicates an entity was spell checked’,‘#00ff00’ /*green*/);
createLabelIfNeeded(ISSUE_LABEL_NAME,‘Indicates an entity has a spelling issue’,‘#ff0000’ /*red*/);
var adIter = AdWordsApp.ads()
.withCondition(“Status = ENABLED”)
.withCondition(Utilities.formatString(“LabelNames CONTAINS_NONE [‘%s’,’%s’]”,
CHECKED_LABEL_NAME,
ISSUE_LABEL_NAME))
.get();
while(adIter.hasNext() && !bing.hitQuota) {
var ad = adIter.next();
var textToCheck = “”;
if (ad.getType() == “EXPANDED_TEXT_AD”) {
var expandedTextAd = ad.asType().expandedTextAd();
textToCheck = [
expandedTextAd.getHeadlinePart1(),
expandedTextAd.getHeadlinePart2(),
expandedTextAd.getDescription()
].join(‘ ‘);
} else {
textToCheck = [
ad.getHeadline(),
ad.getDescription1(),
ad.getDescription2()
].join(‘ ‘);
}
try {
var hasSpellingIssues = bing.hasSpellingIssues(textToCheck);
if(hasSpellingIssues) {
ad.applyLabel(ISSUE_LABEL_NAME);
} else {
ad.applyLabel(CHECKED_LABEL_NAME);
}
} catch(e) {
// This probably means you’re out of quota.
// You can pick up from here next time.
Logger.log(‘INFO: ‘+e);
break;
}
if(!AdWordsApp.getExecutionInfo().isPreview() &&
AdWordsApp.getExecutionInfo().getRemainingTime() < 60) {
// Out of time
Logger.log(“INFO: Ran out of time. Will continue next run.”);
break;
}
}
}
//This is a helper function to create the label if it does not already exist
function createLabelIfNeeded(name,description,color) {
if(!AdWordsApp.labels().withCondition(“Name = ‘”+name+“‘”).get().hasNext()) {
AdWordsApp.createLabel(name,description,color);
}
}
/******************************************
* Bing Spellchecker API v1.0
* By: Russ Savage (@russellsavage)
* Usage:
* // You will need a key from
* // https://www.microsoft.com/cognitive-services/en-us/bing-spell-check-api/documentation
* // to use this library.
* var bing = new BingSpellChecker({
* key : ‘xxxxxxxxxxxxxxxxxxxxxxxxx’,
* toIgnore : [‘list’,’of’,’words’,’to’,’ignore’],
* enableCache : true // <- stores data in a file to reduce api calls
* });
* // Example usage:
* var hasSpellingIssues = bing.hasSpellingIssues(‘this is a speling error’);
******************************************/
function BingSpellChecker(config) {
this.BASE_URL = ‘https://api.cognitive.microsoft.com/bing/v5.0/spellcheck’;
this.CACHE_FILE_NAME = ‘spellcheck_cache.json’;
this.key = config.key;
this.toIgnore = config.toIgnore;
this.cache = null;
this.previousText = null;
this.previousResult = null;
this.delay = (config.delay) ? config.delay : 60000/7;
this.timeOfLastCall = null;
this.hitQuota = false;
// Given a set of options, this function calls the API to check the spelling
// options:
// options.text : the text to check
// options.mode : the mode to use, defaults to ‘proof’
// returns a list of misspelled words, or empty list if everything is good.
this.checkSpelling = function(options) {
if(this.toIgnore) {
options.text = options.text.replace(new RegExp(this.toIgnore.join(‘|’),‘gi’), );
}
options.text = options.text.replace(/{.+}/gi, );
options.text = options.text.replace(/[^a-z ]/gi, ).trim();
if(options.text.trim()) {
if(options.text == this.previousText) {
Logger.log(‘INFO: Using previous response.’);
return this.previousResult;
}
if(this.cache) {
var words = options.text.split(/ +/);
for(var i in words) {
Logger.log(‘INFO: checking cache: ‘+words[i]);
if(this.cache.incorrect[words[i]]) {
Logger.log(‘INFO: Using cached response.’);
return [{“offset”:1,“token”:words[i],“type”:“cacheHit”,“suggestions”:[]}];
}
}
}
var url = this.BASE_URL;
var config = {
method : ‘POST’,
headers : {
‘Ocp-Apim-Subscription-Key’ : this.key,
‘Content-Type’ : ‘application/x-www-form-urlencoded’
},
payload : ‘Text=’+encodeURIComponent(options.text),
muteHttpExceptions : true
};
if(options && options.mode) {
url += ‘?mode=’+options.mode;
} else {
url += ‘?mode=proof’;
}
if(this.timeOfLastCall) {
var now = Date.now();
if(now this.timeOfLastCall < this.delay) {
Logger.log(Utilities.formatString(‘INFO: Sleeping for %s milliseconds’,
this.delay (now this.timeOfLastCall)));
Utilities.sleep(this.delay (now this.timeOfLastCall));
}
}
var resp = UrlFetchApp.fetch(url, config);
this.timeOfLastCall = Date.now();
if(resp.getResponseCode() != 200) {
if(resp.getResponseCode() == 403) {
this.hitQuota = true;
}
throw JSON.parse(resp.getContentText()).message;
} else {
var jsonResp = JSON.parse(resp.getContentText());
this.previousText = options.text;
this.previousResult = jsonResp.flaggedTokens;
for(var i in jsonResp.flaggedTokens) {
this.cache.incorrect[jsonResp.flaggedTokens[i].token] = true;
}
return jsonResp.flaggedTokens;
}
} else {
return [];
}
};
// Returns true if there are spelling mistakes in the text toCheck
// toCheck : the phrase to spellcheck
// returns true if there are words misspelled, false otherwise.
this.hasSpellingIssues = function(toCheck) {
var issues = this.checkSpelling({ text : toCheck });
return (issues.length > 0);
};
// Loads the list of misspelled words from Google Drive.
// set config.enableCache to true to enable.
this.loadCache = function() {
var fileIter = DriveApp.getFilesByName(this.CACHE_FILE_NAME);
if(fileIter.hasNext()) {
this.cache = JSON.parse(fileIter.next().getBlob().getDataAsString());
} else {
this.cache = { incorrect : {} };
}
}
if(config.enableCache) {
this.loadCache();
}
// Called when you are finished with everything to store the data back to Google Drive
this.saveCache = function() {
var fileIter = DriveApp.getFilesByName(this.CACHE_FILE_NAME);
if(fileIter.hasNext()) {
fileIter.next().setContent(JSON.stringify(this.cache));
} else {
DriveApp.createFile(this.CACHE_FILE_NAME, JSON.stringify(this.cache));
}
}
}

We all make mistakes sometimes, but with the help of AdWords scripts watching over us, we can sleep a little easier at night. Now get out there and start thinking of some new ad copy!