Skip to content

Latest commit

 

History

38 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Query Intent Clarification

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.

Setup

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txt

After dependency changes, refresh the existing environment with:

python -m pip install -r requirements.txt

Data

Place the company dataset at:

data/companies.jsonl

The file should be JSONL, with one company object per line.

How It Works

The pipeline is staged so cheap deterministic logic does most of the work, while expensive or platform-sensitive components remain optional.

  1. Load company records

    loader.py reads data/companies.jsonl, one JSON object per line.

  2. Normalize company data

    normalizer.py converts raw records into Company models from models.py. It flattens NAICS fields, normalizes scalar/list fields, parses numeric values, derives country and region from address or country-code TLD fallback, and builds searchable_text for retrieval.

    company_cache.py caches the normalized company records under .cache/normalized_companies.json using a content hash of the source JSONL. If data/companies.jsonl changes, the cache is rebuilt automatically.

  3. Parse query intent

    language.py detects the query language with langdetect when available and falls back to English if detection is disabled or unavailable.

    translation.py can 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.py converts each query into an Intent model. 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.

  4. Expand the query

    query_expansion.py turns 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.py derives 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.json using a hash of those high-signal fields.

  5. Retrieve candidate companies

    retrieval/bm25.py performs BM25 lexical retrieval over searchable_text.

    The BM25 tokenized corpus is cached under .cache/bm25_corpus.json using a hash of company IDs and searchable text. If normalized company text changes, the BM25 cache is rebuilt automatically.

    retrieval/embeddings.py optionally performs local dense retrieval with sentence-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.py provides 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.

  6. Union retrieval hits

    ranker.py merges BM25 and embedding hits by company_id and 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.

  7. Apply filters

    filters.py applies hard filters only when the query explicitly asks for that field. For example, public software companies filters on is_public, but software companies does not.

    It also adds soft signals for missing or mismatched geography, startup hints, missing NAICS labels, and missing role/offering fields.

  8. Score candidates

    scorers.py computes 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
  9. Apply penalties

    scorers.py subtracts 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
  10. Build optional RAG evidence

    rag/evidence.py builds compact evidence packets for candidates using company facts, snippets, matched terms, contradiction terms, component scores, and penalties.

  11. Optionally use LLM hooks

    llm_intent_parser.py can enrich query parsing when ENABLE_LLM_INTENT_PARSER=true and OPENROUTER_API_KEY is available.

    rag/llm_judge.py can judge ambiguous top candidates using RAG evidence when ENABLE_LLM_RAG_JUDGE=true and OPENROUTER_API_KEY is available.

    Both are disabled by default and safely skipped without API keys.

  12. Write explainable output

    output.py writes outputs/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.

Running

Run with the default paths:

python solution.py

Equivalent explicit command:

python solution.py --data data\companies.jsonl --output outputs\results.json

Run 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.json

Streamlit UI

Run the local dashboard:

streamlit run ui\app.py

The 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 hybrid uses BM25 plus local multilingual embeddings.
  • Fast lexical uses BM25 only, so it avoids model loading.
  • Multilingual local enables Argos query translation before parsing.
  • Hybrid + HNSW uses BM25 plus embeddings with the optional HNSWLib vector index.
  • LLM assisted OpenRouter free enables only the optional OpenRouter intent parser using the default free-model router.
  • LLM RAG OpenRouter free enables both OpenRouter intent parsing and candidate judging. This is slower because every judged candidate is another OpenRouter request.

Configuration

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 .env

Example .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.py

Disable embedding cache:

$env:ENABLE_EMBEDDING_CACHE="false"
python solution.py

Disable normalized company cache:

$env:ENABLE_NORMALIZED_COMPANY_CACHE="false"
python solution.py

Disable language detection:

$env:ENABLE_LANGUAGE_DETECTION="false"
python solution.py

Enable optional local query translation with Argos Translate:

$env:ENABLE_QUERY_TRANSLATION="true"
$env:QUERY_TRANSLATION_PROVIDER="argos"
python solution.py

Argos 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.py

Disable dynamic taxonomy expansion:

$env:ENABLE_DYNAMIC_TAXONOMY_EXPANSION="false"
python solution.py

Use a different local embedding model:

$env:EMBEDDING_MODEL_NAME="sentence-transformers/all-MiniLM-L6-v2"
python solution.py

Disable dense vector index cache:

$env:ENABLE_VECTOR_INDEX_CACHE="false"
python solution.py

Use the optional HNSW vector backend:

$env:ENABLE_HNSW_INDEX="true"
$env:VECTOR_INDEX_BACKEND="hnsw"
python solution.py

HNSWLib 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.py

For 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.py

Testing

Run the test suite:

python -m pytest

Project Structure

.
+-- 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

About

A Python pipeline for ranking and qualifying company profiles against structured and reasoning-heavy business search queries.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages