---
# === IDENTITY ===
id: software/system-design/search-engine/2026
canonical_question: "How do I design a search engine architecture?"
aliases:
  - "search engine system design"
  - "full-text search architecture"
  - "inverted index design"
  - "Elasticsearch architecture"
  - "search infrastructure design"
  - "distributed search system"
  - "search engine components"
  - "Lucene architecture overview"
  - "search ranking system design"
  - "hybrid search architecture"
entity_type: software_reference
domain: software > system-design > search_engine
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-23
confidence: 0.91
version: 1.0
first_published: 2026-02-23

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Elasticsearch 8.0 — security on by default, Lucene 9.x vector search support (2022-02)"
  next_review: 2026-08-22
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Inverted indexes are write-heavy on ingestion — never colocate indexing and query workloads on the same nodes in production"
  - "Shard count is fixed at index creation in Elasticsearch — choose shard count based on expected data volume, not current volume"
  - "BM25 relevance scoring assumes term independence — highly correlated terms (e.g., 'New York') need phrase queries or shingles, not default tokenization"
  - "Vector search (kNN) requires dedicated memory budget — each float32 vector of dimension d costs 4*d bytes per document, 1B docs at 768 dims = ~3TB RAM"
  - "Segment merges in Lucene are I/O-intensive — schedule force-merges during off-peak hours only, never during query traffic spikes"
  - "Relevance tuning without A/B testing is guesswork — always instrument click-through rate and nDCG before changing ranking weights"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Looking for a hosted search-as-a-service solution comparison (Algolia vs Typesense vs Meilisearch)"
    use_instead: "Compare hosted search products — this unit covers build-your-own architecture"
  - condition: "Need to debug slow Elasticsearch queries in an existing cluster"
    use_instead: "software/debugging/elasticsearch-slow-queries/2026"
  - condition: "Designing a recommendation engine (collaborative filtering, not search)"
    use_instead: "software/system-design/recommendation-engine/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: scale
    question: "What is the expected document corpus size and query volume?"
    type: choice
    options: ["Small (<1M docs, <100 QPS)", "Medium (1M-100M docs, 100-10K QPS)", "Large (>100M docs, >10K QPS)"]
  - key: search_type
    question: "What type of search does the system need?"
    type: choice
    options: ["Keyword/full-text only", "Semantic/vector search only", "Hybrid (keyword + vector)"]
  - key: latency_requirement
    question: "What is the acceptable p99 query latency?"
    type: choice
    options: ["<50ms (autocomplete/typeahead)", "<200ms (standard search)", "<1s (complex analytics)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/system-design/search-engine/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-23)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/system-design/recommendation-engine/2026"
      label: "Recommendation Engine Architecture"
    - id: "software/system-design/distributed-cache/2026"
      label: "Distributed Cache Design"
    - id: "software/system-design/message-queue/2026"
      label: "Message Queue Architecture"
  solves:
    - id: "software/debugging/elasticsearch-slow-queries/2026"
      label: "Elasticsearch Slow Queries"
  often_confused_with:
    - id: "software/system-design/database-indexing/2026"
      label: "Database Indexing (B-tree vs. inverted index)"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "The Anatomy of a Large-Scale Hypertextual Web Search Engine"
    author: Sergey Brin, Lawrence Page (Stanford University)
    url: https://research.google/pubs/the-anatomy-of-a-large-scale-hypertextual-web-search-engine/
    type: academic_paper
    published: 1998-04-01
    reliability: authoritative
  - id: src2
    title: "Elasticsearch from the Bottom Up, Part 1"
    author: Elastic
    url: https://www.elastic.co/blog/found-elasticsearch-from-the-bottom-up
    type: technical_blog
    published: 2023-01-15
    reliability: high
  - id: src3
    title: "Web Search for a Planet: The Google Cluster Architecture"
    author: Luiz Andre Barroso, Jeffrey Dean, Urs Holzle (Google)
    url: https://research.google.com/archive/googlecluster-ieee.pdf
    type: academic_paper
    published: 2003-03-01
    reliability: authoritative
  - id: src4
    title: "Exploring Apache Lucene - Part 1: The Index"
    author: Jedr Blaszyk
    url: https://j.blaszyk.me/tech-blog/exploring-apache-lucene-index/
    type: technical_blog
    published: 2024-06-10
    reliability: moderate_high
  - id: src5
    title: "How Algolia Tackled the Relevance Problem of Search Engines"
    author: Algolia
    url: https://www.algolia.com/blog/engineering/how-algolia-tackled-the-relevance-problem-of-search-engines
    type: technical_blog
    published: 2023-09-20
    reliability: high
  - id: src6
    title: "Elasticsearch Query DSL Documentation"
    author: Elastic
    url: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
    type: official_docs
    published: 2025-12-01
    reliability: high
  - id: src7
    title: "Scaling Indexing and Search — Algolia New Search Architecture"
    author: Algolia / High Scalability
    url: https://highscalability.com/scaling-indexing-and-search-algolia-new-search-architecture/
    type: technical_blog
    published: 2024-03-15
    reliability: moderate_high
---

# Search Engine Architecture: System Design Guide

## TL;DR

- **Bottom line**: A search engine is a pipeline of four stages — crawl/ingest, index (inverted index + optional vector index), query processing (parse, expand, route), and ranking (BM25/learned) — each independently scalable via sharding and replication.
- **Key tool/command**: `PUT /my-index { "mappings": { "properties": { "content": { "type": "text", "analyzer": "standard" } } } }` (Elasticsearch index creation)
- **Watch out for**: Choosing shard count at index creation without projecting data growth — resharding requires full reindex.
- **Works with**: Elasticsearch 7.x-8.x, OpenSearch 2.x, Apache Lucene 9.x, Solr 9.x, Meilisearch 1.x, Typesense 0.25+.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Inverted indexes are write-heavy on ingestion — never colocate indexing and query workloads on the same nodes in production [src7]
- Shard count is fixed at index creation in Elasticsearch — choose based on projected data volume (target 10-50GB per shard) [src2]
- BM25 assumes term independence — correlated multi-word terms need phrase queries or shingle token filters [src5]
- Vector search (kNN) memory: each float32 vector of dimension d costs 4*d bytes/doc — budget RAM before enabling [src6]
- Segment merges are I/O-intensive — schedule force-merges only during off-peak hours [src4]
- Never tune relevance without instrumented metrics (CTR, nDCG) — subjective "looks better" leads to silent regression [src5]

## Quick Reference

| Component | Role | Technology Options | Scaling Strategy |
|---|---|---|---|
| **Crawler / Ingestor** | Fetches documents from sources (web, DB, file system) and feeds them into the pipeline | Scrapy, Apache Nutch, custom HTTP workers, Kafka consumers | Horizontal — add workers; rate-limit per domain; use priority queues for freshness |
| **Document Processor** | Normalizes raw documents: HTML stripping, language detection, deduplication, chunking | Apache Tika, custom ETL, LangChain document loaders | Horizontal — stateless workers behind a message queue |
| **Tokenizer / Analyzer** | Splits text into tokens, applies stemming, stop-word removal, synonym expansion | Lucene analyzers, ICU tokenizer, custom language-specific analyzers | Co-located with indexer; language-specific analyzer chains |
| **Inverted Index** | Maps terms to document IDs + positions for fast full-text lookup | Elasticsearch, OpenSearch, Solr, Lucene (direct), Tantivy (Rust) | Shard by document ID (hash-based); replicate for read throughput |
| **Vector Index** | Stores dense embeddings for semantic/kNN search | FAISS, HNSW (Lucene 9+), Milvus, Pinecone, Weaviate, Qdrant | Partition by vector space; requires GPU or high-RAM nodes |
| **Query Parser** | Interprets user query: tokenization, spell correction, query expansion, intent detection | Elasticsearch Query DSL, custom NLP pipeline, LLM-based query understanding | Stateless — horizontal scale behind load balancer |
| **Query Router / Scatter-Gather** | Distributes query to relevant shards and merges partial results | Elasticsearch coordinating node, custom gRPC fan-out | Dedicated coordinating nodes; increase for fan-out width |
| **Ranking Engine** | Scores and orders results by relevance (BM25, TF-IDF, learned ranking) | BM25 (default), LTR (Learning to Rank) plugins, RankNet, LambdaMART | CPU-bound — scale vertically or offload to dedicated ranking service |
| **Result Cache** | Caches frequent query results to reduce compute | Redis, Memcached, Elasticsearch request cache | TTL-based invalidation; shard-level caching for popular queries |
| **Autocomplete / Suggest** | Provides typeahead suggestions as user types | Elasticsearch completion suggester, Trie-based, prefix index | Separate lightweight index; <50ms p99 latency target |
| **Relevance Feedback Loop** | Collects click signals, A/B test results to tune ranking | Kafka + ClickHouse, Elasticsearch LTR, custom analytics pipeline | Event streaming — scale consumers independently |
| **Index Manager** | Handles index lifecycle: creation, aliasing, reindexing, retention | Elasticsearch ILM, custom cron jobs, Curator | Automate with ILM policies; alias-based zero-downtime reindex |
| **Monitoring / Observability** | Tracks query latency, indexing throughput, shard health, relevance metrics | Prometheus + Grafana, Elastic APM, Datadog | Alert on p99 latency, indexing lag, shard imbalance |

## Decision Tree

```
START
├── Expected corpus size?
│   ├── <1M documents, <100 QPS
│   │   ├── Need full-text only → Single-node Elasticsearch or Meilisearch/Typesense
│   │   └── Need semantic search → Single-node with HNSW (Elasticsearch 8.x kNN)
│   ├── 1M-100M documents, 100-10K QPS
│   │   ├── Keyword search dominant → Multi-node Elasticsearch cluster (3-10 nodes)
│   │   ├── Hybrid search needed → Elasticsearch 8.x with kNN + BM25 fusion
│   │   └── Real-time indexing critical → Dedicated ingest nodes + hot-warm architecture
│   └── >100M documents, >10K QPS
│       ├── Web-scale crawl → Custom pipeline: Kafka → Spark → Lucene shards (Google-style) [src1]
│       ├── E-commerce catalog → Elasticsearch + Learning to Rank + dedicated vector index
│       └── Log/event search → OpenSearch with hot-warm-cold tiers + rollover aliases
├── Latency requirement?
│   ├── <50ms (autocomplete) → Dedicated completion index, prefix queries, in-memory trie
│   ├── <200ms (standard) → Standard Elasticsearch with result caching
│   └── <1s (analytics) → Aggregation-heavy queries, consider materialized views
└── DEFAULT → Start with managed Elasticsearch (AWS OpenSearch / Elastic Cloud), add vector search when needed
```

## Step-by-Step Guide

### 1. Define the document schema and mapping

Before ingesting data, design your index mapping with explicit field types. Letting Elasticsearch auto-detect types leads to suboptimal mappings. [src2]

```json
PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "content_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "stop", "snowball"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title":       { "type": "text", "analyzer": "content_analyzer", "boost": 2.0 },
      "description": { "type": "text", "analyzer": "content_analyzer" },
      "category":    { "type": "keyword" },
      "price":       { "type": "float" },
      "created_at":  { "type": "date" },
      "embedding":   { "type": "dense_vector", "dims": 768, "index": true, "similarity": "cosine" }
    }
  }
}
```

**Verify**: `GET /products/_mapping` -- expected: mapping with all fields as defined above.

### 2. Build the ingestion pipeline

Create a document processing pipeline that normalizes, deduplicates, and enriches documents before indexing. [src3]

```python
# Ingestion pipeline: fetch -> process -> bulk index
from elasticsearch import Elasticsearch, helpers
import hashlib

es = Elasticsearch("http://localhost:9200")

def process_document(raw_doc):
    """Normalize and enrich a raw document."""
    content = raw_doc["content"].strip()
    return {
        "_index": "products",
        "_id": hashlib.sha256(raw_doc["url"].encode()).hexdigest()[:16],
        "title": raw_doc["title"],
        "description": content,
        "category": raw_doc.get("category", "uncategorized"),
        "created_at": raw_doc["timestamp"],
    }

def bulk_index(documents, chunk_size=500):
    """Bulk index with automatic retry on failure."""
    actions = [process_document(doc) for doc in documents]
    success, errors = helpers.bulk(es, actions, chunk_size=chunk_size, raise_on_error=False)
    print(f"Indexed {success} docs, {len(errors)} failures")
    return errors
```

**Verify**: `GET /products/_count` -- expected: `{"count": N}` matching number of ingested documents.

### 3. Implement query parsing and expansion

Transform raw user queries into structured search requests with spell correction and synonym expansion. [src5]

```python
def build_search_query(user_query, filters=None, page=0, size=10):
    """Build an Elasticsearch query with BM25 + boosting."""
    must_clauses = [{
        "multi_match": {
            "query": user_query,
            "fields": ["title^3", "description"],
            "type": "best_fields",
            "fuzziness": "AUTO",
            "prefix_length": 2
        }
    }]
    filter_clauses = []
    if filters:
        if "category" in filters:
            filter_clauses.append({"term": {"category": filters["category"]}})
        if "price_max" in filters:
            filter_clauses.append({"range": {"price": {"lte": filters["price_max"]}}})

    return {
        "query": {
            "bool": {
                "must": must_clauses,
                "filter": filter_clauses
            }
        },
        "from": page * size,
        "size": size,
        "highlight": {
            "fields": {"title": {}, "description": {"fragment_size": 150}}
        }
    }
```

**Verify**: `POST /products/_search` with the query body -- expected: results with `_score` and `highlight` fields.

### 4. Add vector search for semantic retrieval

Combine keyword search (BM25) with vector search (kNN) for hybrid relevance. [src6]

```python
def hybrid_search(user_query, query_vector, keyword_weight=0.7, vector_weight=0.3, size=10):
    """Hybrid search combining BM25 keyword + kNN vector scores."""
    return {
        "query": {
            "bool": {
                "should": [
                    {
                        "multi_match": {
                            "query": user_query,
                            "fields": ["title^3", "description"],
                            "boost": keyword_weight
                        }
                    }
                ]
            }
        },
        "knn": {
            "field": "embedding",
            "query_vector": query_vector,
            "k": size,
            "num_candidates": size * 10,
            "boost": vector_weight
        },
        "size": size
    }
```

**Verify**: Results include documents matching semantically similar concepts, not just exact keyword matches.

### 5. Configure sharding and replication for scale

Set up your cluster topology based on expected load. [src2] [src7]

```yaml
# Elasticsearch cluster topology (3-node minimum for production)
# Node 1: Master-eligible + Data (hot)
node.roles: [master, data_hot, ingest]
# Node 2: Data (hot) — primary query serving
node.roles: [data_hot]
# Node 3: Data (warm) — older/less-accessed indexes
node.roles: [data_warm]

# Index Lifecycle Management policy
PUT _ilm/policy/search-lifecycle
{
  "policy": {
    "phases": {
      "hot":  { "actions": { "rollover": { "max_size": "50gb", "max_age": "7d" } } },
      "warm": { "min_age": "30d", "actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 } } },
      "delete": { "min_age": "90d", "actions": { "delete": {} } }
    }
  }
}
```

**Verify**: `GET _cat/shards/products?v` -- expected: shards distributed across nodes with status `STARTED`.

### 6. Implement result caching and monitoring

Add caching for popular queries and monitoring for operational visibility. [src3]

```python
import redis
import json
import hashlib
import time

cache = redis.Redis(host="localhost", port=6379, db=0)
CACHE_TTL = 300  # 5 minutes

def cached_search(es, query_body, index="products"):
    """Search with Redis result cache."""
    cache_key = f"search:{hashlib.md5(json.dumps(query_body, sort_keys=True).encode()).hexdigest()}"
    cached = cache.get(cache_key)
    if cached:
        return json.loads(cached)

    start = time.monotonic()
    results = es.search(index=index, body=query_body)
    latency_ms = (time.monotonic() - start) * 1000

    # Cache only if query succeeded and had results
    if results["hits"]["total"]["value"] > 0:
        cache.setex(cache_key, CACHE_TTL, json.dumps(results))

    # Emit metrics (integrate with Prometheus/StatsD)
    print(f"query_latency_ms={latency_ms:.1f} hits={results['hits']['total']['value']} cached=false")
    return results
```

**Verify**: Second identical query returns in <1ms (cache hit); `redis-cli KEYS "search:*"` shows cached entries.

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### Python: Inverted Index from Scratch

> Full script: [python-inverted-index-from-scratch.py](scripts/python-inverted-index-from-scratch.py) (28 lines)

```python
# Input:  List of (doc_id, text) tuples
# Output: Inverted index mapping terms to doc_ids with positions
import re
from collections import defaultdict
def build_inverted_index(documents):
# ... (see full script)
```

### Elasticsearch: Query DSL Multi-Match with Highlighting

> Full script: [elasticsearch-query-dsl-multi-match-with-highlight.json](scripts/elasticsearch-query-dsl-multi-match-with-highlight.json) (29 lines)

```json
// Input:  User search query string
// Output: Ranked results with highlighted snippets
POST /products/_search
{
  "query": {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Single giant shard for all documents

```python
# BAD — one shard with 500GB of data
PUT /my-index
{
  "settings": { "number_of_shards": 1 }
}
# Query latency degrades to seconds as shard grows beyond 50GB
# Cannot distribute load across cluster nodes
```

### Correct: Right-size shards with growth projection

```python
# GOOD — project total data size, target 10-50GB per shard [src2]
PUT /my-index
{
  "settings": {
    "number_of_shards": 5,     # 250GB total / 50GB target = 5 shards
    "number_of_replicas": 1    # 1 replica for HA
  }
}
# Use rollover aliases for time-series data to keep shard sizes bounded
```

### Wrong: Using match_all with post-filter for search

```python
# BAD — fetches ALL documents then filters in application code
results = es.search(index="products", body={
    "query": { "match_all": {} },
    "size": 10000
})
filtered = [r for r in results["hits"]["hits"] if "headphones" in r["_source"]["title"]]
```

### Correct: Push filtering to the query engine

```python
# GOOD — let Elasticsearch do the filtering with inverted index [src6]
results = es.search(index="products", body={
    "query": {
        "bool": {
            "must": [{ "match": { "title": "headphones" } }],
            "filter": [{ "range": { "price": { "lte": 200 } } }]
        }
    },
    "size": 10
})
```

### Wrong: Colocating indexing and search on same nodes

```python
# BAD — heavy bulk indexing causes GC pauses that spike query latency
# Both indexing and search compete for the same heap, I/O, and CPU
node.roles: [master, data, ingest]  # single node does everything
```

### Correct: Separate ingest and query node roles

```yaml
# GOOD — dedicate node roles to isolate workloads [src7]
# Ingest node: handles bulk indexing, enrichment pipelines
node.roles: [ingest, data_hot]

# Query node: coordinating only, routes and merges results
node.roles: [coordinating_only]  # no data, no ingest

# Data nodes: serve queries from local shards
node.roles: [data_hot]
```

### Wrong: Disabling replicas to "save resources"

```python
# BAD — no replicas means single point of failure AND lower query throughput
PUT /my-index/_settings
{ "index": { "number_of_replicas": 0 } }
# One node failure = data loss; all queries hit primary shards only
```

### Correct: Use replicas for both HA and read throughput

```python
# GOOD — replicas serve reads and provide fault tolerance [src2]
PUT /my-index/_settings
{ "index": { "number_of_replicas": 1 } }  # minimum 1 for production
# Elasticsearch automatically load-balances reads across primary + replica
# For read-heavy workloads, increase to 2+ replicas
```

## Common Pitfalls

- **Over-sharding small indexes**: Creating 20 shards for an index with 100K documents wastes cluster resources — each shard has overhead (file handles, memory, thread pool). Fix: target 10-50GB per shard; small indexes need only 1 shard. [src2]
- **Ignoring analyzer chain mismatches**: Indexing with `standard` analyzer but querying with `keyword` yields zero results because tokenization differs. Fix: always verify `GET /_analyze` produces expected tokens for both index and query time. [src4]
- **Deep pagination with `from` + `size`**: Requesting page 1000 (from=10000, size=10) forces each shard to score 10010 documents. Fix: use `search_after` with a sort tiebreaker for deep pagination. [src6]
- **Not setting `index.refresh_interval`**: Default 1s refresh creates a new Lucene segment every second during bulk indexing, causing excessive I/O. Fix: set `refresh_interval: 30s` or `-1` during bulk, then restore after. [src4]
- **Mapping explosion from dynamic fields**: Allowing arbitrary JSON keys as Elasticsearch fields bloats the mapping (10K+ fields = slow cluster state). Fix: set `"dynamic": "strict"` or `"dynamic": false` and explicitly map fields. [src2]
- **Single-field ranking bias**: Boosting only the `title` field causes short-title documents to dominate. Fix: use `multi_match` with `type: "cross_fields"` or `"most_fields"` to balance scoring across fields. [src5]
- **Forgetting to warm up caches after deployment**: Cold Elasticsearch caches cause latency spikes for the first N queries after restart. Fix: send a warm-up query set before routing traffic to a new node. [src3]
- **Not monitoring shard balance**: Uneven shard distribution creates hot nodes that bottleneck the cluster. Fix: use `_cat/allocation` and shard allocation awareness (rack/zone-based). [src2]

## Diagnostic Commands

```bash
# Check cluster health and shard allocation
curl -s localhost:9200/_cluster/health?pretty
curl -s localhost:9200/_cat/shards?v&s=store:desc

# Check index mapping and settings
curl -s localhost:9200/my-index/_mapping?pretty
curl -s localhost:9200/my-index/_settings?pretty

# Analyze how text is tokenized (debug relevance issues)
curl -s -XPOST localhost:9200/my-index/_analyze -H 'Content-Type: application/json' \
  -d '{"analyzer":"standard","text":"search engine architecture"}'

# Check slow query log
curl -s localhost:9200/my-index/_settings -H 'Content-Type: application/json' \
  -d '{"index.search.slowlog.threshold.query.warn":"1s"}'

# Monitor indexing throughput and latency
curl -s localhost:9200/_nodes/stats/indices?pretty | grep -E "indexing|search"

# Check segment count (too many = needs merge)
curl -s localhost:9200/_cat/segments/my-index?v&s=size:desc
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Elasticsearch 8.x (2022-present) | Current | Security enabled by default; Lucene 9.x; native kNN vector search | Enable security from day 1; vector search via `dense_vector` field type |
| Elasticsearch 7.x (2019-2024) | Maintenance | Type removal; single type per index | Remove type from API calls; upgrade to 8.x for vector search |
| OpenSearch 2.x (2022-present) | Current (AWS fork) | Forked from ES 7.10; neural search plugin | Use OpenSearch-specific APIs; not fully compatible with ES 8.x clients |
| Apache Solr 9.x (2022-present) | Current | Module system; requires Java 11+ | Migrate plugins to module architecture |
| Lucene 9.x (2021-present) | Current | New vector search (HNSW); removed deprecated APIs | Direct Lucene use requires Java; most users access through ES/Solr |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Full-text search across large document corpus (>10K docs) | Exact key-value lookups on structured data | PostgreSQL with B-tree indexes or Redis |
| Relevance-ranked results needed (not just filtering) | Simple LIKE/regex search on <10K rows | SQL `LIKE` or `ILIKE` queries |
| Sub-200ms search latency required at scale | Write-heavy, read-light workload | OLTP database (PostgreSQL, MySQL) |
| Faceted search with aggregations (e-commerce filters) | Single-user, single-machine application | SQLite FTS5 or in-process Lucene |
| Autocomplete / typeahead functionality | Only need vector similarity search | Dedicated vector DB (Pinecone, Weaviate) |
| Log search and analytics (ELK stack) | Transactional consistency required (ACID) | PostgreSQL — search engines are eventually consistent |

## Important Caveats

- Elasticsearch and OpenSearch are **eventually consistent** by design — documents are not searchable until the next refresh interval (default 1s). Do not use as a primary data store for transactional workloads. [src2]
- **Vector search quality depends entirely on embedding model choice** — HNSW is an approximate algorithm and may miss relevant results. Always benchmark recall vs. latency for your specific embedding model and corpus. [src6]
- **Lucene segment merges can cause I/O storms** — large force-merges on production clusters temporarily degrade query latency. Schedule during maintenance windows and monitor disk I/O. [src4]
- **Cost scales with data retention** — unlike databases, search engines store data in denormalized, index-optimized formats that consume 1.5-3x the raw data size. Budget storage accordingly and implement index lifecycle management. [src7]
- **Hybrid keyword+vector search is not a silver bullet** — the optimal blend ratio (e.g., 0.7 keyword + 0.3 vector) varies by query type and domain. Invest in A/B testing infrastructure early. [src5]

## Related Units

- [Recommendation Engine Architecture](/software/system-design/recommendation-engine/2026)
- [Distributed Cache Design](/software/system-design/distributed-cache/2026)
- [Message Queue Architecture](/software/system-design/message-queue/2026)
- [Database Indexing (B-tree vs. Inverted Index)](/software/system-design/database-indexing/2026)
- [Elasticsearch Slow Queries](/software/debugging/elasticsearch-slow-queries/2026)
