Smart search & recommendations (Beta)
Task: a shopper types a query and is shown a list of products. Rank that list so the product they went on to buy comes first. Then do the same with no query at all: the For You shelf, ranked from who the shopper is.
Aito answers both with one recommend query over the shop's own impression log:
rank the products shown, by the probability they get bought, given the query
words, the shopper's profile and the products' attributes. There is no model to
train and no search index to sync.
Available since: v2.9.0. v2.8.4 and earlier do not apply basedOn to
recommend and split a query only on the queried table's own text column, so
the figures below do not describe them.
The data
No public dataset carries everything a smart-search ranker learns from: real query wordings, who searched, and what they bought. Amazon ESCI has queries but no shoppers, MIND has shoppers but no queries, and Diginetica's tokens are anonymized. So this benchmark uses a generated musical-instrument shop whose answer key is known.
| Table | Rows |
|---|---|
| products | 600 |
| users | 3,000 |
| searches | 6,000 |
| impressions (a product shown for a search) | 240,000 |
A purchase comes from a hidden model with three parts:
- intent: what the shopper is in the market for;
- user fit: skill level, budget, favourite brand and instrument;
- season: what sells when.
The query is a lossy wording of the intent. Some shoppers type the catalogue's own words ("electric guitar"), some a concept ("beginner guitar"), some a brand, some slang the catalogue never uses ("axe"), some run the words together, and a few make typos.
Because the generator records every candidate's true score, each result is read against oracles computed on the same searches. An oracle ranks by the true score, so it is the most a ranker could reach from that information. The total oracle is the ceiling for the task.
Scoring uses 300 held-out searches, each with the 40 products actually shown, and nDCG@10 with the bought product as the one relevant item.
Result
| Ranker | nDCG@10 | recall@10 | MRR |
|---|---|---|---|
Aito recommend: query + shopper profile + basedOn | 0.545 | 0.887 | 0.443 |
Aito recommend: query + product attributes (basedOn) | 0.534 | 0.893 | 0.425 |
Aito recommend: query only | 0.452 | 0.757 | 0.365 |
Aito $similarity on the product text | 0.455 | 0.813 | 0.351 |
| BM25 over the product text | 0.454 | 0.813 | 0.351 |
| BM25 over the product text + every past query it was bought under | 0.468 | 0.813 | 0.367 |
Hybrid: recommend (query only) + BM25, tuned | 0.486 | 0.803 | 0.393 |
Hybrid: recommend + basedOn + BM25, tuned | 0.543 | 0.897 | 0.435 |
| Total oracle (ceiling) | 0.605 | ||
| Intent oracle (what the query alone can reach) | 0.530 |
Provenance. dataset smart-search corpus (generated,
benchmarks/search/gen_search_corpus.py) ยท source booktest
SmartSearchV2Benchmark (smartSearchAndForYou and state snapshots) ยท every
figure is a build-time metric token resolved from a committed metrics file.
How to read it.
- Plain text search is a real baseline, and Aito's own ranks like BM25.
$similarityproduces the same ranking as the reference BM25 on 280 of the 300 searches. - The strongest BM25 row is the fair one. It indexes, for each product, every past query it was bought under: the same history Aito learns from, handed to a lexical index. A learned ranker only earns its keep where it beats that row.
basedOnis what makes the difference. It isrecommend's way of scoring products through their attributes (category, subcategory, brand, price tier, skill level). Without it, a query word only says something about the exact products it was bought with. With it, the word's evidence reaches every product in the same subcategory, including ones no shopper has searched that way yet.- The hybrids are tuned on a separate set of searches, never on the scored ones.
Is the difference real?
Every ranker is scored on the same searches, so each comparison is a per-search difference with a 95% bootstrap interval. An interval that contains zero means these searches do not separate the two rankers.
| A vs B | Mean difference in nDCG@10 | 95% interval | Searches A wins | Searches B wins |
|---|---|---|---|---|
recommend + basedOn vs BM25 with past query wordings | +0.066 | +0.041 to +0.092 | 106 | 53 |
recommend + basedOn vs the query-only hybrid | +0.048 | +0.024 to +0.072 | 83 | 37 |
adding the shopper profile to recommend + basedOn | +0.011 | -0.016 to +0.038 | 99 | 86 |
adding BM25 to recommend + basedOn | +0.009 | -0.005 to +0.022 | 24 | 22 |
So recommend with basedOn beats both the fair BM25 index and the older
hybrid. Adding the shopper profile, or blending BM25 back in, does not
measurably change it on these searches.
By how the shopper phrased it
| Query style | Searches | recommend + basedOn | BM25, product text | BM25 + past query wordings |
|---|---|---|---|---|
| catalogue words ("electric guitar") | 190 | 0.571 | 0.537 | 0.550 |
| concept ("beginner guitar") | 37 | 0.617 | 0.470 | 0.539 |
| brand ("fender guitar") | 22 | 0.304 | 0.359 | 0.289 |
| slang the catalogue never uses ("axe") | 40 | 0.423 | 0.210 | 0.229 |
| run-together words ("electricguitar") | 8 | 0.505 | 0.063 | 0.048 |
| typo | 3 | 0.444 | 0.000 | 0.000 |
- Where text search wins: brand queries. The brand name is in the product text, so an exact token match finds the brand directly.
- Where learned evidence wins: every wording the catalogue does not contain.
Slang and run-together words share no token with the product text, so a
lexical index over it cannot rank them.
recommendlearns which products those words led to, andbasedOncarries that to the whole subcategory. - The run-together and typo rows have few searches each. Read them as direction, not digits.
For You: no query at all
| Ranker | nDCG@10 | recall@10 |
|---|---|---|
Aito recommend: shopper profile + basedOn | 0.245 | 0.500 |
Aito recommend: shopper profile | 0.181 | 0.367 |
Aito recommend: the shopper's own id | 0.115 | 0.250 |
| Popularity (smoothed purchase rate) | 0.102 | 0.233 |
| User-fit oracle | 0.235 |
Against popularity, the profile + basedOn shelf gains
+0.143
nDCG@10 (95% interval
+0.106 to
+0.180).
It can sit above the user-fit oracle without leaking the answer. That oracle
scores user fit alone, while a shopper's profile also hints at intent: people
mostly shop for their own instrument.
The query
The smart-search ranking is one request against the impression log. goal is
the outcome to rank for, where is what is known about this search, and
basedOn names the product attributes the evidence should flow through.
{
"from": "impressions",
"where": {
"search.query": "axe for a beginner",
"search.user_skill": "beginner",
"search.user_budget": "budget"
},
"recommend": "product",
"goal": { "purchase": true },
"basedOn": ["category", "subcategory", "brand", "tier", "skill_level"],
"select": ["id", "name", "$p", "$why"],
"limit": 10
}
For the For You shelf, drop search.query and keep the profile. $why breaks
each $p down into the evidence behind it: which query word, which profile
value, and through which attribute.
Honest limits
- A generated corpus, one seed. It is designed so neither text nor profile is enough on its own, and its properties are checked by corpus diagnostics. It is still a model of a shop, not a shop. Real query logs are messier, and the relative standing of text search can differ.
- Profile evidence needs
basedOn. Without it, adding the shopper profile to a query lowers the ranking: 0.413 against the query's 0.452. Per-product evidence for each profile value is thin, and it overlaps what the query already says. Giverecommendthe product attributes when you combine the two. - No latency here. This suite ran in-process on a shared machine; see Query, predict & write speed for measured latency.
- Ties break by product id. The generator lists the bought product early in the shown list, so any ranker that falls back to display order is inflated; every ranker here, the baselines included, breaks ties by id.