Skip to content

Add query filters for service-backed retrieval#2258

Open
jioffe502 wants to merge 3 commits into
NVIDIA:mainfrom
jioffe502:codex/root-query-filters
Open

Add query filters for service-backed retrieval#2258
jioffe502 wants to merge 3 commits into
NVIDIA:mainfrom
jioffe502:codex/root-query-filters

Conversation

@jioffe502

@jioffe502 jioffe502 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add typed root query filters for source id, source, page number, and advanced LanceDB where predicates
  • make service query filtering work end to end: RetrieverServiceClient.query(..., filters=...) -> gateway /v1/query -> VectorDB /v1/query -> LanceDB .where(...)
  • preserve existing local Retriever.query(..., vdb_kwargs={"where": ...}) behavior and root query JSON output shape
  • add notebook-prerequisite coverage for compact sidecar metadata predicates such as metadata LIKE '%"meta_a":"alpha"%'

Scope / Follow-Up

Raw where Contract

  • where is intentionally advanced/trusted-caller pass-through to LanceDB/DataFusion, applied as a read-only pre-filter before limit().
  • Common filtering should use structured source_id, source, and page_number fields.
  • Service deployers should expose raw where only inside the same auth/trust boundary as unfiltered /v1/query access. The public service schema now documents that posture.

Design Lineage

Validation

  • Focused local suite: /localhome/local-jioffe/retriever-skills/nemo_retriever/.venv/bin/python -m pytest nemo_retriever/tests/test_lancedb_retrieval_where.py nemo_retriever/tests/test_query_filters.py nemo_retriever/tests/test_query_workflow_options.py nemo_retriever/tests/test_root_query_cli.py nemo_retriever/tests/test_service_query_client.py nemo_retriever/tests/test_service_vectordb_app.py nemo_retriever/tests/test_retriever_queries.py nemo_retriever/tests/test_root_cli_workflow.py -q (157 passed)
  • Touched-file pre-commit: /localhome/local-jioffe/.local/venvs/pre-commit/bin/pre-commit run --files nemo_retriever/src/nemo_retriever/cli/query/options.py nemo_retriever/src/nemo_retriever/service/query_schema.py nemo_retriever/tests/test_lancedb_retrieval_where.py nemo_retriever/tests/test_service_vectordb_app.py
  • Local Retriever query threading is covered by test_retriever_queries.py::TestQueriesGraphExecution::test_queries_thread_top_k_and_vdb_kwargs and LanceDB predicate execution is covered by test_lancedb_retrieval_where.py, including the compact sidecar metadata predicate.
  • Service client payload is covered by test_service_query_client.py::test_service_client_query_posts_filters_to_v1_query.
  • VectorDB /v1/query route filter application is covered by test_service_vectordb_app.py::test_query_route_accepts_filters_and_applies_where.
  • Root service query JSON shape/filter pass-through is covered by test_root_query_cli.py::test_root_query_service_mode_uses_service_options_and_prints_json.

Live Helm Smoke With Real Embed NIM

Environment:

  • MicroK8s namespace/release: nrl-real-filter-smoke
  • Helm chart: ./nemo_retriever/helm
  • Service image used for smoke: nrl-service:2258-filter-smoke-fullsrc, built from nvcr.io/nvidia/nemo-microservices/nrl-service:26.5.0 with this branch's nemo_retriever/src/nemo_retriever overlaid
  • Real embed NIM: scaled existing nv-ingest/llama-nemotron-embed-vl-1b-v2 deployment from 0 to 1; pod became Ready, using nvcr.io/nim/nvidia/llama-nemotron-embed-vl-1b-v2:1.12.0
  • VectorDB embed endpoint: http://llama-nemotron-embed-vl-1b-v2.nv-ingest.svc.cluster.local:8000/v1/embeddings
  • Direct real embed probe returned 2048-dimensional embeddings from nvidia/llama-nemotron-embed-vl-1b-v2
  • VectorDB seeded through /internal/vectordb/write with two rows whose vectors were generated by the real embed NIM:
    • alpha sidecar metadata chunk, metadata={"meta_a":"alpha","page_number":1}
    • bravo sidecar metadata chunk, metadata={"meta_a":"bravo","page_number":2}

Real embed probe:

curl -fsS -X POST http://127.0.0.1:18000/v1/embeddings \
  -H 'Content-Type: application/json' \
  --data-binary '{"model":"nvidia/llama-nemotron-embed-vl-1b-v2","input":["metadata smoke"],"encoding_format":"float","input_type":"query","truncate":"NONE"}'

Probe summary:

  • model: nvidia/llama-nemotron-embed-vl-1b-v2
  • count: 1
  • dim: 2048

Gateway request:

curl -fsS -X POST http://127.0.0.1:18670/v1/query \
  -H 'Content-Type: application/json' \
  --data-binary '{"query":"metadata smoke","top_k":5,"where":"metadata LIKE '\''%\"meta_a\":\"alpha\"%'\''"}'

Gateway result summary:

  • Control request without where returned 2 hits: alpha and bravo.
  • Filtered request returned 1 hit: alpha sidecar metadata chunk from notebook-alpha.pdf, page 1, with metadata.meta_a == "alpha".

Root CLI service smoke against the same real-NIM Helm gateway:

PYTHONPATH=nemo_retriever/src /localhome/local-jioffe/retriever-skills/nemo_retriever/.venv/bin/python -c 'from nemo_retriever.__main__ import main; main()' \
  query service 'metadata smoke' \
  --service-url http://127.0.0.1:18670 \
  --top-k 5 \
  --where 'metadata LIKE '\''%"meta_a":"alpha"%'\'''

Root CLI result summary:

  • 1 JSON hit, preserving the root query hit shape: source=notebook-alpha.pdf, page_number=1, text="alpha sidecar metadata chunk", modality=text, real-service score 1.435014009475708.

@jioffe502 jioffe502 requested review from a team as code owners June 22, 2026 18:04
@jioffe502 jioffe502 requested a review from jdye64 June 22, 2026 18:04
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR threads typed query filters (source_id, source, page_number, and raw where) end-to-end from the CLI and service client through the gateway and VectorDB /v1/query route down to LanceDB predicate execution. It introduces a new QueryFilterOptions dataclass, a filters.py module that translates those options into LanceDB LIKE predicates with proper LIKE-metacharacter escaping, and corresponding Pydantic fields on the service QueryRequest model.

  • New filters.py: builds composed LanceDB WHERE predicates from structured options; LIKE wildcards (%, _) in source/source_id values are properly escaped via _like_pattern_literal and ESCAPE '\\\\', addressing the previously flagged unescaped-metacharacter risk.
  • Service path: RetrieverServiceClient.query now accepts filters; the VectorDB app applies the combined predicate before limit(); the QueryRequest Pydantic model adds source_id, source, page_number (validated ge=0), and where fields.
  • Agentic guard: filter flags are rejected when --agentic is active; raw where is explicitly documented as a trusted-caller pass-through.

Confidence Score: 5/5

Safe to merge. The filter threading is cleanly layered, LIKE metacharacter escaping is properly applied, and the Pydantic boundary validates page_number. Previously flagged raw-where pass-through is a deliberate trusted-caller design documented in the PR and schema.

The change is a well-scoped additive feature with no breaking API changes, comprehensive unit test coverage (157 passing, live Helm smoke), and correct operator precedence in composed predicates. The only remaining gaps are a missing docstring on the new public dataclass and an undocumented raise from the has_query_filters predicate — neither affects runtime correctness.

nemo_retriever/src/nemo_retriever/query/options.py (missing docstring on QueryFilterOptions) and nemo_retriever/src/nemo_retriever/query/filters.py (surprising raise from has_query_filters).

Important Files Changed

