How We Made E-Commerce Search Smarter: Autocomplete, Relevance, and Ranking
Search looks simple from the outside.
A user types a few words, hits enter, and expects the right products to appear.
But at marketplace scale, search quality gets complicated quickly.
We were seeing two recurring problems:
- users had to type queries too precisely before autocomplete became useful;
- unrelated products could rank above genuine brand matches by stuffing popular brand names into their keyword metadata.
Neither issue was caused by Elasticsearch “not working.”
They were relevance problems.
Our indexing strategy, query types, and field boosts were telling Elasticsearch what to care about, and some of those signals were wrong.
We made two targeted changes:
- rebuilt autocomplete around analyzed prefix fields;
- redesigned field priorities in our main product search.
Here’s what changed.
Problem 1: Autocomplete Was Too Strict
Our search suggestions worked, but only when users behaved exactly how the system expected.
That meant typing enough of the query, spelling it correctly, and often matching the beginning of the expected phrase.
Real users don’t search like that.
Someone might type:
beauty
and expect to see:
- Swiss Beauty
- Neyah Beauty
- Sleeping Beauty Lipstick
They don’t care where the word appears. They just expect the system to understand what they are trying to find.
Our implementation was getting in the way.
Using Elasticsearch for Search-as-You-Type
We already had .autocomplete fields indexed using edge n-grams.
Edge n-grams generate prefixes from indexed terms. A word like:
beauty
can effectively produce tokens such as:
b
be
bea
beau
beaut
beauty
That makes them a natural fit for search-as-you-type experiences.
We updated the suggestion query to use those fields directly:
mustQuery := elastic.NewMultiMatchQuery(
searchText,
"brand_info.name.autocomplete⁵",
"name.autocomplete²",
).Type("bool_prefix")The important part here is bool_prefix.
It allows Elasticsearch to treat the final term in a query as a prefix, which is exactly what we want while someone is still typing.
We also weighted brand matches higher than product-name matches:
brand_info.name.autocomplete⁵
name.autocomplete²For our suggestion use case, a strong brand match is usually more useful than a weaker product-title match.
So if a user starts typing a recognizable brand name, it should surface quickly.
We Removed Logic Instead of Adding More
One of the biggest improvements came from deleting code.
We had application-level prefix checks in Go that filtered results after Elasticsearch returned them.
That sounded reasonable at first.
In practice, it made search unnecessarily rigid.
Elasticsearch already had access to the analyzed tokens and was capable of finding matches across the indexed text. Our additional string checks were throwing away valid results.
Once we removed them, searches became much more flexible.
Typing:
beauty
could now return:
Neyah Beauty
and:
Sleeping Beauty Lipstick
instead of only values that literally started with beauty.
The lesson was simple:
if Elasticsearch already understands the analyzed text, avoid recreating weaker matching logic in the application layer.
Cleaning Up Duplicate Suggestions
Better recall introduced another issue: duplicate suggestions.
Multiple products can belong to the same brand or produce identical suggestion values.
We didn’t want users seeing the same option several times, so we handled deduplication in the Go service using a small in-memory map.
Something conceptually like:
seen := make(map[string]bool)
for _, suggestion := range suggestions {
if seen[suggestion] {
continue
}
seen[suggestion] = true
results = append(results, suggestion)
}Elasticsearch handled retrieval.
The service handled response cleanup.
That separation kept the search query focused while still producing a clean UI.
Problem 2: Sellers Could Game Search Ranking
Autocomplete was only half the problem.
Our primary product-search endpoint had a more serious relevance issue.
Products could rank highly simply because sellers inserted popular brand names into their keywords field.
For example, imagine a user searches for:
Swiss Beauty
You would expect official Swiss Beauty products to appear first.
Instead, an unrelated competitor product could rank surprisingly high if its metadata contained Swiss Beauty as a keyword.
Why?
Because of how we had configured our field boosts.
The Problem Wasn’t BM25. It Was Our Signals.
Elasticsearch uses BM25 scoring by default.
At a high level, BM25 considers things like:
- how frequently a term appears in a document;
- how common or rare that term is across the index;
- field length and other relevance signals.
One important part of that scoring is inverse document frequency, or IDF.
Rare terms generally contribute more to relevance than very common terms.
That is usually useful.
But it became a problem when combined with a heavily boosted, seller-controlled field.
If a competitor inserted a brand name into keywords, and that exact term was relatively rare in that field, Elasticsearch could assign it a strong relevance contribution.
Our previous boosts amplified that even further.
The end result was the opposite of what we wanted:
an unrelated product with a strategically placed keyword could outrank a genuine product from the brand being searched.
Get Anugrah B. Marandi’s stories in your inbox
Join Medium for free to get updates from this writer.
The ranking algorithm wasn’t broken.
We had simply told it to trust the wrong field too much.
Rebalancing Search Priority
We changed the query so that product names and structured brand metadata carried most of the ranking weight.
The updated query looked like this:
mustQuery := elastic.NewMultiMatchQuery(
opts.Query,
"brand_info.name⁴",
"name⁵",
"keywords¹",
).
Operator("or").
Type("best_fields").
Fuzziness("2")The new priority became:
Product name ^5
Brand name ^4
Keywords ^1
This reflected how we actually think about the data.
A product title is highly representative of what the item is.
Official brand metadata is structured and authoritative.
Keywords are useful for discovery, synonyms, and alternate search terms — but they are also easier to manipulate.
So they should help recall.
They should not control ranking.
Why best_fields Worked Better for Us
We also used:
Type(“best_fields”)
With best_fields, Elasticsearch gives more weight to the field producing the strongest match.
That fit our search model well.
If the search query strongly matches the product title, that is meaningful.
If it strongly matches the official brand name, that is meaningful.
If it only appears in a low-priority keyword field, it should not automatically compete with those stronger signals.
Combined with explicit field boosts, this gave us a much clearer relevance hierarchy.
We Still Wanted Typo Tolerance
Improving precision can easily make search too strict.
We didn’t want that.
Users misspell brand names and product names all the time, so we retained fuzzy matching:
Fuzziness(“2”)
That allowed the search engine to tolerate a limited number of edit operations while still respecting the new field priority.
The goal wasn’t maximum precision at all costs.
It was to improve ranking without making search fragile.
Good search needs both:
precision — relevant items should rank first.
recall — imperfect queries should still return useful results.
The Bigger Lesson: Search Fields Have Different Trust Levels
The most useful takeaway from this work wasn’t a specific Elasticsearch query.
It was a ranking principle.
Not all searchable fields should be treated equally.
In our case:
Field
Trust level
Role
Product name
High
Primary ranking signal
Official brand metadata
High
Primary ranking signal
Keywords
Lower
Discovery and recall
That distinction matters.
If a field is authoritative and difficult to manipulate, it can safely carry more ranking weight.
If a field is free-form or seller-controlled, it should be treated more cautiously.
Search relevance is partly about matching text.
It is also about deciding which data deserves trust.
Small Query Changes, Large UX Improvements
None of these changes required rebuilding our entire search stack.
We didn’t replace Elasticsearch.
We didn’t introduce a new search service.
We mostly changed how we used the system we already had.
For autocomplete, we:
- queried edge n-gram fields;
- used bool_prefix;
- boosted brand suggestions;
- removed restrictive Go-level matching;
- deduplicated results before returning them.
For product search, we:
- reduced keyword influence;
- increased product-title and brand weight;
- used best_fields;
- preserved fuzzy matching.
The code changes were relatively small.
The impact on search behavior was much larger.
Search Quality Is Product Work
It is easy to classify search as an infrastructure problem.
At some level, it is.
There are analyzers, tokenizers, indexes, scoring models, query types, and latency constraints.
But users never see any of those things.
They see whether the right product appeared.
They notice whether typing three characters gives them a useful suggestion.
They notice when an unrelated product ranks above the brand they explicitly searched for.
That makes search quality product work as much as engineering work.
The implementation lives in Elasticsearch and Go.
The outcome lives in the customer experience.
For us, the biggest improvement came from making the ranking model better reflect the real semantics of our data:
authoritative fields should drive relevance, while flexible fields should support discovery.
Once those priorities were encoded correctly, search started behaving much more like users expected.






