Skip to content

feat(query_understanding): R1 entity-expand + R3 IDF-clip modules (#291 PR-1) - #368

Merged
robotrocketscience merged 1 commit into
mainfrom
feat/issue-291-pr1-query-understanding
May 3, 2026
Merged

robotrocketscience merged 1 commit into
mainfrom
feat/issue-291-pr1-query-understanding

Conversation

@yoshi280

@yoshi280 yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator

First of four atomic PRs implementing the ratified scope of #291 (R1 + R3 query-understanding stack).

What

New aelfrice.query_understanding package with two pure transforms and a per-store quantile helper:

  • expand_with_capitalised_entities(raw_query, base_terms, *, qf_multiplier=2) — R1: detect capitalised tokens, lowercase, append qf_multiplier copies. Pure regex; sub-microsecond.
  • compute_idf_quantile_thresholds(idf, low_q=0.25, high_q=0.75) — derive per-store low/high IDF cutoffs from a BM25Index.idf vector. Replaces the synthetic-tuned (1.5, 2.5) constants from the lab R3.5 audit, which are inert at live IDF medians of 7.5–9.5 (R5 prereq survey, Rebuild redesign: query understanding (entity extraction, intent, term-weighting) #291 body).
  • clip_with_quantile_thresholds(terms, vocabulary, idf, low, high, *, boost_qf=2) — R3: drop terms with IDF strictly below low, duplicate (boost_qf copies) terms strictly above high, mid-band emits once.

24 unit tests covering both transforms, edge cases (empty vectors, OOV terms, boundary IDF, invalid args), and one R1→R3 composition smoke test.

What this PR does not change

No call site is touched. context_rebuilder.py, retrieval.py, bm25.py, and store.py are unchanged. The integration behind query_strategy = "legacy-bm25" lands in PR-2 (#291 rollout step 2).

Verification

  • uv run pytest tests/test_query_understanding.py -q → 24 passed.
  • uv run pytest tests/ --ignore=tests/regression --ignore=tests/bench_gate --ignore=tests/e2e -q → 2067 passed, 14 skipped (no regression).
  • Discretion grep over git diff github/main...HEAD: clean.
  • Commit SSH-signed.

Why per-store quantiles, not synthetic constants

The lab R5 survey (see #291 body, "Calibration evidence") found live store IDF medians at 7.5–9.5 vs the synthetic 2.85. The R3.5 tuned constants (1.5, 2.5) sit far below the live distribution; the boost rule would fire on essentially every term, which is operationally a noop. Per-store quantiles derived from the live IDF distribution are the architectural fix and are the only design that survives the synthetic→live shape divergence.

Closes / refs

Test plan

  • Unit tests pass locally on Python 3.13.
  • CI staging-gate green.
  • CodeQL / vulture / deptry / typos green.

Summary by Sourcery

Introduce a new query_understanding package providing R1 entity expansion and R3 IDF clipping building blocks, along with tests, without wiring them into existing query pipelines yet.

New Features:

  • Add an R1 entity-expansion helper that detects capitalised tokens in raw queries, lowercases them, and appends boosted copies to the BM25 term list.
  • Add an R3 IDF clipping helper that derives per-store low/high IDF thresholds from BM25 IDF quantiles and rewrites term lists by dropping low-IDF and boosting high-IDF terms.

Tests:

  • Add unit test coverage for entity expansion, IDF quantile threshold computation, IDF-based clipping behavior, error handling, and an end-to-end R1→R3 composition smoke test.

Summary by CodeRabbit

New Features

  • Introduced query understanding stack with entity expansion capabilities that recognize and process capitalized tokens in search queries.
  • Added IDF-based term filtering and boosting mechanisms to optimize search term quality and relevance.

Tests

  • Added comprehensive test suite validating query understanding functionality and edge cases.

@sourcery-ai

sourcery-ai Bot commented May 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a new aelfrice.query_understanding package implementing the R1 entity-expansion and R3 IDF-clip query rewriting transforms, plus per-store IDF quantile threshold computation, and wires them into a public API with dedicated unit tests; no existing call sites or BM25 plumbing are changed yet.

Class diagram for new query_understanding package functions and constants

classDiagram
    class entity_expand {
        <<module>>
        +int DEFAULT_QF_MULTIPLIER
        +list~str~ expand_with_capitalised_entities(raw_query: str, base_terms: list~str~, qf_multiplier: int)
    }

    class idf_clip {
        <<module>>
        +float DEFAULT_LOW_QUANTILE
        +float DEFAULT_HIGH_QUANTILE
        +int DEFAULT_BOOST_QF
        +tuple~float,float~ compute_idf_quantile_thresholds(idf: np.ndarray, low_quantile: float, high_quantile: float)
        +list~str~ clip_with_quantile_thresholds(terms: list~str~, vocabulary: dict~str,int~, idf: np.ndarray, low_threshold: float, high_threshold: float, boost_qf: int)
    }

    class query_understanding_package {
        <<package API>>
        +int DEFAULT_QF_MULTIPLIER
        +int DEFAULT_BOOST_QF
        +float DEFAULT_LOW_QUANTILE
        +float DEFAULT_HIGH_QUANTILE
        +list~str~ expand_with_capitalised_entities(raw_query: str, base_terms: list~str~, qf_multiplier: int)
        +tuple~float,float~ compute_idf_quantile_thresholds(idf: np.ndarray, low_quantile: float, high_quantile: float)
        +list~str~ clip_with_quantile_thresholds(terms: list~str~, vocabulary: dict~str,int~, idf: np.ndarray, low_threshold: float, high_threshold: float, boost_qf: int)
    }

    query_understanding_package ..> entity_expand : reexports
    query_understanding_package ..> idf_clip : reexports
Loading

Flow diagram for R1 entity expansion and R3 IDF clip pipeline

flowchart TD
    subgraph Store_side[BeliefStore / BM25Index side]
        idf_vec["BM25Index.idf (np.ndarray)"]
        compute_thresh["compute_idf_quantile_thresholds(idf, low_quantile, high_quantile)"]
        low_t["low_threshold: float"]
        high_t["high_threshold: float"]

        idf_vec --> compute_thresh
        compute_thresh --> low_t
        compute_thresh --> high_t
    end

    subgraph Query_side[Per-query rewriting side]
        raw_q["raw_query: str"]
        base_terms["base_terms: list[str]"]
        entity_expand["expand_with_capitalised_entities(raw_query, base_terms, qf_multiplier)"]
        expanded_terms["terms: list[str]"]

        raw_q --> entity_expand
        base_terms --> entity_expand
        entity_expand --> expanded_terms

        vocab["vocabulary: dict[str,int]"]
        idf_vec2["idf: np.ndarray"]
        idf_vec2 --- idf_vec
        clip["clip_with_quantile_thresholds(terms, vocabulary, idf, low_threshold, high_threshold, boost_qf)"]
        final_terms["final_terms: list[str]"]

        expanded_terms --> clip
        vocab --> clip
        idf_vec2 --> clip
        low_t --> clip
        high_t --> clip
        clip --> final_terms
    end
Loading

File-Level Changes

Change Details Files
Add R1 entity-expansion transform over raw queries that boosts capitalised entities via duplicated lowercased tokens.
  • Implement regex-based capitalised token detection with a minimum two-character rule to avoid matching pronoun 'I' while handling CamelCase and short names.
  • Provide expand_with_capitalised_entities(raw_query, base_terms, qf_multiplier) that returns a new list combining base_terms with appended boosted entity tokens, validating qf_multiplier >= 1 and not mutating inputs.
  • Expose DEFAULT_QF_MULTIPLIER and expand_with_capitalised_entities from the query_understanding package API.
src/aelfrice/query_understanding/entity_expand.py
src/aelfrice/query_understanding/__init__.py
tests/test_query_understanding.py
Add R3 IDF-clip transform parameterised by per-store IDF quantile thresholds to drop very common terms and boost very rare terms.
  • Implement compute_idf_quantile_thresholds(idf, low_quantile, high_quantile) using numpy.quantile with validation of quantile bounds and a safe (0.0, 0.0) result for empty IDF vectors.
  • Implement clip_with_quantile_thresholds(terms, vocabulary, idf, low_threshold, high_threshold, boost_qf) that drops low-IDF terms, emits boost_qf copies for high-IDF terms, preserves mid-band terms once, and passes through OOV terms, validating boost_qf >= 1.
  • Define DEFAULT_LOW_QUANTILE, DEFAULT_HIGH_QUANTILE, and DEFAULT_BOOST_QF constants and export them via the package init for future configuration and caching on BeliefStore.
src/aelfrice/query_understanding/idf_clip.py
src/aelfrice/query_understanding/__init__.py
tests/test_query_understanding.py
Add focused unit test coverage for the new query understanding transforms and their composition.
  • Cover R1 expansion behavior including match ordering, qf_multiplier edge cases, absence of capitalised tokens, empty queries, pronoun and CamelCase handling, and input immutability.
  • Cover quantile threshold computation for typical arrays, defaults, empty and single-element vectors, and invalid quantile parameter combinations.
  • Cover IDF-clip behavior for drop/keep/boost logic, OOV passthrough, boost_qf validation, empty-term handling, repeated low-IDF terms, and an R1-then-R3 composition smoke test ensuring pipeline semantics match documentation.
tests/test_query_understanding.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A new query_understanding package is added with entity-expansion (R1) and IDF-quantile-clipping (R3) query rewriters. Entity expansion scans for capitalized tokens and appends lowercased copies; IDF clipping filters or boosts terms based on quantile thresholds. Package-level exports and comprehensive tests are included.

Changes

Query Understanding Stack

Layer / File(s) Summary
Constants & Core Algorithms
src/aelfrice/query_understanding/entity_expand.py, src/aelfrice/query_understanding/idf_clip.py
R1 expand_with_capitalised_entities detects capitalized tokens via regex and appends lowercased duplicates using configurable multiplier. R3 compute_idf_quantile_thresholds validates and computes low/high IDF thresholds; clip_with_quantile_thresholds filters or boosts terms based on IDF band membership.
Package Assembly
src/aelfrice/query_understanding/__init__.py
Module initializer re-exports all constants and functions from entity-expand and idf-clip modules via __all__ for public API consumption.
Test Coverage
tests/test_query_understanding.py
Validates R1 expansion (capitalization detection, duplication, parameter validation, non-mutation); R3 threshold computation (quantile defaults, edge cases, validation); R3 clipping (filtering, boosting, boundary behavior, OOV handling); end-to-end R1→R3 pipeline.

Sequence Diagram

sequenceDiagram
    participant Client
    participant R1 as Entity Expansion<br/>(R1)
    participant R3 as IDF Clipping<br/>(R3)
    participant Result as Rewritten Query

    Client->>R1: raw_query + base_terms<br/>(with qf_multiplier)
    R1->>R1: Scan for capitalized tokens
    R1->>R1: Append lowercased copies
    R1->>R3: expanded_terms
    R3->>R3: Look up IDF for each term
    R3->>R3: Compare vs quantile thresholds
    alt IDF < low_threshold
        R3->>R3: Drop term
    else low_threshold ≤ IDF ≤ high_threshold
        R3->>R3: Keep once
    else IDF > high_threshold
        R3->>R3: Emit boost_qf copies
    end
    R3->>Result: Final clipped/boosted terms
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: introducing R1 entity-expand and R3 IDF-clip modules to the query_understanding package.
Description check ✅ Passed The description fully addresses required template sections: comprehensive summary, linked issue (#291), feat type selected, verification checklist completed, detailed test plan, and reviewer notes provided.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-291-pr1-query-understanding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@yoshi280 yoshi280 added the attn:review Needs review (PR open, awaiting reviewer) label May 3, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In clip_with_quantile_thresholds, consider validating that each vocabulary index is within the bounds of the idf array (or that idf.size matches the vocabulary’s max index) to avoid unexpected IndexError when the BM25 index and vocabulary get out of sync.
  • For compute_idf_quantile_thresholds, you may want to explicitly document or guard against the case where idf contains NaNs/inf values, since np.quantile can propagate them and silently produce thresholds that make the clip effectively degenerate.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `clip_with_quantile_thresholds`, consider validating that each vocabulary index is within the bounds of the `idf` array (or that `idf.size` matches the vocabulary’s max index) to avoid unexpected `IndexError` when the BM25 index and vocabulary get out of sync.
- For `compute_idf_quantile_thresholds`, you may want to explicitly document or guard against the case where `idf` contains NaNs/inf values, since `np.quantile` can propagate them and silently produce thresholds that make the clip effectively degenerate.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@github-actions github-actions Bot added the attn:merge-conflict PR branch needs rebase label May 3, 2026
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown

This PR is now behind main. Rebase locally so your commit signatures stay intact:

git fetch origin && git checkout 'feat/issue-291-pr1-query-understanding' && git rebase origin/main
# resolve conflicts if any, then
git push --force-with-lease

Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the required_signatures rule on main then blocks the merge. See #341.

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Kulili:2026-05-03T03:53:46Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[claim:review:Gylf:2026-05-03T03:53:54Z]

@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Gylf:2026-05-03T03:53:59Z]

… PR-1)

Adds aelfrice.query_understanding package with the two
deterministic query-rewrite transforms ratified for #291:

- R1 capitalised-token expansion (boost via integer-qf duplication)
- R3 IDF clip with per-store quantile-derived thresholds

Per-store quantile threshold computation lives alongside the clip
itself so callers can cache the (low, high) tuple on a BeliefStore
once per BM25 index build.

PR-1 lands the modules + 24 unit tests; no call-site change. The
context_rebuilder integration lands in PR-2 behind the
query_strategy = "legacy-bm25" setting (#291 rollout step 2).
@robotrocketscience
robotrocketscience force-pushed the feat/issue-291-pr1-query-understanding branch from 0181b13 to f52b4d3 Compare May 3, 2026 03:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/aelfrice/query_understanding/idf_clip.py (1)

73-116: ⚡ Quick win

Add a low_threshold <= high_threshold guard to prevent silent mis-behaviour on inverted thresholds.

clip_with_quantile_thresholds accepts two independent float parameters; if a caller accidentally passes them in the wrong order, the function silently drops everything that would normally be mid-band and boosts the wrong terms—no error, no diagnostic. compute_idf_quantile_thresholds always returns a correctly ordered pair, but future direct callers (e.g. cache-warmed thresholds stored separately) have no protection.

🛡️ Proposed guard
     if boost_qf < 1:
         raise ValueError(f"boost_qf must be >= 1, got {boost_qf}")
+    if low_threshold > high_threshold:
+        raise ValueError(
+            f"low_threshold ({low_threshold}) must be <= high_threshold ({high_threshold})"
+        )
     out: list[str] = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/aelfrice/query_understanding/idf_clip.py` around lines 73 - 116, Add a
guard in clip_with_quantile_thresholds to detect inverted thresholds and raise a
clear error instead of silently misbehaving: check that low_threshold <=
high_threshold at the start of the function (after validating boost_qf) and
raise a ValueError with a descriptive message if not, referencing the provided
low_threshold and high_threshold values so callers get immediate feedback; this
change is to the clip_with_quantile_thresholds function and should not affect
compute_idf_quantile_thresholds which already returns ordered pairs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/aelfrice/query_understanding/idf_clip.py`:
- Around line 73-116: Add a guard in clip_with_quantile_thresholds to detect
inverted thresholds and raise a clear error instead of silently misbehaving:
check that low_threshold <= high_threshold at the start of the function (after
validating boost_qf) and raise a ValueError with a descriptive message if not,
referencing the provided low_threshold and high_threshold values so callers get
immediate feedback; this change is to the clip_with_quantile_thresholds function
and should not affect compute_idf_quantile_thresholds which already returns
ordered pairs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 93a87d15-68c3-42be-b3c9-fa74a21ffdff

📥 Commits

Reviewing files that changed from the base of the PR and between 38bcbf0 and f52b4d3.

📒 Files selected for processing (4)
  • src/aelfrice/query_understanding/__init__.py
  • src/aelfrice/query_understanding/entity_expand.py
  • src/aelfrice/query_understanding/idf_clip.py
  • tests/test_query_understanding.py

@robotrocketscience
robotrocketscience merged commit f52b4d3 into main May 3, 2026
24 of 25 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-291-pr1-query-understanding branch May 3, 2026 04:04
@yoshi280

yoshi280 commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

[release:review:Kulili:2026-05-03T04:04:23Z]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants