Arabic search in production: normalization, stemming and relevance QA
A practical Elasticsearch and OpenSearch pipeline for Arabic normalization, protected terms, judged queries and rollback-safe releases.

Arabic search rarely fails because a team forgot to add a search box. It fails in the gap between what a user types, what the index stores, and what the analyzer quietly changes along the way.
A product name may contain an alef with hamza in the catalogue and a bare alef in the query. A user may include harakat, tatweel or Arabic-Indic digits. A light stemmer may improve recall for common words, then damage a brand or stock-keeping code. None of those failures is obvious from a green deployment.
The production job is to make transformations explicit, protect terms that must remain exact, and test ranking with the language people use in your market. This guide gives MENA product and engineering teams a workable starting point for Elasticsearch or OpenSearch. The configuration is a template, not a performance claim. Validate it against your own catalogue, query logs and human judgments before rollout.
Separate Unicode equivalence from search equivalence
Two strings can look the same while having different code-point sequences. Unicode Standard Annex #15 defines NFC, NFD, NFKC and NFKD. It explains that normalized strings give equivalent strings a unique binary representation. That solves a Unicode representation problem. It does not decide which different Arabic spellings your search product should treat as equivalent.
That distinction matters. Unicode normalization belongs near ingestion boundaries because it makes storage and comparison more predictable. Search normalization is a retrieval choice. It may collapse letters or marks to gain recall, and every collapse can introduce a false match.
Lucene makes those retrieval choices concrete. Its ArabicNormalizer maps alef with hamza or madda to bare alef, teh marbuta to heh, and alef maksura to yeh. It also removes harakat and tatweel. These rules explain why queries such as إمارات and امارات, or مدرسة and مدرسه, can converge after analysis.
They also expose a ranking risk. Mapping ى to ي can collapse على and علي, even though one may be a preposition and the other a person's name. Normalization is useful, but it is not harmless. Keep the original field available for exact matching, display and debugging.
Security needs a separate pass. Unicode Technical Standard #39 provides mechanisms for detecting confusables, mixed scripts and mixed numbers. Use that guidance to flag suspicious identifiers, seller names and account-facing input. Do not assume a language analyzer is also a spoofing control.
Start with the analyzer you can explain
Elasticsearch documents its Arabic analyzer as a standard tokenizer followed by lowercase, decimal_digit, Arabic stopwords, Arabic normalization, an optional keyword marker, and an Arabic stemmer. Lucene's ArabicAnalyzer likewise uses Arabic normalization, light stemming and Arabic stopwords. OpenSearch also documents an Arabic analyzer.
The following Elasticsearch-style configuration mirrors that documented sequence. It is a starting template for a disposable index. It has not been benchmarked against your data, so inspect token output and run relevance tests before using it in production.
PUT /products_ar_v1
{
"settings": {
"analysis": {
"filter": {
"ar_stop": {
"type": "stop",
"stopwords": "_arabic_"
},
"ar_protected": {
"type": "keyword_marker",
"keywords": ["سامسونج", "نون", "S24"]
},
"ar_stemmer": {
"type": "stemmer",
"language": "arabic"
}
},
"analyzer": {
"ar_product_text": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"decimal_digit",
"ar_stop",
"arabic_normalization",
"ar_protected",
"ar_stemmer"
]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ar_product_text",
"search_analyzer": "ar_product_text",
"fields": {
"raw": { "type": "keyword" }
}
}
}
}
}
The protected-term list is deliberately small. Populate it from real brands, model names, place names and domain terms for which stemming causes damage. Treat it as versioned search configuration, with an owner and regression tests, rather than an ever-growing exception dump.
Before indexing documents, inspect what the analyzer emits:
POST /products_ar_v1/_analyze
{
"analyzer": "ar_product_text",
"text": [
"إمارات",
"الإِمَارَات",
"مدرسة",
"مدرسه",
"الـرياض",
"الرياض",
"هاتف ١٢٣",
"هاتف 123"
]
}
Record the input, emitted tokens and the reason for every intended equivalence. If a reviewer cannot explain a transformation, it is not ready to become invisible production behavior.
Credit: SultanByte. Based on the Unicode, Elastic and Lucene documentation linked in this guide.
Keep index-time and search-time behavior deliberate
Elastic says index and search analyzers usually should match. Matching analysis gives the query and stored text the same token rules, which is the safest default for a standard product or content field.
There are valid exceptions. Elastic identifies autocomplete and search-time synonyms as cases where the analyzers may differ. Make that difference field-specific and documented. Do not change the global search analyzer to fix one autocomplete requirement, then discover that normal queries now produce tokens the index never stored.
A practical mapping often keeps several representations:
- an analyzed Arabic text field for recall;
- a raw keyword subfield for exact values and diagnosis;
- a separate autocomplete field if the product needs prefix behavior;
- the untouched source string for display.
This separation makes failures easier to trace. If an exact title exists but the analyzed query misses it, compare the _analyze output on both sides. If an analyzed match ranks poorly, the problem is probably scoring, field weighting or competing documents rather than Unicode storage.
The Elastic ICU analysis plugin adds Unicode normalization, case folding, collation and transliteration. Those capabilities can help a multilingual index, but installing ICU is not a licence to fold every distinction. Choose each transformation for a named use case. Elastic also warns that ICU upgrades may require reindexing, so pin the plugin with the search stack and include analyzer output in upgrade tests.
Arabic is not one market setting
The W3C Arabic and Persian Layout Requirements covers Standard Arabic and Persian, and notes significant regional differences, including numeral conventions. Search QA should reflect that reality.
A GCC commerce catalogue, an Egyptian classifieds site and a Persian knowledge base do not share one complete query profile. Even where the same script appears, teams should build test cases from local content, keyboards, numeral habits, names and product terminology. Do not route Persian text through an Arabic configuration simply because the letters look related. Test each supported language and market as its own retrieval surface.
The same principle applies outside the index. A multilingual site must preserve language and direction across routes, rendering and caches. SultanByte's guide to Arabic and English routing, RTL and cache behavior in Next.js covers that frontend boundary. Search results are only useful if the destination page retains the intended locale.
Build a query set that can catch regressions
Start with a compact judged set that engineers can run on every analyzer or relevance change. Give each query an intent, expected documents, unacceptable documents and notes about the variation being tested.
Use pairs and edge cases such as:
| Query | Variant or comparison | What to inspect |
|---|---|---|
إمارات |
امارات |
Alef normalization and ranking stability |
الإِمَارَات |
الامارات |
Harakat removal plus stopword behavior |
مدرسة |
مدرسه |
Teh marbuta mapping and false positives |
على |
علي |
Collision risk between a word and a name |
الـرياض |
الرياض |
Tatweel removal |
هاتف ١٢٣ |
هاتف 123 |
Decimal digit handling |
سامسونج S24 |
unprotected stemmed form | Brand and model protection |
| Arabic text with Latin product code | Arabic-only version | Mixed-script intent, token boundaries and spoof review |
Add three result expectations to each case: the top result that should win, other acceptable results, and a result that must not enter the first page. That last category catches normalization collisions that recall-only tests miss.
Run the set against a fixed index snapshot. Capture analyzer tokens, ordered result IDs and scores. Review diffs rather than only pass/fail totals. A ranking change may be correct, but it should never be unexplained.
For judged evaluation, report Mean Reciprocal Rank when the first relevant result is the main concern. Use NDCG when relevance has grades or several results matter. Neither metric replaces query review. Segment both by language, market, query type and device input where the data supports it, otherwise a large easy-query bucket can hide a weak Arabic segment.
Join offline judgments to production signals
Production telemetry answers different questions from a judged set. Track at least:
- Zero-result rate: searches that return no results divided by all searches. Break it down by normalized form and original query so folding does not hide demand.
- Reformulation rate: sessions where a user changes the query soon after searching. Store the sequence so reviewers can see whether users removed harakat, changed numerals, switched scripts or corrected spelling.
- Click-through rate: searches with a result click divided by searches with results. Read it beside rank position and result type. A click does not prove relevance, but a sudden fall after an analyzer change deserves inspection.
- MRR and NDCG: offline ranking measures computed from the judged query set. Keep judgments and index snapshots versioned so releases remain comparable.
Do not publish one blended dashboard number and call the system healthy. Create slices for Arabic-only queries, mixed-script queries, brand and model searches, names, numeral variants, and queries affected by each normalization rule. Review high-volume queries and expensive failures, but retain a stable regression set so rare names and edge cases do not disappear from QA.
Ship analyzer changes like schema changes
An analyzer change alters indexed tokens. Treat it as a migration: create a versioned index, reindex the same source documents, run the judged set, compare production-like query slices, and switch an alias only after review. Keep the previous index available for a controlled rollback.
The release checklist is short enough to enforce:
- Compare
_analyzeoutput for the regression queries on old and new indices. - Review every new zero result and every changed top result.
- Check protected brands, models, names and exact-match fields.
- Test Arabic-Indic and European digits, harakat, tatweel and mixed scripts.
- Calculate MRR or NDCG on the fixed judgments, then inspect the largest per-query losses.
- Watch zero-result, reformulation and click-through rates after the alias switch.
Arabic search does not need a mysterious relevance layer. It needs visible transformations, market-specific judgments and releases that can be rolled back. Start with the documented analyzer, preserve the original text, and make every extra equivalence earn its place in the query set.