Filename Overview
nemo_retriever/src/nemo_retriever/query/filters.py New module: translates QueryFilterOptions into LanceDB LIKE predicates; LIKE metacharacters are properly escaped via _like_pattern_literal + ESCAPE clause; raw where is pass-through; previously flagged SQL injection and LIKE escape concerns are documented as known trade-offs.
nemo_retriever/src/nemo_retriever/query/options.py Adds QueryFilterOptions frozen dataclass and threads it into QueryRequest and ServiceQueryRequest; class is a public contract but lacks a docstring, violating the docstrings-public-interface rule.
nemo_retriever/src/nemo_retriever/service/query_schema.py Adds source_id, source, page_number (ge=0 validated), and where fields to QueryRequest Pydantic model; backward-compatible additive change with proper Field descriptions.
nemo_retriever/src/nemo_retriever/service/vectordb_app.py Wires filter fields from QueryRequest through build_query_where_clause into VectorDBState.search; raw where predicate passes through with only whitespace stripping (previously flagged); search call ordering (where before limit) is correct.
nemo_retriever/src/nemo_retriever/service/client.py query() gains an optional filters parameter; query_filter_payload merges structured fields into the POST body; backward-compatible (filters defaults to None).
nemo_retriever/src/nemo_retriever/cli/query/app.py Adds --source-id, --source, --page-number, --where CLI options to both _local_command and _service_command; agentic path correctly rejects filters; filter options are cleanly factored via _filter_options helper.
nemo_retriever/src/nemo_retriever/query/workflow.py Adds where field to ResolvedQueryPlan and threads build_query_where_clause result into vdb_kwargs; vdb_kwargs is only set when where is non-None, preserving existing behavior.
nemo_retriever/src/nemo_retriever/cli/query/options.py Adds SourceIdOption, SourceOption, PageNumberOption (min=0), and WhereOption typer type aliases; straightforward and consistent with existing option definitions.
nemo_retriever/src/nemo_retriever/query/service.py One-line change: passes request.filters to client.query; correctly threads the filter options into the service call.
nemo_retriever/tests/test_query_filters.py New test file covering predicate building, LIKE wildcard escaping, pass-through where, has_query_filters, and negative page_number rejection; good coverage of happy and error paths.
nemo_retriever/tests/test_service_vectordb_app.py Adds tests for negative page_number rejection (HTTP 422), where-before-limit call ordering, and end-to-end route filter application; uses object.new to bypass VectorDBState init which may be fragile if internals change.
nemo_retriever/tests/test_service_query_client.py Adds test verifying all filter fields are serialized into the POST body; straightforward and sufficient for the contract being tested.
nemo_retriever/tests/test_root_query_cli.py New test cases cover filter pass-through to vdb_kwargs, output shape preservation, agentic rejection, service help text visibility, and service mode JSON output; comprehensive end-to-end CLI coverage.
nemo_retriever/tests/test_query_workflow_options.py Extends workflow tests to cover filter options threading into vdb_kwargs and service client filter propagation; correctly updates FakeClient signature to match the new filters parameter.
nemo_retriever/tests/test_lancedb_retrieval_where.py Adds a test for compact sidecar metadata LIKE predicate matching; validates LanceDB retrieval layer correctly applies the where clause to filter results.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as CLI / Programmatic API
    participant App as cli/query/app.py
    participant Filters as query/filters.py
    participant SvcClient as service/client.py
    participant Gateway as Gateway /v1/query
    participant VDB as VectorDB /v1/query
    participant LanceDB as LanceDB table

    CLI->>App: query(text, --source-id, --page-number, --where)
    App->>Filters: build_query_where_clause(QueryFilterOptions)
    Note over Filters: Escapes LIKE metacharacters, composes AND clauses

    alt Local mode
        Filters-->>App: "where_clause (str | None)"
        App->>LanceDB: "Retriever.query(vdb_kwargs={where: where_clause})"
        LanceDB-->>App: hits
    else Service mode
        App->>Filters: query_filter_payload(QueryFilterOptions)
        Filters-->>App: "{source_id, source, page_number, where}"
        App->>SvcClient: client.query(text, top_k, filters)
        SvcClient->>Gateway: "POST /v1/query {query, top_k, source_id, ...}"
        Gateway->>VDB: POST /v1/query (forwarded)
        VDB->>Filters: build_query_where_clause(QueryFilterOptions)
        Filters-->>VDB: combined where_clause
        VDB->>LanceDB: table.search(vector).where(where_clause).limit(top_k)
        LanceDB-->>VDB: hits
        VDB-->>Gateway: QueryResponse
        Gateway-->>SvcClient: QueryResponse
        SvcClient-->>App: list[list[dict]]
    end

    App-->>CLI: JSON hits
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CLI as CLI / Programmatic API
    participant App as cli/query/app.py
    participant Filters as query/filters.py
    participant SvcClient as service/client.py
    participant Gateway as Gateway /v1/query
    participant VDB as VectorDB /v1/query
    participant LanceDB as LanceDB table

    CLI->>App: query(text, --source-id, --page-number, --where)
    App->>Filters: build_query_where_clause(QueryFilterOptions)
    Note over Filters: Escapes LIKE metacharacters, composes AND clauses

    alt Local mode
        Filters-->>App: "where_clause (str | None)"
        App->>LanceDB: "Retriever.query(vdb_kwargs={where: where_clause})"
        LanceDB-->>App: hits
    else Service mode
        App->>Filters: query_filter_payload(QueryFilterOptions)
        Filters-->>App: "{source_id, source, page_number, where}"
        App->>SvcClient: client.query(text, top_k, filters)
        SvcClient->>Gateway: "POST /v1/query {query, top_k, source_id, ...}"
        Gateway->>VDB: POST /v1/query (forwarded)
        VDB->>Filters: build_query_where_clause(QueryFilterOptions)
        Filters-->>VDB: combined where_clause
        VDB->>LanceDB: table.search(vector).where(where_clause).limit(top_k)
        LanceDB-->>VDB: hits
        VDB-->>Gateway: QueryResponse
        Gateway-->>SvcClient: QueryResponse
        SvcClient-->>App: list[list[dict]]
    end

    App-->>CLI: JSON hits
Loading

Reviews (11): Last reviewed commit: "Reject unsupported agentic query filters" | Re-trigger Greptile

Comment thread nemo_retriever/src/nemo_retriever/query/filters.py
Comment thread nemo_retriever/src/nemo_retriever/service/vectordb_app.py Outdated
top_k: int = Field(default=10, ge=1, le=1000)
source_id: str | None = None
source: str | None = None
page_number: int | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The page_number field has no lower-bound validation, unlike top_k which uses ge=1, le=1000. A negative or zero page number is almost certainly invalid for a document page and would produce nonsensical LIKE patterns in _page_number_clause. Adding ge=0 makes the contract explicit and catches bad inputs at the API boundary.

Suggested change
page_number: int | None = None
page_number: int | None = Field(default=None, ge=0)
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/service/vectordb_app.py
Line: 57

Comment:
The `page_number` field has no lower-bound validation, unlike `top_k` which uses `ge=1, le=1000`. A negative or zero page number is almost certainly invalid for a document page and would produce nonsensical LIKE patterns in `_page_number_clause`. Adding `ge=0` makes the contract explicit and catches bad inputs at the API boundary.

```suggestion
    page_number: int | None = Field(default=None, ge=0)
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread nemo_retriever/src/nemo_retriever/query/filters.py
@jioffe502 jioffe502 force-pushed the codex/root-query-filters branch 5 times, most recently from ed929cd to d4789d3 Compare June 24, 2026 16:09
@jioffe502 jioffe502 changed the title Add root query source and page filters Add query filters for service-backed retrieval Jun 24, 2026
@jioffe502 jioffe502 force-pushed the codex/root-query-filters branch from 2f851aa to fb2e58c Compare June 25, 2026 21:17
@jioffe502 jioffe502 force-pushed the codex/root-query-filters branch from 2149a51 to 9f4df77 Compare June 30, 2026 20:09
@jioffe502

Copy link
Copy Markdown
Collaborator Author

waiting for @jdye64 to get back to take a look and talk this through

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.

1 participant