System Design (High-Level Design) Module 14 – Search, geospatial and probabilistic data structures
Search for Indian languages and mixed scripts
How to normalise Indian-script text before indexing, split words without breaking vowel signs, and match queries typed in Latin letters or two scripts.
What you will learn
- Normalise Unicode text in Indian scripts the same way at index time and at query time
- Split Indian-script text into words without breaking vowel signs, viramas or nuktas
- Handle transliterated and code-mixed queries with a Latin field and a phonetic key
- Plan language coverage where built-in analysers are missing
- Measure relevance per language and per script with judged test queries
Before you start
What you’ll learn · Before you start
What you will learn
- Normalise Unicode text in Indian scripts the same way at index time and at query time
- Split Indian-script text into words without breaking vowel signs, viramas or nuktas
- Handle transliterated and code-mixed queries with a Latin field and a phonetic key
- Plan language coverage where built-in analysers are missing
- Measure relevance per language and per script with judged test queries
Before you start
On this page
Search for Indian languages fails in ways that English search rarely does, and three changes fix most of it. The same visible word can be stored as different sequences of Unicode code points, so every title and every query is first normalised to one form, NFC (Normalization Form C): The Unicode normal form that decomposes a string and then recomposes it into one standard sequence, so canonically equivalent spellings of the same text become the same code points.. Words are then split by Unicode’s rules, which keep vowel signs, viramas and nuktas inside their word, not by a pattern written for English. And many people type Hindi, Tamil or Bengali in Latin letters, “kurta” or “saree”, or type queries in mixed scripts, so the index also keeps a Latin form of every word. Elasticsearch ships analysers for Hindi and Bengali but not for Tamil, Telugu, Marathi, Kannada or Malayalam, so those languages are assembled from Unicode components and checked against real queries.
Two spellings that look the same
Unicode encodes many letters of Indian scripts in more than one way. Devanagari क़ (qa) is one code point, U+0958, and also क followed by the nukta sign. Tamil, Malayalam and Bengali write some vowel signs in two parts, typed as one code point or as the two parts. UAX #15 calls such sequences canonical equivalence: Unicode's rule that two sequences of code points, such as a precomposed letter and the letter followed by its mark, represent the same text and should look and behave the same.: they should look and behave the same, yet a comparison of code points sees different strings (UAX #15). Which form a title arrives in depends on the keyboard, the app and any converter it passed through. The example stores four words in one form and searches in the other:
import unicodedata
# Four words, each typed in two ways that look the same on screen. Escapes keep the code points visible.
WORDS = [
("Hindi, 'pen'", "\u0958\u0932\u092e", "\u0915\u093c\u0932\u092e"),
("Tamil, 'flag'", "\u0b95\u0bca\u0b9f\u0bbf", "\u0b95\u0bc6\u0bbe\u0b9f\u0bbf"),
("Bengali, 'bottle'", "\u09ac\u09cb\u09a4\u09b2", "\u09ac\u09c7\u09be\u09a4\u09b2"),
("Malayalam, 'Kochi'", "\u0d15\u0d4a\u0d1a\u0d4d\u0d1a\u0d3f", "\u0d15\u0d46\u0d3e\u0d1a\u0d4d\u0d1a\u0d3f"),
]
def code_points(text):
return " ".join(f"U+{ord(c):04X}" for c in text)
def nfc(text):
return unicodedata.normalize("NFC", text)
for label, a, b in WORDS:
print(f"{label}: {a}")
print(f" typed as {code_points(a)}")
print(f" or as {code_points(b)}")
print(f" equal? {a == b} equal after NFC? {nfc(a) == nfc(b)} NFC: {code_points(nfc(a))}")
# Titles stored in the first form, queries typed in the second: raw comparison, then NFC on both sides.
titles = [a for _, a, _ in WORDS]
queries = [b for _, _, b in WORDS]
raw_hits = sum(q in titles for q in queries)
index = {nfc(t) for t in titles}
nfc_hits = sum(nfc(q) in index for q in queries)
print(f"raw strings: {raw_hits} of {len(queries)} queries match; NFC on both sides: {nfc_hits} of {len(queries)}")
# Checks: both forms of every word normalise to one string, and normalising twice changes nothing.
assert all(nfc(a) == nfc(b) for _, a, b in WORDS)
assert all(nfc(nfc(a)) == nfc(a) for _, a, _ in WORDS)
print("checks passed") Output
Hindi, 'pen': क़लम typed as U+0958 U+0932 U+092E or as U+0915 U+093C U+0932 U+092E equal? False equal after NFC? True NFC: U+0915 U+093C U+0932 U+092E Tamil, 'flag': கொடி typed as U+0B95 U+0BCA U+0B9F U+0BBF or as U+0B95 U+0BC6 U+0BBE U+0B9F U+0BBF equal? False equal after NFC? True NFC: U+0B95 U+0BCA U+0B9F U+0BBF Bengali, 'bottle': বোতল typed as U+09AC U+09CB U+09A4 U+09B2 or as U+09AC U+09C7 U+09BE U+09A4 U+09B2 equal? False equal after NFC? True NFC: U+09AC U+09CB U+09A4 U+09B2 Malayalam, 'Kochi': കൊച്ചി typed as U+0D15 U+0D4A U+0D1A U+0D4D U+0D1A U+0D3F or as U+0D15 U+0D46 U+0D3E U+0D1A U+0D4D U+0D1A U+0D3F equal? False equal after NFC? True NFC: U+0D15 U+0D4A U+0D1A U+0D4D U+0D1A U+0D3F raw strings: 0 of 4 queries match; NFC on both sides: 4 of 4 checks passed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 normalise_indic.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Raw comparison finds none of the four. Normalisation Form C decomposes a string and recomposes it into one standard sequence, so both spellings become the same string and all four queries match. The Hindi word shows a detail worth knowing: NFC turns U+0958 into two code points, क and the nukta, because precomposed nukta letters are excluded from composition. Normalise when you index and again when you search, or the two sides drift apart. Step through two of the words:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Hindi, two ways. The Hindi word for pen can start with the one code point U+0958, or with क followed by the nukta sign. On screen the two look the same; to an index they are different strings.
- NFC on the Hindi word. NFC first decomposes, so U+0958 becomes क and the nukta. It would then recompose, but precomposed nukta letters are excluded from composition, so both spellings end as four code points.
- Tamil, two ways. The Tamil word for flag has a two-part vowel sign. It can be typed as the one code point U+0BCA, or as its two parts, U+0BC6 and U+0BBE.
- NFC on the Tamil word. Here NFC recomposes the two parts into U+0BCA, so the second spelling becomes the first. Either way the word is one string of four code points.
- One string each. Once titles and queries both go through NFC, each pair is one string, and in the program all four queries match. Normalise on one side only and the other side keeps its own spelling.
One chain for titles and queries
Text description of the diagram
The diagram is a column of steps, from top to bottom. The same steps run on every listing title and on every query.
- The input: a listing title or a query, written in Devanagari, Tamil or another script, in Latin letters, or in two scripts at once.
- Normalise to NFC, so that two spellings of one character become one sequence of code points.
- Split into words, keeping vowel signs, viramas and nuktas inside the word they belong to.
- Fold what NFC keeps apart: native digits, zero-width joiners and the older spelling of Malayalam chillu letters.
- Index every word twice: in its own script, and as a Latin key in which spellings such as kurta, kurtha and kurtaa become one term.
At the bottom: a query word matches a title when either form matches.
Search for Indian languages chains NFC with further steps, each removing another source of mismatch:
- Unicode word splitting, as in the ICU tokenizer, keeps every vowel sign, virama and nukta inside its word.
- Folding turns native digits into 0 to 9, removes zero-width joiners and merges both spellings of Malayalam chillu letters.
- A Latin field stores every word transliterated, so queries in Latin letters or in mixed scripts find Devanagari titles.
- A phonetic key folds spelling variants such as kurta, kurtha and kurtaa into one term.
- Per-language analysers add stop words and stemming where Elasticsearch has them.
- Judged test queries per language and per script show whether each change helped.
Checkpoint
Check: One string from two spellings
Splitting words: the regex failure reproduced Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Folding what NFC keeps apart, in a fixed order
- Matching Latin-letter and mixed-script queries, three designs measured
- Choosing analysers for the languages that have none
- Measuring relevance per language and per script
- Practise: match titles in any script
- Match Indian-script titles from queries in any script
- Interview questions
- Why must search normalise text to one Unicode form, such as NFC, both when it indexes a title and when it reads a query?
- Shoppers search a catalogue of Hindi product titles by typing words in Latin letters, such as kurta or saree, and sometimes mix both scripts in one query. Design the indexing and the query path so that these searches find the Devanagari listings, and say how you would know it works.
- After a popular keyboard app updates, some Hindi and Malayalam searches return nothing, although the listings exist and look identical to the queries on screen. How do you find the cause, fix it and stop it from coming back?
- Your marketplace adds search in ten Indian languages to an Elasticsearch cluster, and only Hindi and Bengali have built-in analysers. Plan the analysis chains, the index layout and how you decide that each language is ready to launch.
- Key takeaways
4 diagrams · 2 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 43 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- canonical equivalence
- Unicode's rule that two sequences of code points, such as a precomposed letter and the letter followed by its mark, represent the same text and should look and behave the same.
- chillu (Malayalam)
- A Malayalam consonant written without its inherent vowel as a letter of its own. It has a single code point, and older text writes it as the consonant, a virama and a zero-width joiner.
- combining mark
- A Unicode character, such as a vowel sign, a virama or a nukta in Indian scripts, that attaches to the character before it instead of standing alone.
- mean reciprocal rank (MRR)
- The average, over a set of queries, of one divided by the rank of the first relevant result, so a relevant result in first place scores 1 and one in third place scores 1/3.
- NFC (Normalization Form C)
- The Unicode normal form that decomposes a string and then recomposes it into one standard sequence, so canonically equivalent spellings of the same text become the same code points.
- phonetic key
- A folded spelling of a word, built by rules such as dropping vowel length or aspiration, so that variant spellings like kurta, kurtha and kurtaa share one term in the index.
References
- Unicode Standard Annex #15: Unicode Normalization Forms (Unicode Consortium)
- Unicode Standard Annex #29: Unicode Text Segmentation (Unicode Consortium)
- The Unicode Standard, chapter 12: South and Central Asia-I (Unicode Consortium)
- Python documentation: re, regular expression operations (Python Software Foundation)
- Elasticsearch documentation: Language analyzers (Elastic)
- Elasticsearch documentation: ICU analysis plugin (Elastic)
- Elasticsearch documentation: ICU tokenizer (Elastic)
- Elasticsearch documentation: ICU normalization character filter (Elastic)
- Elasticsearch documentation: ICU normalization token filter (Elastic)
- Elasticsearch documentation: Decimal digit token filter (Elastic)
- Elasticsearch documentation: Mapping character filter (Elastic)
- Elasticsearch documentation: Anatomy of an analyzer (Elastic)
- Elasticsearch documentation: Normalization token filters (Elastic)
- Apache Lucene source: IndicNormalizer.java (The Apache Software Foundation)
- Query Expansion for Mixed-Script Information Retrieval (Gupta, Bali, Banchs, Choudhury and Rosso) (ACM SIGIR (Microsoft Research copy))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress