Skip to content

Instantly share code, notes, and snippets.

@MatMoore
Created July 30, 2018 10:34
Show Gist options
  • Save MatMoore/9b590c962df233aa998f4608e3c7288d to your computer and use it in GitHub Desktop.
Save MatMoore/9b590c962df233aa998f4608e3c7288d to your computer and use it in GitHub Desktop.
# Full code here:
# https://github.com/googleads/google-api-ads-ruby/blob/master/adwords_api/examples/solutions/conflicting_keywords/conflicting_keywords.rb
def compare_keywords(negatives, positive)
negatives.each do |negative|
match_type = negative.match_type.downcase
negative_text = negative.text.downcase
positive_text = positive.text.downcase
match = false
# If the negative keyword is more strict than the positive one, it cannot
# match.
# E.g. a negative exact "cool shoe" will not prevent positive phrase
# "cool shoe shine".
positive_match_type = positive.match_type.downcase
next if positive_match_type == 'broad' && match_type != 'broad'
next if positive_match_type == 'phrase' && match_type == 'exact'
# Exact matching with negative keywords triggers only when the full text of
# the keywords is exactly the same.
# E.g. a negative "silk scarves" will only match "silk scarves", not
# "red silk scarves".
if match_type == 'exact'
match = (negative_text == positive_text)
end
# Phrase matching with negative keywords triggers when the negative phrase
# is present in the target, completely unmodified.
# E.g. a negative "silk scarves" will match "gift silk scarves", but not
# "silk gift scarves".
if match_type == 'phrase'
negative_tokens = negative_text.split(' ')
positive_tokens = positive_text.split(' ')
positive_tokens.each_with_index do |positive_token, positive_index|
# Iterate until the current token matches the first token in the
# negative keyword.
if positive_token == negative_tokens.first
candidate_match = true
# Do all of the subsequent tokens also match?
negative_tokens[1..-1].each_with_index do |token, index|
if token != positive_tokens[positive_index + index + 1]
candidate_match = false
break
end
end
match = candidate_match
end
end
end
# Broad matching with negative keywords triggers when all of the words are
# present and exactly the same.
# E.g. a negative "silk scarves" will match "silk gift scarves", but not
# "wool scarves".
if match_type == 'broad'
negative_tokens = negative_text.split(' ')
positive_tokens = positive_text.split(' ')
candidate_match = true
negative_tokens.each do |token|
if !positive_tokens.include?(token)
candidate_match = false
break
end
end
match = candidate_match
end
negative.add_blocked(positive) if match
end
end
def compare_keywords(negatives, positive):
for negative in negatives:
match_type = negative.match_type.downcase
negative_text = negative.text.downcase
positive_text = positive.text.downcase
positive_match_type = positive.match_type.downcase
match = false
if positive_match_type == 'broad' && match_type != 'broad':
continue
if positive_match_type == 'phrase' && match_type == 'exact':
continue
if match_type == 'exact':
match = (negative_text == positive_text)
if match_type == 'phrase':
negative_tokens = negative_text.split(' ')
positive_tokens = positive_text.split(' ')
for positive_token, positive_index in positive_tokens:
# Iterate until the current token matches the first token in the
# negative keyword.
if positive_token == negative_tokens.first:
candidate_match = true
# Do all of the subsequent tokens also match?
for token, index in negative_tokens[‘1..-1’]:
if token != positive_tokens[positive_index + index + 1]:
candidate_match = false
match = candidate_match
if match_type == 'broad':
negative_tokens = negative_text.split(' ')
positive_tokens = positive_text.split(' ')
candidate_match = true
for token in negative_tokens:
if positive_tokens.include(token):
candidate_match = false
match = candidate_match
negative.add_blocked(positive) if match
@MatMoore
Copy link
Author

Things that jump out at me:

Syntax

  • && in ruby is and in python
  • .downcase is .lower()
  • for positive_token, positive_index in positive_tokens: should be for positive_index, positive_token in enumerate(positive_tokens):. The enumerate function takes a sequence and transforms it to a sequence of pairs, where the first item is the index and the second is the value.
  • negative_tokens[‘1..-1’] should be negative_tokens[1:] I think - read up on slices if it turns out to be off by one
  • positive_tokens.include(token) should be token in positive_tokens
  • negative.add_blocked(positive) if match isn't valid python - you need to do a full if statement like above

Also - if you install a command line tool like flake8 it can report small errors like this without having to run the code 👍

Inputs and outputs

The function assumes you're passing it a list of objects with the following methods:

  • match_type
  • text
  • add_blocked(positive)

In the ruby version add_blocked is implemented like this:

  def add_blocked(keyword)
    @blocked_examples << keyword if @blocked < 5
    @blocked += 1
  end

which basically says "add the blocked keywords to a list associated with the negative keyword, but only keep the first 5 if this is called multiple times"

So to use the function you'll need something equivalent to this to be defined in your python code somewhere:

class Negative:
  def __init__(self, match_type, text):
    self.match_type = match_type
    self.text = text
    self.blocked_examples = []

  def add_blocked(self, keyword):
     if len(blocked_examples) < 5:
       self.blocked_examples.append(keyword)

class Positive:
  def __init__(self, match_type, text):
    self.match_type = match_type
    self.text = text

if it's already part of a library you're using then you'll just need to import it wherever you call the function, but you might need to change how it's used in the function if it doesn't match up with the ruby version (eg if the properties have different names).

Testing

As long as you have those classes defined, you should be able to test it with something like this:

# Inputs
negative_foo = Negative(match_type='exact', text='foo')
negative_bar = Negative(match_type='broad', text='bar')

negatives = [negative_foo, negative_bar]
positive = Positive(match_type='exact', text='foo')

# Run the thing
compare_keywords(negatives, positive)

# Outputs
assert negative_foo.blocked_examples == [positive]
assert negative_bar.blocked_examples == []

I recommend using something like pytest for writing tests as it will let you define multiple tests and show you which ones pass or fail: https://docs.pytest.org/en/latest/getting-started.html

You can write tests for each match_type to make sure the logic works as you expect (or if you aren't using all the match types then you could just delete that part of that code)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment