Google Shopping SKU Matching in Python: Real Scores on 5 Products
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:
| Query | Candidate scores | Interpretation |
|---|---|---|
| sony wh-1000xm5 | 1, 1, 1, 1, 1 | All five candidates carry the model token |
| airpods pro 2 | 1, 1, 1, 1, 1 | All five candidates match |
| dyson v15 detect | 1, 1, 1, 1, 1 | All five candidates match |
| nintendo switch oled | 1, 1, 1, 1, 1 | All five candidates match |
| kindle paperwhite | 1, 1, 1, 1, 0.5 | One 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:
- 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. - Score. Compute title-token overlap for each candidate against the query.
- Filter. Apply negative tokens (
case,charger,kit,used,renewed,compatible) and drop any candidate whose title contains one. - Bucket. Route to
auto_match,review, orrejectby score. - Compare. Feed
auto_matchrows into price comparison; keepreviewin a queue; logrejectfor 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:
auto_match(≥ 0.85). Safe to feed into price comparison when no negative token is present.review(0.60–0.84). Route to a manual queue or an enrichment step (check the URL host, model number, variant).reject(< 0.60). Do not compare. Store the row for audit but exclude it from price intelligence.
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.
- Treating a successful API response as a successful product-offer response. A query can return HTTP 200, seven organic results, and People Also Ask while carrying no Shopping items at all (0/20 in the audit). If your dashboard expects one matched SKU per query, an empty Shopping block should be a distinct state, not a failed job.
- Mixing query intent. "iPad Air 13 inch" can surface Apple pages, retailer pages, support pages, and comparison articles. "Samsung Galaxy S25 case" is an accessory query, not a phone query. A matcher keyed only on the model token will happily score both at 1.0 — the negative-token filter is what separates them.
- Using price as a matching feature too early. Price is volatile and differs by bundle, condition, seller, tax, shipping, and region. Match the product first, compare price second. The moment you let price influence whether two rows are "the same product," a sale looks like a new SKU.
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
- Title-token overlap is a first pass. It does not handle variant tokens well (capacity, color, region) on its own — those need extra rules.
- One country, one date, five queries. The scores are a dated field check, not a guarantee for every query or market.
- The matcher used organic candidates because the Shopping block was absent in the audit run. A dedicated Shopping API grid gives cleaner input but still needs matching logic.
- Illustrative candidate titles in the worked example are shown to explain scoring; the measured score table is the real result.
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.



