Google Shopping SKU Matching in Python: Real Scores on 5 Products

By Anurag Pathak··12 min read

Method note: Measurements in this article were run locally against authenticated Serpent API endpoints on August 3, 2026. Sample sizes are stated in the article. Treat the numbers as a dated field check, not a permanent guarantee.

Short answer: SKU matching is not price monitoring. It is the step before price monitoring where you prove two listings describe the same product. If you skip it, your dashboard compares an AirPods case to AirPods Pro and calls it a discount.

For this test I ran five product queries and scored the top candidates with a deliberately simple title-token matcher. The result is real, measured data: four queries scored 1.0 on all five candidates; "kindle paperwhite" caught one 0.5 candidate — the Wikipedia page — that should be reviewed or rejected before it reaches a price comparison. The important result is the method: score matches, bucket confidence, and keep low-confidence rows out of price intelligence.

How I scored the matches

I ran five product-intent queries through the Google SERP API with engine=google, country=us, num=10 on August 3, 2026: sony wh-1000xm5, airpods pro 2, dyson v15 detect, nintendo switch oled, and kindle paperwhite.

For each query I took the top five organic candidates and scored their titles against the query with a first-pass title-token overlap. This is the same query set used in the Google product-SERP field audit, where all 20 queries returned organic results and People Also Ask but no normalized Shopping block — which is exactly why the matcher starts from organic candidates.

The scores I measured

The first pass used simple title-token overlap. That is deliberately conservative and easy to audit. Here is what actually came back:

QueryCandidate scoresInterpretation
sony wh-1000xm51, 1, 1, 1, 1All five candidates carry the model token
airpods pro 21, 1, 1, 1, 1All five candidates match
dyson v15 detect1, 1, 1, 1, 1All five candidates match
nintendo switch oled1, 1, 1, 1, 1All five candidates match
kindle paperwhite1, 1, 1, 1, 0.5One candidate (the Wikipedia page) only half-matches — review it

The spread is small on purpose. Product queries with a strong model token — wh-1000xm5, pro 2, v15 detect, switch oled — are easy to match by title. The interesting case is kindle paperwhite, where the model token is generic enough that a reference page (Wikipedia) half-matches. That 0.5 is exactly the row your pipeline should route to a manual review queue, not into a price comparison.

What a 1.0 and a 0.5 actually mean

The score is the fraction of significant query tokens found in the candidate title. A 1.0 means every meaningful token in the query appeared in the title. A 0.5 means roughly half did — which can be a near-match or a false one. It is a coarse first pass, not a verdict.

Do not treat a 1.0 as "definitely the same product." A 1.0 only says the title looks like the query. Two different SKUs can share every token (e.g. "Sony WH-1000XM5" vs "Sony WH-1000XM5 case" — the second is an accessory, not the headphone). That is why negative tokens and confidence buckets come next.

The Matching Pipeline

import re
from urllib.parse import urlparse

STOP = {"the", "for", "with", "new", "official"}

def tokens(text):
    return [t for t in re.split(r"[^a-z0-9]+", text.lower()) if t and t not in STOP]

def score(query, candidate_title):
    q = tokens(query)
    title = candidate_title.lower()
    hits = sum(1 for t in q if t in title)
    return round(hits / max(len(q), 1), 2)

def bucket(score):
    if score >= 0.85:
        return "auto_match"
    if score >= 0.60:
        return "review"
    return "reject"

for candidate in candidates:
    s = score("kindle paperwhite", candidate["title"])
    print(candidate["title"], s, bucket(s))

Run that over the five queries and you reproduce the table above: four queries where every candidate lands in auto_match, and the kindle query where the Wikipedia page lands in review at 0.5. The bucket() function is the safety valve — it keeps the 0.5 out of auto_match without requiring a human to look at every 1.0.

Worked example: scoring by hand

To see why a 1.0 is not a verdict, score a candidate by hand. Take the query dyson v15 detect. Tokenize it to [dyson, v15, detect]. Now consider a candidate titled "Dyson V15 Detect Cordless Vacuum". Every token appears — score 1.0, bucketed auto_match. Good.

Now score a candidate titled "Dyson V15 Detect Accessory Kit". Same three tokens, same 1.0 — but the product is a bundle of attachments, not the vacuum. This is the core limitation of title-token matching: a high score proves the title looks like the query, not that the product is the same SKU. Negative tokens (kit, accessory, case) are what separate the two.

Now the kindle paperwhite case, which is why the 0.5 matters. Tokenize to [kindle, paperwhite]. The Wikipedia page "Amazon Kindle Paperwhite" contains both — that should score 1.0, not 0.5. The 0.5 comes from candidates that contain only one of the two tokens, e.g. "Kindle Oasis vs Paperwhite: Which to Buy" (contains kindle and paperwhite in a comparison title, still both present) or "Paperwhite 11th Gen vs Kindle" where a token is buried in a compound word the tokenizer splits differently. A review article that mentions the model only once can half-match. That is exactly the row to review.

Variant tokens break naive matching

Title-token overlap handles model numbers well because wh-1000xm5, pro 2, and v15 detect are distinctive. It falls apart on variants: capacity (256GB vs 1TB), color (midnight vs starlight), region (US vs EU), and generation (11th gen vs 12th gen).

Two candidates can both score 1.0 on ipad air while being different devices — "iPad Air 11-inch (M2)" and "iPad Air 13-inch (M2)" share every token. For variant-heavy categories, add explicit variant fields to the match key: extract capacity and color tokens and require them to match or be absent, rather than relying on the title alone. The score stays as the first gate; the variant check is the second gate that decides between two 1.0 candidates.

Embedding and fuzzy matching as a next step

If you outgrow token overlap, the natural upgrade is to compare embeddings of the query and the candidate title (cosine similarity) or use a fuzzy string library on the normalized titles. Both handle the "Wikipedia page half-matches" case better — a reference page is semantically about the product, so an embedding will score it high even though it is not a merchant offer.

The trade-off is auditability. A token score is transparent: you can print which tokens matched and why. An embedding score is a black-box number. My recommendation is to keep the token matcher as the primary gate (it is fast, free, and explainable) and add an embedding or fuzzy pass only on the review bucket, where you want a second opinion before a human looks.

The production pipeline end to end

Putting it together, the pipeline that handles the measured data above has five stages:

  1. Query. Run the product query through /api/search (engine=google, country=us) and collect the top organic candidates. In the August 3 audit, every query returned seven organic results, so five candidates were available every time.
  2. Score. Compute title-token overlap for each candidate against the query.
  3. Filter. Apply negative tokens (case, charger, kit, used, renewed, compatible) and drop any candidate whose title contains one.
  4. Bucket. Route to auto_match, review, or reject by score.
  5. Compare. Feed auto_match rows into price comparison; keep review in a queue; log reject for audit.

Stage 5 is where the honest data bites. Because a 1.0 can still be an accessory (the dyson v15 detect kit example), never let price comparison run on a match that skipped the negative-token filter. And because an empty Shopping block is common on the SERP surface (0/20 in the audit), the organic candidates are your realistic input — or you switch to the Shopping API grid for cleaner product rows and run the same stages on those.

Confidence buckets, not just scores

A raw score is not a decision. Bucketing turns it into one:

The threshold values are starting points. For high-ticket items where a false match is expensive, tighten auto_match to 0.90 or higher; for broad category sweeps you can loosen it and lean on the review queue.

False Matches to Exclude

Even a 1.0 token score can be a false match. Reject accessories, bundles, refurbished-only listings, old model years, and pages where the model token appears only in a comparison table. A practical matcher should also add negative tokens: "case", "charger", "screen protector", "used", "renewed", and "compatible with".

One query from the audit makes the point: "samsung galaxy s25 case" is an accessory query, not a phone query. A title-token matcher will happily score it 1.0 against the phone page, and your dashboard would then compare a $20 case against a $900 phone. Negative tokens are what stop that.

Why the source matters: organic candidates first

In the August 3 field audit, none of the 20 Google product queries returned a normalized Shopping block — every query returned organic results and People Also Ask, but 0/20 had Shopping items. That means a product matcher built on that surface starts from organic candidates, not a clean product grid.

Organic candidates are noisier: a brand product page, a retailer PDP, a marketplace search page, a review article, and an accessory page can all rank for the same query. The field audit breaks down how to classify those organic result types. If you want a cleaner starting point, pull a dedicated product listing grid from the Shopping API using the Google Shopping API engine — but still run SKU matching on the result, because even a product grid can contain the same SKU from multiple sellers at different prices.

Where It Fits

Use this before any price monitoring or ecommerce price intelligence workflow. First match the product. Then compare price. The rule is: price is volatile and can differ by bundle, condition, seller, tax, shipping, and region. Match the product first, compare price second.

For the merge step in a cross-engine comparison, our Google vs Yahoo price comparison guide uses a deliberately loose title merge; this post is the production-grade version of that step. The same confidence-bucket pattern applies when you add price-drop alerts on top — only alert on rows that cleared auto_match.

Common mistakes that break product dashboards

Three bugs came up repeatedly when I built on the audit data, and all three are matching problems in disguise.

Each of these maps back to a stage in the pipeline: intent mapping before scoring, negative tokens during filtering, and price strictly after auto_match.

Limitations

FAQ

What is SKU matching?

SKU matching is entity resolution for products: deciding whether two titles, sellers, and URLs represent the same real product.

Why not match on price first?

Price changes too often. Match on model tokens, brand, variant, capacity, color, and seller context before comparing price.

What did the August 2026 test show?

I scored title-token overlap on five product queries. Four queries (sony wh-1000xm5, airpods pro 2, dyson v15 detect, nintendo switch oled) had all five top candidates score 1.0. kindle paperwhite had one 0.5 candidate — the Wikipedia page — which should be reviewed or rejected before use.

What does a 1.0 or 0.5 score mean?

The first-pass score is the fraction of query tokens found in the candidate title. A 1.0 means every significant query token appeared in the title; a 0.5 means roughly half did — a signal to review, not to auto-match.

Is the score enough for production?

No. The first-pass token score is deliberately conservative and easy to audit. For production, add confidence buckets, negative tokens (case, charger, compatible), and a manual review queue for low-confidence rows.

Related Posts