Skip to content

feat: add scoped collection and document management APIs - #2378

Open
KyleZheng1284 wants to merge 44 commits into
NVIDIA:mainfrom
KyleZheng1284:collection-management-api
Open

feat: add scoped collection and document management APIs#2378
KyleZheng1284 wants to merge 44 commits into
NVIDIA:mainfrom
KyleZheng1284:collection-management-api

Conversation

@KyleZheng1284

Copy link
Copy Markdown
Collaborator

Description

This PR adds collection and document lifecycle management to the NeMo Retriever service. It provides a logical, scope-isolated API for creating collections, ingesting and replacing documents, tracking asynchronous jobs, querying collection content, and cleaning up resources without exposing physical LanceDB table names or storage locations to clients.

What changed

  • Added scope-isolated APIs for managing collections, documents, ingestion jobs, and retrieval.
  • Added stable document identities, idempotent ingestion, asynchronous status tracking, and retryable lifecycle cleanup.
  • Kept authentication, physical VectorDB identifiers, storage locations, and owned artifacts within the service boundary.
  • Added Python client support with API compatibility handling and legacy SSE fallback.
  • Added deployment support through Docker Compose and Helm, including non-root artifact storage.
  • Added regression tests, API documentation, configuration examples, and operational guidance.

Why

Applications that want to integrate NeMo Retriever (like aiq) need a stable service boundary for managing durable knowledge collections. Previously, consumers had to depend on ingestion-specific behavior or physical VectorDB details.

This change establishes a logical API boundary where:

  • clients work with collection names and stable document IDs;
  • authentication determines the authorized scope;
  • internal table names and storage locations remain service-owned;
  • ingestion attempts can be retried without changing the logical document identity;
  • collection cleanup and expiration remain recoverable after service restarts.

Validation

Focused regression testing was run from commit edfed55da:

35 passed in 26.06s

The focused coverage includes:

  • collection CRUD and pagination;
  • scope authorization and cross-scope isolation;
  • internal VectorDB authentication;
  • idempotent job replay;
  • stable document IDs and attempt-ID tracking;
  • collection and document cleanup behavior;
  • API-version compatibility errors;
  • legacy SSE fallback behavior;
  • unique OpenAPI operation IDs;
  • non-root artifact-directory ownership; and
  • collection-management CI and Compose contracts.

The collection workflow was also exercised end to end with collection creation, asynchronous ingestion, job polling, document listing, retrieval, deletion, cleanup, and persistence across service recreation.

Out of scope

The separate TXT/HTML tokenizer fix is intentionally not included in this PR. The integrated validation environment contained that independent patch, but this PR does not modify the tokenizer implementation.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@KyleZheng1284
KyleZheng1284 requested review from a team as code owners July 17, 2026 20:23
@KyleZheng1284
KyleZheng1284 requested a review from edknv July 17, 2026 20:23
@KyleZheng1284 KyleZheng1284 changed the title Collection management api feat: add scoped collection and document management APIs Jul 17, 2026
@KyleZheng1284
KyleZheng1284 marked this pull request as draft July 17, 2026 20:24
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces scope-isolated collection and document lifecycle management to NeMo Retriever, adding a logical API boundary where clients work with collection names and stable document IDs while physical LanceDB tables and storage locations remain service-owned. The changes address several issues flagged in earlier review rounds — allow_unscoped_dev now defaults to False, the 401/404 scope-oracle is closed, the idempotency-map memory leak is fixed, reconciliation scans outside _write_lock, and the document-ID mismatch for legacy jobs is corrected.

  • LanceDBCollectionStore (lancedb_collections.py): 1,211-line new class implementing collection/document CRUD, a reader/writer table-user lease mechanism, retryable lifecycle cleanup, and a background reconciliation loop. The lease mechanism and reconciliation pattern are well-designed, but _collection_write_lock is a single global lock that serializes writes across all collections.
  • Gateway router + VectorDB collection endpoints: New proxy routes forward authenticated requests to VectorDB protected by an internal token. Path parameters in the gateway use untyped str where the CollectionName alias should be used.
  • JobTracker / JobCreateRequest: Extended with scope, idempotency, document manifest, and IngestOperation fields to support retryable, idempotent multi-file collection ingestion.

Confidence Score: 3/5

The collection management API is well-structured and most previously identified issues have been addressed, but the global _collection_write_lock will serialize all document writes across all collections, limiting throughput of the primary use case at scale.

The global write-serialization bottleneck in LanceDBCollectionStore means only one document can be written at a time across the entire service, directly undermining the multi-collection API. The other findings are lower-stakes and would not block a controlled rollout, but the write-lock issue warrants a fix before production-scale usage.

