gsearch-api / normalizer.py
tanmay-bm's picture
fix: normalizer over-expansion + stopword keyword filter + intent/program coverage
6f30221
Raw
History Blame Contribute Delete
22.5 kB
"""
GCAS Search Engine – Query Normalizer
======================================
Handles three classes of input noise before the query reaches FAISS:
1. Language detection – detects English / Hindi / Gujarati
2. Script transliteration – Gujarati/Hindi Unicode → Latin (for fuzzy matching)
3. Alias resolution – maps regional spellings, ASR phonetic variants,
and common abbreviations to canonical forms used in the database
4. Text normalisation – lowercase, collapse whitespace, strip punctuation
Pipeline
--------
raw query
→ detect_language()
→ transliterate_to_latin() (only if Gujarati/Hindi Unicode)
→ expand_abbreviations()
→ resolve_aliases()
→ normalize_text()
→ cleaned query string + metadata dict
"""
from __future__ import annotations
import re
import unicodedata
from typing import Dict, Optional, Tuple
# ---------------------------------------------------------------------------
# 1. Known entity alias tables
# Keys are lowercase, stripped variants (incl. common ASR mistakes)
# Values are the canonical form found in the database
# ---------------------------------------------------------------------------
# ── Districts & Cities ──────────────────────────────────────────────────────
CITY_ALIASES: Dict[str, str] = {
# Ahmedabad variants (Gujarati: અમદાવાદ, Hindi: अहमदाबाद)
"amdavad": "Ahmedabad",
"amdabad": "Ahmedabad",
"ahemadabad": "Ahmedabad",
"ahemdabad": "Ahmedabad",
"ahmadabad": "Ahmedabad",
"ahmedabaad": "Ahmedabad",
"ahmed abad": "Ahmedabad",
"ahmedabad": "Ahmedabad",
# Vadodara / Baroda
"baroda": "Vadodara",
"vadodra": "Vadodara",
"vadodar": "Vadodara",
"barod": "Vadodara",
# Surat
"soorat": "Surat",
"sorat": "Surat",
"suret": "Surat",
# Rajkot
"rajkote": "Rajkot",
"rajkott": "Rajkot",
"raj kot": "Rajkot",
# Bhavnagar
"bhawnagar": "Bhavnagar",
"bhavanagar": "Bhavnagar",
"bhavnagar": "Bhavnagar",
"bhaunagar": "Bhavnagar",
# Gandhinagar
"gandhi nagar": "Gandhinagar",
"gandhinagr": "Gandhinagar",
# Mehsana
"mahesana": "Mehsana",
"mehsana": "Mehsana",
"mehasana": "Mehsana",
"mahisana": "Mehsana",
# Anand
"aanand": "Anand",
"annad": "Anand",
# Kutch / Kachchh
"kutch": "Kutch",
"kachchh": "Kutch",
"kaach": "Kutch",
"kachch": "Kutch",
# Junagadh
"junagarh": "Junagadh",
"junagar": "Junagadh",
"junaagadh": "Junagadh",
# Jamnagar
"jamnagar": "Jamnagar",
"jamnagr": "Jamnagar",
# Surendranagar
"surendranagar": "Surendra Nagar",
"surendra nagar": "Surendra Nagar",
# GirSomnath
"gir somnath": "GirSomnath",
"girsomnath": "GirSomnath",
# Banaskantha
"banaskanta": "Banaskantha",
"banas kantha": "Banaskantha",
# Sabarkantha
"sabar kantha": "Sabarkantha",
"sabarkanta": "Sabarkantha",
# Navsari
"navsari": "Navsari",
"navsarri": "Navsari",
# Valsad
"valsad": "Valsad",
"valsaad": "Valsad",
# Patan
"patan": "Patan",
# Bharuch
"bharuch": "Bharuch",
"bharooch": "Bharuch",
"broach": "Bharuch",
# Amreli
"amreli": "Amreli",
"amrely": "Amreli",
# Porbandar
"porbandar": "Porbandar",
"porbander": "Porbandar",
# Dahod
"dahod": "Dahod",
"daaod": "Dahod",
# Panchmahal
"panch mahal": "Panchmahal",
"panchmahal": "Panchmahal",
"panchmahals": "Panchmahal",
# Chhota Udepur
"chhota udepur": "Chhota Udepur",
"chota udepur": "Chhota Udepur",
# Narmada
"narmada": "Narmada",
# Navsari
"navsari": "Navsari",
# Tapi
"tapi": "TAPI",
# Aravalli
"aravalli": "Aravalli",
"aravali": "Aravalli",
# Morbi
"morbi": "Morbi",
"morvi": "Morbi",
# Botad
"botad": "Botad",
# Mahisagar
"mahisagar": "Mahisagar",
# Devbhumi Dwarka
"dwarka": "DEVBHUMI DWARKA",
"devbhumi dwarka": "DEVBHUMI DWARKA",
"deobhumi dwarka": "DEVBHUMI DWARKA",
# Dang
"dang": "Dang",
# Kheda
"kheda": "Kheda",
"kaira": "Kheda",
"nadiad": "Kheda",
"visnagar": "Mehsana",
"vallabh vidyanagar": "Anand",
"vallabhpur": "Anand",
"vapi": "Valsad",
"godhra": "Panchmahal",
"palanpur": "Banaskantha",
"ankleshwar": "Bharuch",
"dahej": "Bharuch",
"halol": "Panchmahal",
}
# ── University abbreviations & common mistakes ──────────────────────────────
UNIVERSITY_ALIASES: Dict[str, str] = {
"gtu": "Gujarat Technological University",
"gujarat tech university": "Gujarat Technological University",
"gujarat technical university": "Gujarat Technological University",
"vnsgu": "VEER NARMAD SOUTH GUJARAT UNIVERSITY",
"veer narmad university": "VEER NARMAD SOUTH GUJARAT UNIVERSITY",
"south gujarat university": "VEER NARMAD SOUTH GUJARAT UNIVERSITY",
"spu": "SARDAR PATEL UNIVERSITY",
"sardar patel university": "SARDAR PATEL UNIVERSITY",
"hngu": "HEMCHANDRACHARYA NORTH GUJARAT UNIVERSITY, PATAN",
"north gujarat university": "HEMCHANDRACHARYA NORTH GUJARAT UNIVERSITY, PATAN",
"hemchandracharya university": "HEMCHANDRACHARYA NORTH GUJARAT UNIVERSITY, PATAN",
"msu": "THE MAHARAJA SAYAJIRAO UNIVERSITY OF BARODA",
"maharaja sayajirao university": "THE MAHARAJA SAYAJIRAO UNIVERSITY OF BARODA",
"ms university": "THE MAHARAJA SAYAJIRAO UNIVERSITY OF BARODA",
"msu baroda": "THE MAHARAJA SAYAJIRAO UNIVERSITY OF BARODA",
"gu": "GUJARAT UNIVERSITY",
"gujarat university": "GUJARAT UNIVERSITY",
"sau": "SAURASHTRA UNIVERSITY",
"saurashtra university": "SAURASHTRA UNIVERSITY",
"saurastra university": "SAURASHTRA UNIVERSITY",
"ksv": "KRANTIGURU SHYAMJI KRISHNA VERMA KACHCHH UNIVERSITY",
"kachchh university": "KRANTIGURU SHYAMJI KRISHNA VERMA KACHCHH UNIVERSITY",
"kutch university": "KRANTIGURU SHYAMJI KRISHNA VERMA KACHCHH UNIVERSITY",
"mkbu": "MAHARAJA KRISHNAKUMARSINHJI BHAVNAGAR UNIVERSITY",
"bhavnagar university": "MAHARAJA KRISHNAKUMARSINHJI BHAVNAGAR UNIVERSITY",
"bknmu": "BHAKTA KAVI NARSINH MEHTA UNIVERSITY",
"narsinh mehta university": "BHAKTA KAVI NARSINH MEHTA UNIVERSITY",
"junagadh university": "BHAKTA KAVI NARSINH MEHTA UNIVERSITY",
"sggu": "SHRI GOVIND GURU UNIVERSITY",
"govind guru university": "SHRI GOVIND GURU UNIVERSITY",
"sssu": "SHREE SOMNATH SANSKRIT UNIVERSITY, VERAVAL",
"somnath university": "SHREE SOMNATH SANSKRIT UNIVERSITY, VERAVAL",
"iite": "INDIAN INSTITUTE OF TEACHER EDUCATION",
}
# ── Course / Program abbreviations ──────────────────────────────────────────
COURSE_ALIASES: Dict[str, str] = {
# Engineering
"btech": "B.TECH",
"b tech": "B.TECH",
"b.tech.": "B.TECH",
"be": "BACHELOR OF ENGINEERING",
"bachelor of engineering": "BACHELOR OF ENGINEERING",
"mtech": "M.TECH",
"m tech": "M.TECH",
"me": "MASTER OF ENGINEERING",
# Management
"mba": "MASTER OF BUSINESS ADMINISTRATION",
"bba": "BACHELOR OF BUSINESS ADMINISTRATION",
"pgdm": "POST GRADUATE DIPLOMA IN MANAGEMENT",
# Computer
"mca": "MASTER OF COMPUTER APPLICATIONS",
"bca": "BACHELOR OF COMPUTER APPLICATIONS",
# Commerce
"bcom": "B.COM.",
"b com": "B.COM.",
"b.com": "B.COM.",
"mcom": "M.COM.",
"m com": "M.COM.",
# Science
"bsc": "B.SC.",
"b sc": "B.SC.",
"b.sc": "B.SC.",
"msc": "M.SC.",
"m sc": "M.SC.",
"m.sc": "M.SC.",
# Arts
"ba": "BACHELOR OF ARTS",
"ma": "MASTER OF ARTS",
# Law
"llb": "BACHELOR OF LAWS",
"ll.b": "BACHELOR OF LAWS",
"llm": "MASTER OF LAWS",
"ll.m": "MASTER OF LAWS",
# Medicine / Pharmacy
"mbbs": "BACHELOR OF MEDICINE AND BACHELOR OF SURGERY",
"bpharm": "B.PHARM.",
"b pharm": "B.PHARM.",
"b.pharm": "B.PHARM.",
"mpharm": "M.PHARM.",
"m pharm": "M.PHARM.",
"dpharm": "D.PHARM.",
"d pharm": "D.PHARM.",
# Architecture / Design
"barch": "B.ARCH.",
"b arch": "B.ARCH.",
"march": "M.ARCH.",
"bdes": "B.DES.",
"mdes": "M.DES.",
# Education
"bed": "BACHELOR OF EDUCATION",
"b ed": "BACHELOR OF EDUCATION",
"med": "MASTER OF EDUCATION",
"m ed": "MASTER OF EDUCATION",
"bped": "BACHELOR OF PHYSICAL EDUCATION",
# Social work
"msw": "MASTER OF SOCIAL WORK",
"bsw": "BACHELOR OF SOCIAL WORK",
# Research
"phd": "DOCTOR OF PHILOSOPHY",
"ph d": "DOCTOR OF PHILOSOPHY",
"ph.d": "DOCTOR OF PHILOSOPHY",
"mphil": "MASTER OF PHILOSOPHY",
# Hotel / Hospitality
"bhmct": "BACHELOR OF HOTEL MANAGEMENT AND CATERING TECHNOLOGY",
"mhmct": "MASTER OF HOTEL MANAGEMENT AND CATERING TECHNOLOGY",
# Fine Arts
"bfa": "BACHELOR OF FINE ARTS",
"mfa": "MASTER OF FINE ARTS",
# Nursing
"bsc nursing": "B.SC. NURSING",
"gnm": "GENERAL NURSING AND MIDWIFERY",
# NOTE: Single common English words like "commerce", "arts", "science",
# "law", "computer" are intentionally NOT expanded here because they
# appear in college names (e.g. "Gujarat Commerce College") and
# expanding them before college resolution corrupts the college name.
# The query_planner._PROGRAM_ALIASES handles program detection from
# these plain words independently after college resolution.
"nursing": "B.SC. NURSING",
}
# ── Common ASR phonetic mistakes (English accent variants) ──────────────────
# These arise from Indian English speech recognition errors
ASR_PHONETIC_ALIASES: Dict[str, str] = {
# University name phonetic errors
"saurashtra": "SAURASHTRA UNIVERSITY",
"sourastra": "SAURASHTRA UNIVERSITY",
"saurastra": "SAURASHTRA UNIVERSITY",
"saurashra": "SAURASHTRA UNIVERSITY",
"hemchandracharya": "HEMCHANDRACHARYA NORTH GUJARAT UNIVERSITY, PATAN",
"hemchandra": "HEMCHANDRACHARYA NORTH GUJARAT UNIVERSITY, PATAN",
"krantiguru": "KRANTIGURU SHYAMJI KRISHNA VERMA KACHCHH UNIVERSITY",
# Common English→Indian accent ASR variants
"collage": "college",
"colege": "college",
"coledge": "college",
"univercity": "university",
"universty": "university",
"univeristy": "university",
"addmission": "admission",
"admision": "admission",
"cutof": "cutoff",
"cut off": "cutoff",
"enginering": "engineering",
"engeenering": "engineering",
"engeneering": "engineering",
"manegment": "management",
"managment": "management",
"comercial": "commerce",
"comerce": "commerce",
"infrastucture": "infrastructure",
"infrastrucutre": "infrastructure",
"hostle": "hostel",
"hosttel": "hostel",
"scolarship": "scholarship",
"sholarship": "scholarship",
"faculity": "faculty",
"facalty": "faculty",
"labratry": "laboratory",
"labratory": "laboratory",
"librery": "library",
"libary": "library",
"placment": "placement",
"plcament": "placement",
"afiliation": "affiliation",
"afilliation": "affiliation",
"govenment": "government",
"goverment": "government",
"scince": "science",
"sience": "science",
}
# ── Gujarati common words transliterated (ASR output from Gujarati speech) ──
# Maps common Gujarati words that may appear in queries to their English equivalents
GUJARATI_WORD_ALIASES: Dict[str, str] = {
# Location prepositions / particles
"ma": "in", # "Ahmedabad ma" = "in Ahmedabad"
"ni": "of", # "GTU ni college" = "college of GTU"
"no": "of",
"na": "of",
"thi": "from",
"mate": "for",
"ane": "and",
"ke": "and", # Hindi: "aur" / Gujarati "ane"
# Common query words in Gujarati
"college": "college",
"vishwavidyalaya": "university",
"pravesh": "admission",
"merit": "merit",
"shikshan": "education",
"abhyas": "course",
"jilla": "district",
"shaher": "city",
# Hindi multi-word phrases (processed before single words due to length-sorted matching)
"ladkiyon ke liye": "for girls",
"ladkon ke liye": "for boys",
"ke liye": "for", # "girls ke liye" = "for girls"
"ki fees": "fees",
"ki jankari": "information",
"ki jaankari": "information",
"se kam": "less than",
"se jyada": "more than",
"se adhik": "more than",
"se zyada": "more than",
"ke saath": "with",
"ke sath": "with",
"ke baad": "after",
"ke upar": "above",
"ke neeche": "below",
"ke andar": "inside",
"ke under": "under", # "GTU ke under" = "under GTU"
"ke baare mein": "", # "M N College ke baare mein" → "M N College"
"ke baare": "",
"ke vishay mein": "",
"milta hai": "available",
"milti hai": "available",
"milte hain": "available",
"milega": "available",
"milegi": "available",
"kahan milta": "where available",
"kahan milegi": "where available",
"kahan hain": "where",
"saal ka": "year",
"saal ki": "year",
"saal ke": "year",
"kitne saal": "how many years",
"kya hai": "",
"kya hain": "",
"kya hogi": "",
"kitni hai": "",
"kitna hai": "",
"batao bhai": "",
"bata do": "",
# Hindi common query words (single tokens)
"mein": "in",
"ka": "of",
"ki": "of",
"ke": "of",
"liye": "for", # "ladkiyon liye" fallback
"aur": "and",
"wale": "", # "Ahmedabad wale college" → "Ahmedabad college"
"wali": "",
"koi": "",
"kaunse": "",
"kaun": "",
"kitna": "",
"kitne": "",
"fees": "fees",
"kitni": "",
"hai": "",
"hain": "",
"hogi": "",
"kya": "",
"kaise": "",
"kaisi": "",
"kab": "",
"batao": "",
"bataiye": "",
"vidyalay": "school",
"mahavidyalaya": "college",
"vishvavidhyalay": "university",
"pravesh": "admission",
"ank": "marks",
"ankpatti": "marksheet",
"chhatravritti": "scholarship",
"ladkiyon": "girls",
"ladkon": "boys",
"vanijya": "commerce",
"vigyan": "science",
"vidnyan": "science",
"kala": "arts",
"kanoon": "law",
"kanun": "law",
"prabandhan": "management",
"shikshan": "education",
"private": "self finance",
"sarkari": "government",
"aided": "grant in aid",
}
# ---------------------------------------------------------------------------
# 2. Language detection
# ---------------------------------------------------------------------------
def detect_language(text: str) -> str:
"""
Detect the language of the input text.
Returns: "en" | "hi" | "gu" | "unknown"
Strategy:
1. Unicode block analysis (fast, no dependencies)
2. Optionally backed by langdetect library if installed
"""
# Check for Devanagari (Hindi): U+0900–U+097F
devanagari = sum(1 for ch in text if '\u0900' <= ch <= '\u097F')
# Check for Gujarati script: U+0A80–U+0AFF
gujarati = sum(1 for ch in text if '\u0A80' <= ch <= '\u0AFF')
total = len(text.replace(" ", "")) or 1
if gujarati / total > 0.15:
return "gu"
if devanagari / total > 0.15:
return "hi"
# Try langdetect if available (handles Romanised Hindi/Gujarati)
try:
from langdetect import detect
lang = detect(text)
if lang in ("hi", "gu", "en"):
return lang
# langdetect sometimes says "mr" or "ne" for Gujarati/Hindi Romanised
return "en"
except Exception:
return "en"
# ---------------------------------------------------------------------------
# 3. Script transliteration (Gujarati/Hindi Unicode → Latin)
# ---------------------------------------------------------------------------
def transliterate_to_latin(text: str) -> str:
"""
Convert Gujarati or Hindi Unicode text to approximate Latin representation
using the indic-transliteration library. Falls back to unicodedata
normalization if the library is not installed.
"""
try:
from indic_transliteration import sanscript
from indic_transliteration.sanscript import transliterate
# Detect script
gujarati_chars = sum(1 for ch in text if '\u0A80' <= ch <= '\u0AFF')
devanagari_chars = sum(1 for ch in text if '\u0900' <= ch <= '\u097F')
if gujarati_chars > devanagari_chars:
return transliterate(text, sanscript.GUJARATI, sanscript.ITRANS).lower()
elif devanagari_chars > 0:
return transliterate(text, sanscript.DEVANAGARI, sanscript.ITRANS).lower()
return text
except ImportError:
# Fallback: unicode normalization strips diacritics (partial help)
nfkd = unicodedata.normalize("NFKD", text)
ascii_text = nfkd.encode("ascii", "ignore").decode("ascii")
return ascii_text if ascii_text.strip() else text
# ---------------------------------------------------------------------------
# 4. Abbreviation expansion
# ---------------------------------------------------------------------------
def expand_abbreviations(text: str) -> str:
"""
Expand university / course abbreviations to their full canonical names.
Operates on the lowercased version but returns title-cased expansions.
"""
lower = text.lower().strip()
# Check full-string match first (e.g., "GTU" alone)
if lower in UNIVERSITY_ALIASES:
return UNIVERSITY_ALIASES[lower]
if lower in COURSE_ALIASES:
return COURSE_ALIASES[lower]
# Replace abbreviations that appear as whole words in the text
tokens = re.split(r'(\s+)', lower) # split preserving whitespace
result_tokens = []
i = 0
while i < len(tokens):
token = tokens[i].strip()
if not token:
result_tokens.append(tokens[i])
i += 1
continue
# Try two-word phrases first (e.g., "b tech", "m tech")
if i + 2 < len(tokens):
two_word = (tokens[i] + " " + tokens[i+2]).strip()
if two_word in COURSE_ALIASES:
result_tokens.append(COURSE_ALIASES[two_word])
i += 3
continue
# Single token
if token in UNIVERSITY_ALIASES:
result_tokens.append(UNIVERSITY_ALIASES[token])
elif token in COURSE_ALIASES:
result_tokens.append(COURSE_ALIASES[token])
else:
result_tokens.append(tokens[i])
i += 1
return "".join(result_tokens)
# ---------------------------------------------------------------------------
# 5. Alias resolution (city names, ASR phonetics, Gujarati/Hindi words)
# ---------------------------------------------------------------------------
def resolve_aliases(text: str) -> str:
"""
Replace known regional / ASR variant spellings with canonical forms.
Returns the text with substitutions applied.
"""
lower = text.lower()
# Merge all alias maps: city > asr_phonetic > gujarati_words
# (process city/university aliases as multi-word phrases first)
combined: Dict[str, str] = {}
combined.update(ASR_PHONETIC_ALIASES)
combined.update(GUJARATI_WORD_ALIASES)
combined.update(CITY_ALIASES)
# Sort by length descending so longer phrases are matched first
for alias, canonical in sorted(combined.items(), key=lambda x: -len(x[0])):
pattern = r'\b' + re.escape(alias) + r'\b'
lower = re.sub(pattern, canonical.lower(), lower, flags=re.IGNORECASE)
return lower
# ---------------------------------------------------------------------------
# 6. Text normalisation (final clean-up)
# ---------------------------------------------------------------------------
def normalize_text(text: str) -> str:
"""
Lowercase, collapse whitespace, remove leading/trailing punctuation.
Preserves dots inside abbreviations (B.Tech, M.Sc).
Also deduplicates consecutive repeated words that arise from alias
expansion (e.g. "GTU" → "Gujarat Technological University" + "university"
remaining from the original query → "...University university...").
"""
text = text.strip()
# Collapse multiple spaces/newlines
text = re.sub(r'\s+', ' ', text)
# Remove leading/trailing punctuation that is not alpha-numeric
text = text.strip('.,;:!?"\'-')
# Deduplicate consecutive identical words (case-insensitive)
text = re.sub(
r'\b(\w+)\s+\1\b',
r'\1',
text,
flags=re.IGNORECASE,
)
# One more whitespace collapse after dedup
text = re.sub(r'\s+', ' ', text).strip()
return text
# ---------------------------------------------------------------------------
# 7. Full pipeline
# ---------------------------------------------------------------------------
def process_query(raw_query: str) -> Tuple[str, str, Dict]:
"""
Full pre-processing pipeline.
Parameters
----------
raw_query : raw user input (may be English, Hindi, or Gujarati)
Returns
-------
(corrected_query, detected_language, metadata)
corrected_query – cleaned, normalised query ready for embedding
detected_language – "en" | "hi" | "gu" | "unknown"
metadata – {"original": str, "transliterated": str,
"after_alias": str, "language": str}
"""
original = raw_query
# Step 1 – Detect language
lang = detect_language(raw_query)
# Step 2 – Transliterate if Indic script
if lang in ("gu", "hi"):
transliterated = transliterate_to_latin(raw_query)
else:
transliterated = raw_query
# Step 3 – Resolve Gujarati/Hindi particles and city/ASR aliases
after_alias = resolve_aliases(transliterated)
# Step 4 – Expand abbreviations
after_expand = expand_abbreviations(after_alias)
# Step 5 – Final normalisation
corrected = normalize_text(after_expand)
metadata = {
"original": original,
"detected_language": lang,
"transliterated": transliterated if lang in ("gu", "hi") else None,
"after_alias_resolution": after_alias,
"corrected_query": corrected,
}
return corrected, lang, metadata