Query Intent Clarification ranks company profiles against business search queries. It is designed for cases where simple semantic similarity is not enough, such as distinguishing a packaging supplier from a cosmetics brand or a logistics operator from logistics software.
The pipeline combines structured parsing, query expansion, lexical retrieval, optional dense retrieval, deterministic scoring, contradiction penalties, and explainable JSON output.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txtAfter dependency changes, refresh the existing environment with:
python -m pip install -r requirements.txtPlace the company dataset at:
data/companies.jsonl
The file should be JSONL, with one company object per line.
The pipeline is staged so cheap deterministic logic does most of the work, while expensive or platform-sensitive components remain optional.
-
Load company records
loader.pyreadsdata/companies.jsonl, one JSON object per line. -
Normalize company data
normalizer.pyconverts raw records intoCompanymodels frommodels.py. It flattens NAICS fields, normalizes scalar/list fields, parses numeric values, derives country and region from address or country-code TLD fallback, and buildssearchable_textfor retrieval.company_cache.pycaches the normalized company records under.cache/normalized_companies.jsonusing a content hash of the source JSONL. Ifdata/companies.jsonlchanges, the cache is rebuilt automatically. -
Parse query intent
language.pydetects the query language withlangdetectwhen available and falls back to English if detection is disabled or unavailable.translation.pycan optionally translate non-English queries before deterministic parsing. Translation is disabled by default and falls back to the original query if the configured provider is unavailable.intent_parser.pyconverts each query into anIntentmodel. It records language and translation metadata, preserves the original query, and extracts industries, roles, offerings, locations, regions, business models, target markets, public/private constraints, employee thresholds, revenue thresholds, founding-year constraints, and query complexity.If a query contains important terms that are not covered by the static taxonomy, the parser keeps those unknown terms as positive retrieval terms instead of dropping them. This makes the retrieval layer more robust for new datasets and less dependent on hardcoded industry aliases.
-
Expand the query
query_expansion.pyturns the parsed intent into a richer retrieval query. It uses taxonomy seed terms, role terms, offering aliases, NAICS hints, locations, regions, and optional corpus-derived terms. Negative terms stay separate so they can be used for penalties instead of retrieval.dynamic_taxonomy.pyderives extra expansion terms from high-signal company fields such as NAICS labels, offerings, business models, target markets, and descriptions. If no known taxonomy concept is detected, it can also use the preserved fallback query terms as seeds. The derived terms are cached under.cache/dynamic_taxonomy.jsonusing a hash of those high-signal fields. -
Retrieve candidate companies
retrieval/bm25.pyperforms BM25 lexical retrieval oversearchable_text.The BM25 tokenized corpus is cached under
.cache/bm25_corpus.jsonusing a hash of company IDs and searchable text. If normalized company text changes, the BM25 cache is rebuilt automatically.retrieval/embeddings.pyoptionally performs local dense retrieval withsentence-transformers/paraphrase-multilingual-MiniLM-L12-v2.Company embeddings are cached under
.cache/after the first run, so later runs can reuse them instead of re-encoding every company profile.retrieval/vector_index.pyprovides exact brute-force cosine search by default and an optional HNSWLib approximate vector index for larger datasets.The normalized brute-force vector matrix or HNSW index is cached under
.cache/vector_index/when dense retrieval is enabled, so later runs can avoid rebuilding the vector-search structure. -
Union retrieval hits
ranker.pymerges BM25 and embedding hits bycompany_idand preserves per-source retrieval scores such as:{ "bm25": 0.72, "embedding": 0.64 }If retrieval is disabled or returns no hits, the pipeline falls back to scoring all companies.
-
Apply filters
filters.pyapplies hard filters only when the query explicitly asks for that field. For example,public software companiesfilters onis_public, butsoftware companiesdoes not.It also adds soft signals for missing or mismatched geography, startup hints, missing NAICS labels, and missing role/offering fields.
-
Score candidates
scorers.pycomputes component scores for structured constraints, geography, industry, offering, role, business model, target market, BM25 retrieval, and embedding retrieval.Weights adapt by query complexity:
- structured queries emphasize explicit fields and geography
- hybrid queries balance structured and semantic signals
- reasoning-heavy queries emphasize role, offering, and retrieval signals
-
Apply penalties
scorers.pysubtracts numeric penalties for soft-filter problems such as wrong requested country or region, missing geography, and missing explicit structured fields.It also applies contradiction penalties for common false positives, such as:
- logistics software instead of logistics operators
- cosmetics brands instead of packaging suppliers
- renewable installers instead of equipment manufacturers
- EV manufacturers instead of battery component suppliers
- HR agencies instead of HR SaaS vendors
-
Build optional RAG evidence
rag/evidence.pybuilds compact evidence packets for candidates using company facts, snippets, matched terms, contradiction terms, component scores, and penalties. -
Optionally use LLM hooks
llm_intent_parser.pycan enrich query parsing whenENABLE_LLM_INTENT_PARSER=trueandOPENROUTER_API_KEYis available.rag/llm_judge.pycan judge ambiguous top candidates using RAG evidence whenENABLE_LLM_RAG_JUDGE=trueandOPENROUTER_API_KEYis available.Both are disabled by default and safely skipped without API keys.
-
Write explainable output
output.pywritesoutputs/results.json. Each result includes rank, company name, website, score, confidence, matched signals, weak signals, penalties, and component scores.
The default run does not require API keys. LLM paths are optional and disabled by default.
Run with the default paths:
python solution.pyEquivalent explicit command:
python solution.py --data data\companies.jsonl --output outputs\results.jsonRun one custom query:
python solution.py --query "Logistics companies in Romania"Run multiple custom queries:
python solution.py --query "Logistics companies in Romania" --query "B2B SaaS HR companies in Europe"Results are written to:
outputs/results.json
Pretty-print the output:
python -m json.tool outputs\results.jsonRun the local dashboard:
streamlit run ui\app.pyThe dashboard lets you:
- run the default query set or custom queries
- apply ready-made configuration presets
- run a fixed benchmark flow across presets
- toggle pipeline settings from the sidebar
- inspect ranked results, confidence, signals, penalties, and component scores
- compare total run time, per-query runtime, average query time, and startup/cache overhead
- review parsed intent and expanded queries
- preview the local company dataset
- inspect cache paths and runtime config
- read this README in a dedicated tab
The benchmark flow runs a small fixed matrix of queries against multiple presets and writes separate files under:
outputs/benchmark/
By default, the benchmark stays local and skips the OpenRouter preset. Enable Include LLM preset in the sidebar if you want the benchmark to include the optional LLM intent parser run.
Useful presets:
Default hybriduses BM25 plus local multilingual embeddings.Fast lexicaluses BM25 only, so it avoids model loading.Multilingual localenables Argos query translation before parsing.Hybrid + HNSWuses BM25 plus embeddings with the optional HNSWLib vector index.LLM assisted OpenRouter freeenables only the optional OpenRouter intent parser using the default free-model router.LLM RAG OpenRouter freeenables both OpenRouter intent parsing and candidate judging. This is slower because every judged candidate is another OpenRouter request.
Configuration is controlled through environment variables in config.py.
You can also place local secrets and overrides in a .env file at the project root. This file is ignored by Git.
Copy-Item .env.example .envExample .env:
OPENROUTER_API_KEY=your_key_here
OPENROUTER_DEFAULT_MODEL=openrouter/free
HF_TOKEN=your_optional_huggingface_token
OPENROUTER_API_KEY is only needed when ENABLE_LLM_INTENT_PARSER or ENABLE_LLM_RAG_JUDGE is enabled. By default, the LLM hooks use openrouter/free. HF_TOKEN is optional and only helps with Hugging Face download limits. Argos Translate does not need an API key.
Common local settings:
$env:ENABLE_NORMALIZED_COMPANY_CACHE="true"
$env:ENABLE_LANGUAGE_DETECTION="true"
$env:ENABLE_QUERY_TRANSLATION="false"
$env:ENABLE_DYNAMIC_TAXONOMY_EXPANSION="true"
$env:ENABLE_BM25_RETRIEVAL="true"
$env:ENABLE_BM25_CACHE="true"
$env:ENABLE_EMBEDDING_RETRIEVAL="true"
$env:ENABLE_VECTOR_INDEX_CACHE="true"
$env:ENABLE_HNSW_INDEX="false"
$env:VECTOR_INDEX_BACKEND="bruteforce"
$env:ENABLE_LLM_INTENT_PARSER="false"
$env:ENABLE_LLM_RAG_JUDGE="false"
$env:LLM_RAG_JUDGE_TOP_K="3"
$env:OPENROUTER_DEFAULT_MODEL="openrouter/free"Disable embeddings for a faster lexical-only run:
$env:ENABLE_EMBEDDING_RETRIEVAL="false"
python solution.pyDisable embedding cache:
$env:ENABLE_EMBEDDING_CACHE="false"
python solution.pyDisable normalized company cache:
$env:ENABLE_NORMALIZED_COMPANY_CACHE="false"
python solution.pyDisable language detection:
$env:ENABLE_LANGUAGE_DETECTION="false"
python solution.pyEnable optional local query translation with Argos Translate:
$env:ENABLE_QUERY_TRANSLATION="true"
$env:QUERY_TRANSLATION_PROVIDER="argos"
python solution.pyArgos Translate is installed through requirements.txt. When translation is enabled, missing Argos language-pair packages are installed automatically when available. The first run for a new language pair may download a package. If translation is unavailable, the pipeline continues with the original query.
Disable BM25 token cache:
$env:ENABLE_BM25_CACHE="false"
python solution.pyDisable dynamic taxonomy expansion:
$env:ENABLE_DYNAMIC_TAXONOMY_EXPANSION="false"
python solution.pyUse a different local embedding model:
$env:EMBEDDING_MODEL_NAME="sentence-transformers/all-MiniLM-L6-v2"
python solution.pyDisable dense vector index cache:
$env:ENABLE_VECTOR_INDEX_CACHE="false"
python solution.pyUse the optional HNSW vector backend:
$env:ENABLE_HNSW_INDEX="true"
$env:VECTOR_INDEX_BACKEND="hnsw"
python solution.pyHNSWLib is installed through requirements.txt. If it is unavailable, the pipeline falls back to brute-force vector search.
Enable optional LLM features only if an OpenRouter API key is set:
$env:OPENROUTER_API_KEY="your_key"
$env:OPENROUTER_DEFAULT_MODEL="openrouter/free"
$env:ENABLE_LLM_INTENT_PARSER="true"
$env:ENABLE_LLM_RAG_JUDGE="false"
python solution.pyFor slower RAG judging, enable it explicitly and keep the top K small:
$env:ENABLE_LLM_RAG_JUDGE="true"
$env:LLM_RAG_JUDGE_TOP_K="3"
python solution.pyRun the test suite:
python -m pytest.
+-- data/
+-- outputs/
+-- tests/
| +-- test_filters.py
| +-- test_bm25_cache.py
| +-- test_company_cache.py
| +-- test_dynamic_taxonomy.py
| +-- test_intent_parser.py
| +-- test_language.py
| +-- test_normalizer.py
| +-- test_pipeline_smoke.py
| +-- test_ranker.py
| +-- test_scorers.py
| +-- test_translation.py
| +-- test_vector_index_cache.py
+-- config.py
+-- company_cache.py
+-- dynamic_taxonomy.py
+-- filters.py
+-- intent_parser.py
+-- language.py
+-- llm_intent_parser.py
+-- loader.py
+-- models.py
+-- normalizer.py
+-- openrouter_client.py
+-- output.py
+-- query_expansion.py
+-- queries.py
+-- ranker.py
+-- rag/
| +-- __init__.py
| +-- evidence.py
| +-- llm_judge.py
+-- rerankers/
| +-- __init__.py
| +-- noop.py
+-- retrieval/
| +-- __init__.py
| +-- bm25.py
| +-- embeddings.py
| +-- vector_index.py
+-- requirements.txt
+-- scorers.py
+-- solution.py
+-- taxonomy.py
+-- translation.py
+-- ui/
| +-- __init__.py
| +-- app.py
+-- WRITEUP.md