Practical guide
How to implement full-text search with Elasticsearch
Start with user questions, not a cluster. Documents, analysis, and relevance must come from real searches.
In short
A search index is a derived model
Elasticsearch stores documents optimized for search. It analyzes text into terms and scores relevance while exact filters run without text scoring.
The primary database remains the source of truth. The search index must be fully rebuildable and incremental synchronization must tolerate delayed, repeated, and delete events.
Prepare
What you need
Before creating a mapping, list the most important queries and which results should rank first.
- Real search phrases including typos, synonyms, diacritics, and zero-result cases.
- A source of truth with an immutable document ID and a change version used to identify newer state.
- Text fields, exact filters, sorting, and values displayed in each result.
- Relevance and operational metrics: zero-result rate, clicks, conversions, p95 latency, and synchronization failures.
Steps 1 to 3
Build a dedicated search read model
One document should contain everything needed for a search result. Do not replace a relational join with dozens of follow-up queries.
1. Design mapping and text analysis
- Denormalize ID, name, description, category, brand, availability, and other values required for search and display.
- Map text as text and exact codes, filters, and aggregations as keyword or numeric fields. A name can have text and keyword multi-fields.
- Choose analysis for the content language. Test tokenization, lowercase, diacritics, stemming, and domain synonyms with real phrases.
- Version the mapping. A major analyzer change cannot fix existing terms without a new index and reindexing.
"name": {
"type": "text",
"analyzer": "english",
"fields": {"raw": {"type": "keyword"}}
} Official language analyzer documentation 2. Synchronize documents idempotently
- For initial indexing, read the database in batches and write with the Bulk API. Tune batch size to document size and cluster response.
- Publish changes through a reliable outbox or change feed. The consumer indexes by stable product ID, so a repeated event creates no duplicate.
- Handle ordering with a document version or updated_at comparison. An older message must never overwrite newer state.
- Deletion and hiding need explicit synchronization. A periodic reconciliation job compares the source of truth with the index and repairs gaps.
POST /products-v1/_bulk Official Elasticsearch Bulk API 3. Compose text queries and filters
- Use full-text match or multi_match for name, brand, and description and weight each field by importance.
- Put availability, tenant, category, and price range in bool.filter so they do not distort text relevance.
- Set a maximum page size and use search_after instead of unbounded from for deep traversal.
- Return ID, display values, and a stable documented ordering strategy. Do not query the primary database once per hit.
multi_match: query=name^4, brand^2, description; filter: active=true Official Elastic full-text documentation Step 4
Test relevance as a feature
Returning something is not enough. Important phrases need the right documents in the right order.
-
Build an expected-results set
For dozens of real phrases, store relevant products and acceptable ordering. Run it whenever mappings or weights change.
php bin/phpunit --filter SearchRelevance -
Verify an index rebuild
Build a new index from the source of truth, switch the alias, and compare document counts and control queries without downtime.
POST /_aliases -
Simulate a delayed event
Deliver an older event after a newer update. The document must stay new and reconciliation must report no difference.
When it goes wrong
Common mistakes
An exact filter finds nothing
The value may be mapped as analyzed text. Use a keyword field for codes, states, and aggregations and inspect the actual mapping.
GET /products/_mapping Results rank poorly
Create concrete relevance tests and adjust field weights, analysis, or synonyms. Do not judge quality from one hand-picked query.
The index contains stale data
Monitor synchronization lag, failed bulk items, and event ordering. Add reconciliation against the source of truth.
An analyzer change cannot be applied to the existing field
Build a new versioned index, populate and test it, then switch the alias atomically. Do not rewrite a production mapping blindly.
Done
Full-text search has a rebuildable index and measurable relevance.
Elasticsearch now serves as a derived search model: documents are rebuildable, changes synchronize, and relevance tests protect result order.