On a multi-vendor marketplace, search is not a feature — it is the store. Thousands of products from vendors who never coordinated their naming, browsed by customers who type three words and judge you on the first five results. This article describes the search architecture we run in production — a hybrid of keyword and semantic retrieval — and, more usefully, the dozen small decisions around it that separate search that demos well from search that sells.
The two layers: keyword and semantic
Layer one is a keyword engine — in our stack, Meilisearch. It's ranked BM25-style lexical matching with millisecond latency, typo tolerance ("creatne" → creatine), facets for filtering, and synonym support. For the majority of ecommerce queries — a product name, a brand, a category word — this layer alone is excellent, and it's cheap to run.
Layer two is semantic retrieval — product embeddings in a vector database (Qdrant, in our case), queried with an embedding of the user's search. It matches meaning: "something for post-workout recovery" has no lexical overlap with "whey hydrolysate", yet lands near it in embedding space. This is the layer that rescues intent queries, long natural-language queries, and cross-language queries.
Neither layer is sufficient alone. Pure keyword search returns nothing for intent phrasing. Pure semantic search is dangerous in the other direction: it always returns something, including confident look-alikes for a brand you don't carry — and it never says "no results", which sounds nice until you realize "no results, but here's a similar brand" is sometimes exactly the honest answer.
Fusing them: when each layer should win
We merge the two result lists with reciprocal rank fusion (RRF) — a simple, robust algorithm that rewards products ranked well by both layers without needing the two scores to be comparable. But the more interesting engineering decision is when to run the semantic layer at all. Running embeddings on every keystroke is wasteful and often harmful; we trigger hybrid retrieval selectively:
- Longer queries (several tokens) — short queries are almost always lexical.
- Intent-shaped phrasing — words like "similar", "like", "alternative", "for …".
- Cross-language signals — Cyrillic characters or local-language diacritics in a mostly-English index.
- Weak lexical recall — when the keyword layer comes back thin, semantics get a chance to fill in.
One more ranking guard that pays off: an adaptive minimum relevance score that scales with query length. A one-word query like "protein" should be permissive — the user wants the category. A five-word query is specific, and weak matches should be cut rather than padded. A fixed threshold cannot do both; a threshold that grows with token count can.
Search is a data problem wearing an algorithm costume
The biggest search-quality gains we've ever shipped were not ranking changes — they were enrichment changes. The pattern worth stealing: push understanding into the index at write time instead of hardcoding it at query time.
The searchable text of each product is assembled during enrichment to include its goal tags, diet tags and product subtype. That's why a query like "vegan protein" works with no hardcoded query→filter rules: the word "vegan" is simply in the searchable text of vegan products, because enrichment put it there from a verified data attribute. Every hardcoded rule you avoid is a rule that can't go stale.
Enrichment also generates 5–15 search synonyms per product, per language — the colloquial names, abbreviations and misspellings customers actually use. And variant grouping collapses eighteen flavor/size variants into one result via a distinct-on-parent rule, which fixes the most common marketplace search complaint: a first page filled with one product in different colors.
Typo tolerance is a default you should selectively disable. We turn it off for SKU, brand and subtype fields — because in supplements, fuzzy matching cheerfully turns "EAA" into "BCAA", which are different products, and a typo-corrected brand match can hide the honest "we don't carry this brand" answer. Typo tolerance on descriptions: yes. On identifiers: never.
Multilingual search: the part everyone underestimates
Our marketplace serves customers in Estonian, English and Russian. Multilingual search is not "translate the index":
- Native fields per locale. Each product carries per-language searchable text, names and synonyms — generated natively during enrichment, not machine-translated, because customers search in words a translator wouldn't choose.
- A cross-language synonym dictionary. Dozens of curated entries mapping local vocabulary to catalog vocabulary — the Estonian taimne and the Russian растительный both reach "vegan" products. Curated deliberately, not generated blindly: synonym dictionaries are where subtle product distinctions go to die if you're careless (see EAA vs BCAA above).
- Fallback to the primary language. A query in Estonian searches Estonian fields first but falls back to English fields, because vendor data is never uniformly localized.
- Embeddings as the safety net. Vector similarity is largely language-agnostic, so the semantic layer quietly catches cross-language queries the lexical layer misses.
If you operate in a small-language market, this is a competitive moat hiding in plain sight: global platforms tune for English, and local customers can tell.
Five details that quietly decide search quality
- The empty state is a feature. When a query looks like a brand you don't carry, say so — a dedicated "we don't carry this brand, here are alternatives" state converts better than pretending the look-alikes are the answer.
- Prefer in-stock variants post-search. When a variant family matches, surface an in-stock flavor rather than the exact-match sold-out one.
- Never navigate on keystrokes. Live-filtering must not trigger router navigation per keystroke — an easy frontend mistake that unmounts the search input mid-typing. Search UX bugs read as "search is bad" to users, even when ranking is perfect.
- Facets come from enrichment. Filters are only as good as the attributes behind them; a "form: powder/capsules" facet exists because enrichment normalized that field across every vendor.
- Index hygiene beats index size. Products without images, discontinued items and orphaned variants get dropped at ingest. A smaller index of sellable products outperforms a complete index of noise.
The operational layer: exclusions, compliance, choke points
Marketplace search has a property single-store search doesn't: vendors come and go. When a vendor is disabled, every one of their products must vanish from every surface immediately — search, category pages, recommendations, embeds. The architectural rule we learned the hard way: route every search path through one choke point and apply exclusions there. Our first implementation filtered disabled vendors in the main search service — and missed a second, direct-to-index fast path that kept serving their products. If exclusion logic lives in two places, one of them is wrong already.
The same choke-point thinking applies to compliance. Under the EU Omnibus rules, showing "was €30, now €20" requires a provable lowest-prior-price; our pricing display routes through a single gate that fails closed — no substantiated prior price, no discount badge, on any surface. Search result cards, product pages and category shelves all call the same gate, so there is no surface where an unlawful claim can leak through independently.
Measuring it: the query→click join
The instrumentation that actually improves search is unglamorous:
- Zero-result rate, with the actual failing queries reviewed weekly — each one is either a synonym to add, an enrichment gap, or a product to source.
- The query→click join. Every search event carries an ID that the subsequent click event references, so "what did people click after searching X" is a query. This join is ground truth: it finds queries where position one is being skipped (ranking problem), queries with clicks but no add-to-cart (product/price problem), and queries abandoned entirely (catalog problem).
- Search→cart conversion per query class, which tells you whether semantic retrieval is actually earning its latency on intent queries.
Search quality is not a launch milestone; it's a weekly habit of reading these three reports and fixing the top offender.
Key takeaways
- Hybrid = keyword by default, semantic on trigger, merged with rank fusion — not embeddings on every keystroke.
- Push understanding into the index at write time (enriched searchable text, per-language synonyms) instead of hardcoding query rules.
- Disable typo tolerance on identifiers — brands, SKUs, subtypes — where fuzziness sells the wrong product.
- Multilingual means native fields + curated cross-language synonyms, with embeddings as the safety net.
- One choke point for exclusions and compliance — logic that lives in two places is already wrong in one.
- Instrument the query→click join — it's the ground truth that turns search tuning from taste into evidence.
Frequently asked questions
What is hybrid search in ecommerce?
Is semantic search always better?
How do you handle multilingual search?
Which search metrics matter most?
Try it on a live catalog.
The search described here runs in production on the marketplace we operate. Type a typo, try Estonian, look for a brand we don't carry.
Search the live marketplace Request early access