Files Needing Attention: lancedb_collections.py — the _collection_write_lock design deserves a second pass. routers/collections.py and client.py have minor type-safety gaps worth closing.

Important Files Changed

Filename Overview
nemo_retriever/src/nemo_retriever/common/vdb/lancedb_collections.py New 1211-line LanceDB-backed catalog for collection/document lifecycle. Key concern: _collection_write_lock is a global lock that serializes all collection writes regardless of target collection. Reader/writer lease mechanism is sound; reconciliation scan pattern is correct.
nemo_retriever/src/nemo_retriever/service/routers/collections.py New gateway proxy router forwarding collection/document management requests to VectorDB. Path parameters typed as str instead of the validated CollectionName alias, missing early validation at the gateway boundary.
nemo_retriever/src/nemo_retriever/service/vectordb_app.py Major refactor: VDB operations moved behind a contract-based VDB interface; collection CRUD endpoints and metrics added. Health endpoint now returns 503 on transient backend exceptions, a behavioral change from the always-200 old implementation.
nemo_retriever/src/nemo_retriever/service/auth.py Added ScopeAuthorizer with multi-token/scope file support; fixed the previously reported 401/404 oracle. Internal token path added for worker/VectorDB authentication.
nemo_retriever/src/nemo_retriever/service/services/job_tracker.py Added scope, idempotency map, document manifest, and register_document_idempotent; idempotency-key ownership check in _drop_job_locked addresses the previously reported memory leak.
nemo_retriever/src/nemo_retriever/service/client.py Added collection/document lifecycle methods and _scope header support. Most async methods now have explicit typed signatures, but aupdate_collection still uses **changes: Any.
nemo_retriever/src/nemo_retriever/common/schemas/collections.py New shared wire models. CollectionName and DocumentId use a strict pattern addressing the previously reported SQL-injection concern. Models are well-structured.
nemo_retriever/src/nemo_retriever/common/schemas/requests.py Added manifest, idempotency, and collection fields to JobCreateRequest. _validate_job_contract enforces operation-specific constraints correctly. collection_name now uses the validated CollectionName type.
nemo_retriever/src/nemo_retriever/service/config.py Added scope/token/internal-VDB-token configuration; allow_unscoped_dev default is now False (fail-closed). Secrets loaded from environment variables and secret files correctly.
nemo_retriever/src/nemo_retriever/service/routers/ingest.py Extended with collection-aware document handling; _resolve_stable_document_id correctly uses attempt_id for legacy jobs. _prepare_job_work_item consolidates shared upload logic cleanly.

Reviews (27): Last reviewed commit: "fix: reconcile agentic queries with VDB ..." | Re-trigger Greptile

Comment thread nemo_retriever/src/nemo_retriever/service/routers/ingest.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/requests.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/service/client.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/service/config.py Outdated
@KyleZheng1284
KyleZheng1284 marked this pull request as ready for review July 20, 2026 17:36
Comment thread nemo_retriever/src/nemo_retriever/service/vectordb_app.py Outdated
@jperez999 jperez999 linked an issue Jul 20, 2026 that may be closed by this pull request
@KyleZheng1284
KyleZheng1284 force-pushed the collection-management-api branch from d2a7419 to 3833c68 Compare July 20, 2026 22:46
@KyleZheng1284

Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment thread nemo_retriever/src/nemo_retriever/service/vectordb_app.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/service/routers/ingest.py Outdated

@jperez999 jperez999 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lets do this in multiple passes

Comment thread nemo_retriever/dev/compose/secrets/artifact-storage-options.json.example Outdated
Comment thread nemo_retriever/dev/compose/secrets/internal-vdb-token.example Outdated
Comment thread nemo_retriever/dev/compose/secrets/scope-tokens.json.example Outdated
Comment thread nemo_retriever/dev/compose/collection-management.compose.yaml Outdated
Comment thread nemo_retriever/dev/compose/collection-management.service.yaml Outdated
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/collections.py
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/collections.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/requests.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/common/vdb/records.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/common/vdb/records.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/service/auth.py
Comment thread nemo_retriever/src/nemo_retriever/service/vectordb_app.py Outdated
Comment thread nemo_retriever/src/nemo_retriever/service/vectordb_app.py Outdated
KyleZheng1284 and others added 14 commits July 23, 2026 04:59
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Comment thread nemo_retriever/dev/compose/service-mode.compose.yaml
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/collections.py
Comment thread nemo_retriever/src/nemo_retriever/service/app.py
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/collections.py
Comment thread nemo_retriever/src/nemo_retriever/service/routers/ingest.py
Comment thread nemo_retriever/src/nemo_retriever/service/routers/ingest.py
Comment thread nemo_retriever/src/nemo_retriever/service/routers/ingest.py
The gateway-forwarded upload path has been unreachable since work moved to
pull scheduling, so its X-Gateway-* headers, spec recovery and worker-side
fallbacks are removed rather than left as untested branches.

