Search & Relevance
Scope: retrieval & relevance. Event analytics → .claude/skills/analytics/SKILL.md.
When to use
- Adding search to an application (product catalogue, docs, users, content)
- Improving relevance or ranking of existing search
- Adding semantic / AI-powered search (embeddings + vector DB)
- Implementing hybrid search (BM25 + vector recall merged with RRF)
- Designing index sync pipeline from primary DB to search engine
- Facets, filters, aggregations, geo-search, autocomplete
- Multi-tenant search with per-tenant index or filter isolation
Workflow
- Clarify requirements — corpus size (docs count + avg size), query types (keyword, semantic, mixed), latency SLA (p99 <X ms), language(s), facets needed, freshness requirement (real-time vs eventually consistent), multi-tenancy?
- Select engine — Use decision matrix in Standards.
- Design index schema — Fields, data types, analyzers per language,
keyword vs text mappings (ES/OS), sortable/filterable flags (Meilisearch/Typesense), dense_vector dimension.
- Build indexing pipeline — Source of truth → transformer → bulk index. For real-time: CDC (Debezium) or application-level dual-write. Batch: scheduled full re-index weekly + incremental sync on change events.
- Implement query layer — BM25 for keyword, ANN (HNSW) for vector. Hybrid: reciprocal rank fusion (RRF) or linear combination. Boost recency/popularity via
function_score or equivalent.
- Tune analyzers — Language-specific tokenization (ICU plugin, kuromoji for Japanese). Custom synonym files per domain. Edge-ngram for prefix autocomplete. Phonetic/stemming for recall.
- Relevance evaluation — Define golden dataset (query + expected top-K). Measure NDCG@10, MRR. Run A/B or shadow test before promoting ranking changes.
- Add facets & filters — Keyword-mapped fields for facets. Cache heavy aggregations. Pagination with
search_after (not from+size past 10k).
- Observability — Log query, latency, result count, zero-result rate. Alert on p99 > SLA. Track zero-result queries weekly for gaps.
- Security — Tenant isolation: per-tenant index or mandatory filter injected server-side (never trust client). Role-based index-level permissions (ES/OS security plugin). No PII in indexed fields unless encrypted at field level.
- Scaling — Shard count: 1 shard per ~30–50 GB of data. Replicas: 1 per shard in prod. For high-write: hot-warm-cold architecture.
Standards
Engine selection matrix
| Requirement |
Best choice |
Avoid |
| Large corpus (>10M docs), complex aggregations, log analytics |
Elasticsearch 8.x or OpenSearch 2.x |
Typesense (limited aggregations) |
| Simple full-text, fast setup, SaaS/SMB product |
Typesense 0.26+ or Meilisearch v1.x |
Elasticsearch (operational complexity) |
| Already on Postgres, <1M docs, vector search |
pgvector 0.7+ extension |
Separate infra cost |
| Pure semantic / embedding search |
Qdrant, Weaviate, or Pinecone |
BM25-only engines |
| Hybrid (keyword + vector) production |
Elasticsearch (RRF GA in 8.14) or OpenSearch (neural-search plugin) |
pgvector alone (no BM25) |
| Managed, AWS-native |
OpenSearch Serverless or Amazon Kendra |
Self-hosted Kafka |
| Offline / edge / embedded |
Tantivy (Rust) or MiniSearch (JS) |
Elasticsearch |
Elasticsearch / OpenSearch index design
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"english_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "english_stop", "english_stemmer", "synonym_filter"]
}
}
}
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "english_analyzer", "boost": 2 },
"body": { "type": "text", "analyzer": "english_analyzer" },
"title_kw": { "type": "keyword" },
"category": { "type": "keyword" },
"created_at": { "type": "date" },
"embedding": { "type": "dense_vector", "dims": 1536, "index": true, "similarity": "cosine" }
}
}
}
- Never use dynamic mapping in production (
"dynamic": "strict").
- Use
_source: false for fields only needed for ranking, not retrieval.
- Alias indices; re-index to new index then atomically swap alias (zero-downtime schema changes).
Hybrid search with RRF (Elasticsearch 8.14+, OpenSearch 2.11+)
{
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "match": { "body": "query text" } } } },
{ "knn": { "field": "embedding", "query_vector": [0.1, ...], "k": 50 } }
],
"rank_window_size": 100,
"rank_constant": 60
}
}
}
- RRF
rank_constant=60 is a reasonable default; tune via offline evaluation.
- Pre-compute embeddings with
text-embedding-3-small (1536 dims) or text-embedding-3-large (3072 dims) for OpenAI; or BAAI/bge-m3 for multilingual open-source.
pgvector (Postgres extension ≥0.7)
CREATE EXTENSION vector;
CREATE TABLE items (id BIGSERIAL PRIMARY KEY, content TEXT, embedding vector(1536));
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
SELECT id, content, 1 - (embedding <=> query_vec) AS score
FROM items ORDER BY embedding <=> query_vec LIMIT 20;
- HNSW is preferred over IVFFlat for recall (≥95% vs ~90%).
- Combine with
WHERE clause for filtered ANN (partial index if selectivity >5%).
SET hnsw.ef_search = 100 at query time to trade latency for recall.
Meilisearch / Typesense
- Meilisearch: configure
searchableAttributes order (title first), filterableAttributes, sortableAttributes. Vector search via _vectors field. Tenant token JWTs for multi-tenancy.
- Typesense: schema-first (all fields declared upfront). Use
Collection aliases for zero-downtime re-index. drop_tokens_threshold for partial match.
Index sync patterns
- Dual-write (simplest): application writes DB + calls search index API in same request. Risk: partial failure. Use with small corpora.
- CDC-based (robust): Debezium captures Postgres WAL → Kafka topic → search indexer consumer. Guarantees eventual consistency without coupling.
- Scheduled batch: cron job queries
updated_at > last_run, bulk-indexes changes. Latency = cron interval. Acceptable for <5 min freshness.
- Full re-index: always write to new index alias, then swap. Never mutate live index schema.
Autocomplete / typeahead
- Edge-ngram analyzer on a dedicated
title.autocomplete sub-field (min_gram=2, max_gram=10).
- Or use Elasticsearch Completion Suggester for very fast prefix lookup.
- Debounce client-side to 200–300 ms; cancel in-flight requests on new keystroke.
Multi-tenancy isolation
- Index-per-tenant: strongest isolation, higher ops cost. Use when tenants number <1000 or have very different schemas.
- Single index + filter: mandatory
term filter injected server-side on tenant_id field (keyword, not analyzed). Enable index.query.default_field to prevent filter bypass. Use ES/OS role-based DLS (document-level security) in enterprise setups.
Common mistakes to avoid
from + size pagination past 10 000 — causes heap pressure; use search_after with sort tiebreaker.
- Analyzing filter fields —
category should be keyword, not text; analyzed text cannot be reliably filtered.
- Dynamic mapping enabled in production — unexpected field explosions cause mapping conflicts and OOM.
- Single shard for large corpus — no parallelism, query bottleneck; cannot split later without reindex.
- Embedding every document on write with synchronous API call — slows writes; batch embed asynchronously via queue.
- Trusting client-provided filters for tenant isolation — always inject tenant filter server-side.
- No zero-result monitoring — zero-result queries are a direct revenue/UX signal; alert weekly.
- Forgetting stopword lists per language — English stopwords on Spanish content wrecks recall.
- Shard count set too high — >1 shard per 30 GB wastes resources; over-sharding is harder to fix than under-sharding.
Output format
Produce artifacts in docs/search/ using .claude/templates/architecture.md adapted for search:
index-schema.json — full mapping/schema definition
query-patterns.md — documented query templates (keyword, vector, hybrid, autocomplete, facets)
sync-pipeline.md — indexing pipeline design (source, trigger, transform, bulk, error handling)
relevance-evaluation.md — golden dataset approach, metrics (NDCG, MRR), tuning methodology
- Code examples (language matching project stack) inline as fenced code blocks
Related checklists
.claude/checklists/architecture.md
.claude/checklists/performance.md
.claude/checklists/security.md
.claude/checklists/backend.md
Related agents
.claude/agents/engineering/backend-engineer.md
.claude/agents/engineering/search-engineer.md
.claude/agents/engineering/data-engineer.md
.claude/agents/engineering/database-architect.md
.claude/agents/quality/performance-engineer.md
.claude/agents/core/solution-architect.md
1---2name: search3description: Use for search/relevance features. Triggers — full-text, faceted, autocomplete, vector/semantic/hybrid search, Elasticsearch, OpenSearch, Meilisearch, Typesense, pgvector, Qdrant, Pinecone.4---56# Search & Relevance78**Scope: retrieval & relevance. Event analytics → `.claude/skills/analytics/SKILL.md`.**910## When to use11- Adding search to an application (product catalogue, docs, users, content)12- Improving relevance or ranking of existing search13- Adding semantic / AI-powered search (embeddings + vector DB)14- Implementing hybrid search (BM25 + vector recall merged with RRF)15- Designing index sync pipeline from primary DB to search engine16- Facets, filters, aggregations, geo-search, autocomplete17- Multi-tenant search with per-tenant index or filter isolation1819## Workflow20211. **Clarify requirements** — corpus size (docs count + avg size), query types (keyword, semantic, mixed), latency SLA (p99 <X ms), language(s), facets needed, freshness requirement (real-time vs eventually consistent), multi-tenancy?222. **Select engine** — Use decision matrix in Standards.233. **Design index schema** — Fields, data types, analyzers per language, `keyword` vs `text` mappings (ES/OS), sortable/filterable flags (Meilisearch/Typesense), dense_vector dimension.244. **Build indexing pipeline** — Source of truth → transformer → bulk index. For real-time: CDC (Debezium) or application-level dual-write. Batch: scheduled full re-index weekly + incremental sync on change events.255. **Implement query layer** — BM25 for keyword, ANN (HNSW) for vector. Hybrid: reciprocal rank fusion (RRF) or linear combination. Boost recency/popularity via `function_score` or equivalent.266. **Tune analyzers** — Language-specific tokenization (ICU plugin, kuromoji for Japanese). Custom synonym files per domain. Edge-ngram for prefix autocomplete. Phonetic/stemming for recall.277. **Relevance evaluation** — Define golden dataset (query + expected top-K). Measure NDCG@10, MRR. Run A/B or shadow test before promoting ranking changes.288. **Add facets & filters** — Keyword-mapped fields for facets. Cache heavy aggregations. Pagination with `search_after` (not `from+size` past 10k).299. **Observability** — Log query, latency, result count, zero-result rate. Alert on p99 > SLA. Track zero-result queries weekly for gaps.3010. **Security** — Tenant isolation: per-tenant index or mandatory filter injected server-side (never trust client). Role-based index-level permissions (ES/OS security plugin). No PII in indexed fields unless encrypted at field level.3111. **Scaling** — Shard count: 1 shard per ~30–50 GB of data. Replicas: 1 per shard in prod. For high-write: hot-warm-cold architecture.3233## Standards3435### Engine selection matrix3637| Requirement | Best choice | Avoid |38|---|---|---|39| Large corpus (>10M docs), complex aggregations, log analytics | **Elasticsearch 8.x** or **OpenSearch 2.x** | Typesense (limited aggregations) |40| Simple full-text, fast setup, SaaS/SMB product | **Typesense 0.26+** or **Meilisearch v1.x** | Elasticsearch (operational complexity) |41| Already on Postgres, <1M docs, vector search | **pgvector 0.7+** extension | Separate infra cost |42| Pure semantic / embedding search | **Qdrant**, **Weaviate**, or **Pinecone** | BM25-only engines |43| Hybrid (keyword + vector) production | **Elasticsearch** (RRF GA in 8.14) or **OpenSearch** (neural-search plugin) | pgvector alone (no BM25) |44| Managed, AWS-native | **OpenSearch Serverless** or **Amazon Kendra** | Self-hosted Kafka |45| Offline / edge / embedded | **Tantivy** (Rust) or **MiniSearch** (JS) | Elasticsearch |4647### Elasticsearch / OpenSearch index design48```json49{50 "settings": {51 "number_of_shards": 3,52 "number_of_replicas": 1,53 "analysis": {54 "analyzer": {55 "english_analyzer": {56 "type": "custom",57 "tokenizer": "standard",58 "filter": ["lowercase", "english_stop", "english_stemmer", "synonym_filter"]59 }60 }61 }62 },63 "mappings": {64 "properties": {65 "title": { "type": "text", "analyzer": "english_analyzer", "boost": 2 },66 "body": { "type": "text", "analyzer": "english_analyzer" },67 "title_kw": { "type": "keyword" },68 "category": { "type": "keyword" },69 "created_at": { "type": "date" },70 "embedding": { "type": "dense_vector", "dims": 1536, "index": true, "similarity": "cosine" }71 }72 }73}74```75- Never use dynamic mapping in production (`"dynamic": "strict"`).76- Use `_source: false` for fields only needed for ranking, not retrieval.77- Alias indices; re-index to new index then atomically swap alias (zero-downtime schema changes).7879### Hybrid search with RRF (Elasticsearch 8.14+, OpenSearch 2.11+)80```json81{82 "retriever": {83 "rrf": {84 "retrievers": [85 { "standard": { "query": { "match": { "body": "query text" } } } },86 { "knn": { "field": "embedding", "query_vector": [0.1, ...], "k": 50 } }87 ],88 "rank_window_size": 100,89 "rank_constant": 6090 }91 }92}93```94- RRF `rank_constant=60` is a reasonable default; tune via offline evaluation.95- Pre-compute embeddings with `text-embedding-3-small` (1536 dims) or `text-embedding-3-large` (3072 dims) for OpenAI; or `BAAI/bge-m3` for multilingual open-source.9697### pgvector (Postgres extension ≥0.7)98```sql99CREATE EXTENSION vector;100CREATE TABLE items (id BIGSERIAL PRIMARY KEY, content TEXT, embedding vector(1536));101CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)102 WITH (m = 16, ef_construction = 64);103SELECT id, content, 1 - (embedding <=> query_vec) AS score104FROM items ORDER BY embedding <=> query_vec LIMIT 20;105```106- HNSW is preferred over IVFFlat for recall (≥95% vs ~90%).107- Combine with `WHERE` clause for filtered ANN (partial index if selectivity >5%).108- `SET hnsw.ef_search = 100` at query time to trade latency for recall.109110### Meilisearch / Typesense111- **Meilisearch**: configure `searchableAttributes` order (title first), `filterableAttributes`, `sortableAttributes`. Vector search via `_vectors` field. Tenant token JWTs for multi-tenancy.112- **Typesense**: schema-first (all fields declared upfront). Use `Collection aliases` for zero-downtime re-index. `drop_tokens_threshold` for partial match.113114### Index sync patterns115- **Dual-write** (simplest): application writes DB + calls search index API in same request. Risk: partial failure. Use with small corpora.116- **CDC-based** (robust): Debezium captures Postgres WAL → Kafka topic → search indexer consumer. Guarantees eventual consistency without coupling.117- **Scheduled batch**: cron job queries `updated_at > last_run`, bulk-indexes changes. Latency = cron interval. Acceptable for <5 min freshness.118- **Full re-index**: always write to new index alias, then swap. Never mutate live index schema.119120### Autocomplete / typeahead121- Edge-ngram analyzer on a dedicated `title.autocomplete` sub-field (min_gram=2, max_gram=10).122- Or use Elasticsearch Completion Suggester for very fast prefix lookup.123- Debounce client-side to 200–300 ms; cancel in-flight requests on new keystroke.124125### Multi-tenancy isolation126- **Index-per-tenant**: strongest isolation, higher ops cost. Use when tenants number <1000 or have very different schemas.127- **Single index + filter**: mandatory `term` filter injected server-side on `tenant_id` field (keyword, not analyzed). Enable `index.query.default_field` to prevent filter bypass. Use ES/OS role-based DLS (document-level security) in enterprise setups.128129## Common mistakes to avoid130131- **`from + size` pagination past 10 000** — causes heap pressure; use `search_after` with sort tiebreaker.132- **Analyzing filter fields** — `category` should be `keyword`, not `text`; analyzed text cannot be reliably filtered.133- **Dynamic mapping enabled in production** — unexpected field explosions cause mapping conflicts and OOM.134- **Single shard for large corpus** — no parallelism, query bottleneck; cannot split later without reindex.135- **Embedding every document on write with synchronous API call** — slows writes; batch embed asynchronously via queue.136- **Trusting client-provided filters for tenant isolation** — always inject tenant filter server-side.137- **No zero-result monitoring** — zero-result queries are a direct revenue/UX signal; alert weekly.138- **Forgetting stopword lists per language** — English stopwords on Spanish content wrecks recall.139- **Shard count set too high** — >1 shard per 30 GB wastes resources; over-sharding is harder to fix than under-sharding.140141## Output format142143Produce artifacts in `docs/search/` using `.claude/templates/architecture.md` adapted for search:144- `index-schema.json` — full mapping/schema definition145- `query-patterns.md` — documented query templates (keyword, vector, hybrid, autocomplete, facets)146- `sync-pipeline.md` — indexing pipeline design (source, trigger, transform, bulk, error handling)147- `relevance-evaluation.md` — golden dataset approach, metrics (NDCG, MRR), tuning methodology148- Code examples (language matching project stack) inline as fenced code blocks149150## Related checklists151- `.claude/checklists/architecture.md`152- `.claude/checklists/performance.md`153- `.claude/checklists/security.md`154- `.claude/checklists/backend.md`155156## Related agents157- `.claude/agents/engineering/backend-engineer.md`158- `.claude/agents/engineering/search-engineer.md`159- `.claude/agents/engineering/data-engineer.md`160- `.claude/agents/engineering/database-architect.md`161- `.claude/agents/quality/performance-engineer.md`162- `.claude/agents/core/solution-architect.md`