query_analyzer module
On this page
Overview
Implements rule-based query understanding: language detection, intent classification, complexity estimation, keyword/entity extraction, and question-type tagging — entirely through regular expressions and static word lists. There is no machine learning, no external NLP library, and no network call anywhere in this module.
Module-level data
These are not part of the public API (not exported from the package root) but are referenced throughout this page since they fully determine the analyzer's behavior:
_INTENT_SIGNALS: a dict mapping eachQueryIntentto{"ar": [...], "en": [...]}phrase lists._COMPLEXITY_HIGH_SIGNALS/_COMPLEXITY_LOW_SIGNALS: compiled regexes for complexity heuristics._STOP_WORDS_AR/_STOP_WORDS_EN: sets of common words excluded from keyword extraction._ARABIC_RE: matches the Arabic Unicode block (\u0600–\u06FF)._ENTITY_RE: matches capitalized English phrases or 3+ character Arabic word sequences.
RuleBasedQueryAnalyzer
class RuleBasedQueryAnalyzer(QueryAnalyzerStrategy):
def __init__(self, config: ContextEngineConfig): ...
The default, and currently only, concrete implementation of QueryAnalyzerStrategy in the package.
Constructor
analyze(query)
def analyze(self, query: str) -> QueryAnalysis: ...
Parameters
Returns
A fully populated QueryAnalysis. Every field is set explicitly (no defaults left in place) for a non-empty query.
Raises
No exceptions are raised by this method under normal string input. It does not validate that query is actually a str — passing a non-string will raise inside .strip() or .lower() with a standard AttributeError, not a custom exception.
Internal workflow (in order)
_normalize(query)— strips, removes Arabic diacritics (tashkeel, Unicode range\u064B–\u0652), unifies alef variants (إأآا→ا), and collapses whitespace._detect_arabic(normalized)— Arabic-character ratio vs.arabic_threshold._extract_intent(normalized, lang)— counts signal-phrase occurrences per intent (checking both the detected language's signals and the other language's, since text can be mixed); the highest-count intent wins,FACTUALon a tie of zero._estimate_complexity(normalized, lang)— see below._extract_keywords(normalized, lang)— tokenizes, removes stop words, scores byfrequency * (1 + 0.05 * word_length)(favoring longer/rarer words), returns the top 12._extract_entities(normalized)— regex-based entity extraction plus anycustom_entitiesfrom config found as a substring (case-insensitive), deduplicated while preserving order, capped at 8._question_type(normalized, lang)—"wh-question","yes/no", or"open"based on prefix matching._compute_budget(complexity)— maps complexity tobudget_low/medium/high.- Constructs and returns the
QueryAnalysis, and logs a debug line with intent, complexity, and the first 5 keywords.
_estimate_complexity — exact thresholds
def _estimate_complexity(self, text: str, lang: str) -> QueryComplexity:
word_count = len(text.split())
if _COMPLEXITY_HIGH_SIGNALS.search(text):
return QueryComplexity.HIGH
if word_count > 20:
return QueryComplexity.HIGH
if _COMPLEXITY_LOW_SIGNALS.match(text) and word_count <= 7:
return QueryComplexity.LOW
if word_count <= 12:
return QueryComplexity.LOW
return QueryComplexity.MEDIUM
Checked in this exact order — a query matching the high-complexity regex (analyze|compare|evaluate|critique|... / قارن|حلل|قيّم|...) is always HIGH regardless of length. Anything 13–20 words that doesn't match either regex pattern is MEDIUM.
Complexity / Time / Space
- Time complexity: O(n) in query length for normalization and tokenization; intent/entity extraction are O(n × s) where s is the (small, fixed) number of signal phrases/patterns — effectively linear in practice for typical query lengths.
- Space complexity: O(n) — proportional to query length plus a fixed-size keyword/entity list (capped at 12 and 8 respectively).
Thread safety
RuleBasedQueryAnalyzer holds no mutable instance state beyond self.cfg (read-only after construction), so concurrent calls to analyze() from multiple threads on the same instance are safe — there is nothing to race on.
Side effects
Logs one logger.debug call per analyze() invocation. No file, network, or cache I/O.
_empty_analysis(query)
def _empty_analysis(self, query: str) -> QueryAnalysis: ...
Returns a QueryAnalysis with intent=UNKNOWN, complexity=LOW, normalized="", and context_budget=cfg.budget_low. Used when query is empty/whitespace.
build_query_analyzer(config, strategy=None)
def build_query_analyzer(
config: ContextEngineConfig,
strategy: Optional[QueryAnalyzerStrategy] = None,
) -> QueryAnalyzerStrategy: ...
A one-line factory: returns strategy if provided, otherwise constructs and returns a new RuleBasedQueryAnalyzer(config). Used internally by ContextEngine.__init__ when no query_analyzer is explicitly passed.
Examples
1. Basic:
from zerda.context import ContextEngineConfig, RuleBasedQueryAnalyzer
analyzer = RuleBasedQueryAnalyzer(ContextEngineConfig())
analysis = analyzer.analyze("What is machine learning?")
print(analysis.intent, analysis.complexity)
# QueryIntent.FACTUAL QueryComplexity.LOW
2. Intermediate — Arabic query:
analysis = analyzer.analyze("قارن بين التعلم الخاضع للإشراف والتعلم غير الخاضع للإشراف من حيث الدقة والتكلفة")
print(analysis.is_arabic, analysis.intent, analysis.complexity)
# True QueryIntent.COMPARATIVE QueryComplexity.HIGH
3. Advanced — custom entities:
from zerda.context import ContextEngineConfig, QueryAnalyzerConfig, RuleBasedQueryAnalyzer
cfg = ContextEngineConfig(query_analyzer=QueryAnalyzerConfig(custom_entities=["zerda", "RAG"]))
analyzer = RuleBasedQueryAnalyzer(cfg)
analysis = analyzer.analyze("How does zerda handle RAG context budgets?")
print(analysis.entities) # includes 'zerda' and 'RAG' even if the regex alone wouldn't have caught them
4. Error handling — empty query:
analysis = analyzer.analyze(" ")
print(analysis.intent, analysis.complexity, analysis.context_budget)
# QueryIntent.UNKNOWN QueryComplexity.LOW 800
5. Production usage — via the factory, inside an engine:
from zerda.context import ContextEngine, ContextEngineConfig, build_query_analyzer
cfg = ContextEngineConfig()
engine = ContextEngine(config=cfg, query_analyzer=build_query_analyzer(cfg))
Best practices
- If your domain has jargon the regexes won't catch as entities, populate
QueryAnalyzerConfig.custom_entitiesrather than trying to extend the regex. - Remember intent/complexity are heuristic, not semantic — a query like "why is the sky not green" will be tagged
REASONINGpurely because it contains "why", regardless of actual difficulty.
Common mistakes
- Assuming
context_budgeton the returnedQueryAnalysisis final —ContextEngine.run()may override it viaAdaptiveLearningSystem.suggest_budget()before retrieval happens. - Forgetting that
_question_typeonly looks at the start of the normalized text — a query phrased as a statement with an embedded question won't be detected as"wh-question".
Related
strategy.QueryAnalyzerStrategymodels.QueryAnalysis,models.QueryIntent,models.QueryComplexity- Concepts → Query intent and complexity
Version history
Not determinable from the source — no version tags, changelog, or git history were available in the analyzed files.