“Let’s add search” is rarely as simple as it sounds. This article covers what actually goes into a production product search built on Elasticsearch — the indexing pipeline, relevance, faceted filters, and the operational realities.

Why Elasticsearch

Alternatives: OpenSearch (fork), Algolia (managed), Typesense, Meilisearch. For most serious product search at scale, Elasticsearch or OpenSearch is the pragmatic choice — mature ecosystem, flexible, battle-tested.

Postgres full-text search works for small datasets. Once relevance and faceting matter, ES is worth the operational cost.

Indexing pipeline

The source of truth is your product database. Search is a projection — keep it in sync.

Two common patterns:

Pull (polling). Job periodically reads updated products, indexes them. Simple. Latency = poll interval.

Push (event-driven). Product service publishes events on change. Consumer indexes. Near-real-time.

For product search where minute-level freshness is acceptable, pull is fine. For inventory-style search where seconds matter, push.

Either way: the indexer must be idempotent. Re-indexing the same product should be a no-op or replace. Never append duplicates.

@Component
public class ProductIndexer {
    @KafkaListener(topics = "product.updated", groupId = "search")
    public void onProductUpdated(ProductUpdatedEvent event) {
        Product p = productRepo.findById(event.productId()).orElseThrow();
        IndexRequest request = new IndexRequest("products").id(p.getId().toString()).source(toDocument(p));
        esClient.index(request, RequestOptions.DEFAULT);
    }
}

Document design

Flatten aggressively for search. The document is what you’ll query; shape it for queries, not normalization.

{
  "id": "p-123",
  "title": "Wool winter coat",
  "description": "...",
  "brand": "Example Brand",
  "category": "outerwear",
  "subcategory": "coats",
  "price_cents": 24999,
  "currency": "USD",
  "sizes": ["S", "M", "L", "XL"],
  "colors": ["black", "navy"],
  "stock": 45,
  "tags": ["sale", "winter"],
  "created_at": "2023-09-12T10:00:00Z",
  "popularity_score": 0.78
}

Multi-language? Separate fields per language (title_en, title_de) or separate indexes per locale. Separate indexes scale better.

Relevance

The hard part. Default ES scoring (BM25) works out of the box but rarely matches business priorities. Typical refinements:

  • Boost by field. Title matches weighted more than description matches.
  • Function scoring. Multiply score by popularity, recency, or stock.
  • Business boosts. Boost products on sale, or in-stock, or from top-tier brands.
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "wool coat",
          "fields": ["title^3", "description", "tags^2"]
        }
      },
      "functions": [
        {"filter": {"term": {"tags": "sale"}}, "weight": 1.3},
        {"field_value_factor": {"field": "popularity_score", "modifier": "sqrt"}}
      ],
      "score_mode": "multiply"
    }
  }
}

Tune relevance with real queries + human judgment. Measure — don’t guess.

Faceting (aggregations)

Product search UIs typically show filters: brand, category, price range, size, color. Each is an aggregation:

{
  "aggs": {
    "brands": {"terms": {"field": "brand", "size": 20}},
    "sizes":  {"terms": {"field": "sizes"}},
    "price_ranges": {
      "range": {
        "field": "price_cents",
        "ranges": [{"to": 5000}, {"from": 5000, "to": 10000}, {"from": 10000}]
      }
    }
  }
}

Paired with the main query, this returns both results and facet counts in one call.

Autocomplete

Users type “wo” — show suggestions. ES has dedicated features:

  • Completion suggester — fast, prefix-based
  • Edge n-grams — more flexible, higher storage cost
  • Search-as-you-type field type — modern default for most use cases

Latency target: < 100ms. Achievable with well-tuned completion suggester.

Query latency

Indexes grow. Shards multiply. Queries slow down. Mitigations:

  • Index sizing. Aim for shards around 10-50GB each. Too small = wasted overhead; too large = slow queries.
  • Primary shards fixed at create. Choose carefully; reindex required to change.
  • Replicas for read scaling. Add replicas to handle more QPS.
  • Hot/warm architecture. Recent data on SSD nodes; older archived data on cheaper storage.
  • Query caching. ES caches frequent queries; hit rates should be high.

p99 under 100ms for product search is achievable with the above.

Operational pain points

Re-indexing. Schema changes often require reindex into a new index, alias swap, cutover. Automate this workflow; it’s frequent.

Cluster health. Yellow (missing replicas) is tolerable; red (missing primaries) is urgent. Monitor and alert on cluster state.

Memory pressure. ES is JVM-based; wrong heap size = pauses. 50% of container memory for heap, remaining for OS page cache is typical.

Disk use. Indexes compress well, but growth is real. Retention policies for data that doesn’t need to stay searchable forever.

Mapping explosions. Every unique field name counted. Dynamic mapping on unpredictable data = mapping hell. Disable dynamic mapping on user-generated fields.

Sync bugs

The most common production issue: search index drifts from source of truth. Products deleted from DB but still in search. Updates missed.

Mitigations:

  • Full reindex as recovery tool. Can always rebuild from scratch.
  • Consistency checks. Periodic compare of product IDs in DB vs search; alert on drift.
  • Event replay. Re-publish product events from outbox if needed.
  • Soft delete in DB. Never truly delete; mark as archived. Search filters on archive status.

Alternative: hybrid with Postgres

For simpler apps, hybrid works:

  • Postgres holds the truth and handles structured filters (price, category)
  • ES handles full-text and fuzzy matching
  • Query combines both

Less elegant than pure ES but operationally cheaper for mid-size catalogs.

Closing note

Search looks like a feature; it’s actually infrastructure. Getting it right takes ongoing investment — not just initial setup. Invest in the indexing pipeline (events + idempotency), in relevance tuning (measurement + iteration), and in operations (cluster monitoring, reindexing automation). Once those are solid, search becomes a product feature that scales. Skip them and you’ll spend quarterly sprints fighting search-index fires.