With a single admission path remaining, the five loose collection fields on
WorkItem collapse into one DocumentWriteContext that crosses the broker
intact. This replaces the getattr defaults that were silently absorbing a
missing context across the process boundary, and lets the executor take one
argument where it previously reconstructed the same values from six.

IngestOperation becomes a str enum so backends can compare by identity
instead of by string. CollectionWriteContext coerces in __post_init__
because it is a frozen dataclass, not a Pydantic model, and would otherwise
hold a raw wire string that silently failed every identity comparison.

The wire contract is unchanged: the enum still parses and serialises as
"append"/"replace", and the job idempotency fingerprint is byte-identical.

Also drops the /page broker payload, whose source_document_id and
page_number were written but never read and were discarded on
reconstruction because RichModel ignores unknown keys.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Every collection, document and job operation carried a near-identical
synchronous and asynchronous copy. Keeping two copies in step is how
query() and aquery() drifted to different top_k defaults without any test
noticing, and nine of the eleven async methods had no coverage at all.

Each operation now has a single async implementation, with the synchronous
method as a thin facade over it. The facade hands the coroutine to a worker
thread when a loop is already running, so synchronous callers inside async
code keep working exactly as they did under the blocking httpx.Client.

The top_k asymmetry is deliberately preserved and pinned by a signature
test: query() requires it, aquery() defaults to 10, and both are released
API. Parity tests now cover all nine previously untested async methods.

_arequest keeps building its client inside the coroutine, since the facade
may run it on another thread's loop; a comment records that constraint so a
future connection-pooling change does not break the bridge.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Comment thread nemo_retriever/src/nemo_retriever/common/vdb/lancedb_collections.py Outdated
JobTracker.get_idempotent_job and ScopeAuthorizer.credentials_required had
no production callers. Idempotent replay and conflict rejection already run
inside register_job, so the tracker test now drives them through the path
the routers actually use.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
The delete path and the expiry sweeper each open the deletion phase with a
byte-identical row update; both now call _mark_collection_deleting_locked so
the durable phase transition is defined once.

_table_capabilities and _resolve_effective_retrieval_mode also re-checked
table existence that their only caller already establishes under the write
lock, so each query repeated two list_tables round trips. Dropping the
redundant probes lets _table_capabilities return a non-optional result.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Comment thread nemo_retriever/src/nemo_retriever/common/schemas/requests.py Outdated

@jperez999 jperez999 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks good to me, aside from the greptile comments that have not been resolved. I had a few questions about the abstract_methods decorator not being used for the new function in adt_vdb. We should probably require those, so everything is on the new setup.

Comment thread nemo_retriever/src/nemo_retriever/common/vdb/adt_vdb.py
Comment thread nemo_retriever/src/nemo_retriever/service/routers/ingest.py
Comment thread nemo_retriever/src/nemo_retriever/service/services/job_tracker.py Outdated
@KyleZheng1284
KyleZheng1284 force-pushed the collection-management-api branch from e6e6914 to e2e3009 Compare August 5, 2026 22:54
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Collection and document operations were optional capabilities that raised
UnsupportedVDBOperation from default implementations, which let a backend
claim the collection API while silently supporting none of it. They are now
abstract, so an incomplete backend fails at construction instead of at the
first request.

UnsupportedVDBOperation itself remains for genuinely unsupported retrieval
modes; reconcile_collections() and health() stay optional with safe defaults.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Scope enforcement now defaults to on, so fixtures that build a ServiceConfig
directly must request allow_unscoped_dev explicitly rather than inheriting a
permissive default.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
create_job() validated the target collection and the replacement document
with two byte-identical httpx blocks, each followed by its own copy of the
upstream response relay, and built the idempotency fingerprint inline.

Three private helpers now cover those concerns: _vectordb_get() for a scoped
read, _proxied_response() for the verbatim relay, and
_job_idempotency_fingerprint() for the explicit field allowlist. The relay
helper also replaces the identical block in the answer-generation route.

Behavior is unchanged: same scope and internal-auth headers, same 30s
timeout, same 502 details and exception chaining, and non-200 upstream
responses are still relayed verbatim rather than reshaped into a gateway
error envelope.

Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
Signed-off-by: Kyle Zheng <126034466+KyleZheng1284@users.noreply.github.com>
@KyleZheng1284
KyleZheng1284 force-pushed the collection-management-api branch from e2e3009 to f3a0b41 Compare August 5, 2026 23:14
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.

[FEA]: Enable VDB collection management via Python API

3 participants