Spaces:
Sleeping
Sleeping
File size: 6,560 Bytes
6fc17e7 b26d5a9 6fc17e7 b26d5a9 6fc17e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """
GCAS Search Engine β Query Processor
======================================
The single entry-point that orchestrates the full pre-search pipeline:
raw query (English / Hindi / Gujarati, possibly with ASR errors)
β
βΌ 1. normalizer.process_query()
β β’ detect language
β β’ transliterate Indic script β Latin
β β’ resolve city/university/ASR aliases
β β’ expand abbreviations
β
βΌ 2. fuzzy_matcher.analyze_entities()
β β’ find entity spans (college, district, program β¦)
β β’ multi-layer matching: exact β fuzzy β phonetic
β
βΌ 3. fuzzy_matcher.rewrite_query_with_entities()
β β’ replace near-miss spans with canonical entity names
β β’ produce final embedding-ready query string
β
βΌ 4. (after search) fuzzy_matcher.assess_confidence()
β’ combine FAISS score + entity match quality
β’ produce confidence_level + did_you_mean suggestions
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from normalizer import process_query as normalize_query
from fuzzy_matcher import (
EntityMatch,
analyze_entities,
assess_confidence,
generate_did_you_mean,
resolve_college_in_query,
rewrite_query_with_entities,
)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Pre-search result
# ---------------------------------------------------------------------------
@dataclass
class ProcessedQuery:
"""Everything the search engine needs, derived from the raw query."""
# The cleaned, entity-corrected query string to pass to the embedder
final_query: str
# Human-readable metadata about what was transformed
original_query: str
detected_language: str # "en" | "hi" | "gu" | "unknown"
corrected_query: str # after alias/abbreviation resolution
detected_entities: List[EntityMatch] = field(default_factory=list)
# Per-step intermediate strings (useful for debugging)
normalization_metadata: Dict[str, Any] = field(default_factory=dict)
# ---------------------------------------------------------------------------
# Post-search result enrichment
# ---------------------------------------------------------------------------
@dataclass
class SearchAnalysis:
"""Confidence + "did you mean" analysis produced after FAISS returns."""
confidence_level: str # "high" | "medium" | "low"
adjusted_score: float # 0-1 score after entity boost
did_you_mean: List[str] # human-readable suggestions
entity_corrections: List[Dict[str, str]] # [{from, to, type, score}]
# ---------------------------------------------------------------------------
# Pre-search pipeline
# ---------------------------------------------------------------------------
def process(raw_query: str) -> ProcessedQuery:
"""
Run the full pre-search normalisation and entity correction pipeline.
Parameters
----------
raw_query : str β raw input from user (voice transcript or chat text)
Returns
-------
ProcessedQuery β contains `final_query` ready for the embedding model
"""
# Step 1 β Language normalisation
corrected, lang, norm_meta = normalize_query(raw_query)
# Step 1b β College keyword resolution
# Checks CollegeNameSearchKeyword1-4 data: "MN" β "M. N. COLLEGE, VISNAGAR"
# Must run before geographic entity matching so college name is in place.
corrected, college_entities = resolve_college_in_query(corrected)
# Step 2 β Entity detection on the normalised query (districts/talukas only)
geo_entities = analyze_entities(corrected)
entities = college_entities + geo_entities
if entities:
logger.debug(
"Detected %d entity matches: %s",
len(entities),
[(e.query_span, e.matched, e.score) for e in entities],
)
# Step 3 β Rewrite query with canonical entity names
final = rewrite_query_with_entities(corrected, entities)
# If rewriting produced an empty string (shouldn't happen), fall back
if not final.strip():
final = corrected
logger.info(
"Query processed | lang=%s | raw=%r | final=%r | entities=%d",
lang, raw_query, final, len(entities),
)
return ProcessedQuery(
final_query=final,
original_query=raw_query,
detected_language=lang,
corrected_query=corrected,
detected_entities=entities,
normalization_metadata=norm_meta,
)
# ---------------------------------------------------------------------------
# Post-search analysis
# ---------------------------------------------------------------------------
def analyze_results(
processed: ProcessedQuery,
top_results: List[Dict[str, Any]],
) -> SearchAnalysis:
"""
After FAISS + LLM reranking, assess result quality and generate
"did you mean?" suggestions if confidence is low.
Parameters
----------
processed : ProcessedQuery from the pre-search step
top_results : list of result dicts from search_engine.search()
Returns
-------
SearchAnalysis
"""
top_score = max((r.get("score", 0.0) for r in top_results), default=0.0)
confidence_level, adjusted_score = assess_confidence(
top_faiss_score=top_score,
entities=processed.detected_entities,
)
did_you_mean: List[str] = []
if confidence_level in ("low", "medium"):
did_you_mean = generate_did_you_mean(
query=processed.final_query,
entities=processed.detected_entities,
top_faiss_score=top_score,
top_results=top_results,
)
# Summarise entity corrections for the API response
entity_corrections = [
{
"original_span": e.query_span,
"corrected_to": e.matched,
"entity_type": e.entity_type,
"match_score": e.score,
"method": e.method,
}
for e in processed.detected_entities
if e.query_span.lower() != e.matched.lower() # only show actual corrections
]
return SearchAnalysis(
confidence_level=confidence_level,
adjusted_score=adjusted_score,
did_you_mean=did_you_mean,
entity_corrections=entity_corrections,
)
|