Sitemap
HYPD Tech

Creator Commerce

How We Made E-Commerce Search Smarter: Autocomplete, Relevance, and Ranking

6 min readSep 1, 2026

--

Press enter or click to view image in full size

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:

  1. rebuilt autocomplete around analyzed prefix fields;
  2. 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.

--

--