Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions nemo_retriever/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ retriever ingest /your-example-dir \
--table-name nemo-retriever
```

You do not need to choose a retrieval index mode for the normal workflow. The
default `index_mode=auto` creates a hybrid table (dense
vectors plus BM25/full-text search), and query mode `auto` uses that table
automatically. The explicit `dense`, `hybrid`, and `sparse` modes are advanced
overrides for experiments or specialized deployments.

Chunks land at `./lancedb/nemo-retriever`, which matches the storage settings
used in [Run a recall query](#run-a-recall-query) below. With the
`[local]` extra installed (see setup), defaults point at local-GPU extraction
Expand Down
2 changes: 1 addition & 1 deletion nemo_retriever/docs/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ These options apply to `retriever ingest`, `retriever ingest local`, and
| `--lancedb-uri` | `lancedb` | LanceDB database URI. |
| `--table-name` | `nemo-retriever` | LanceDB table name. Must match query-time storage flags. |
| `--overwrite/--append` | overwrite | Overwrite the table by default; use `--append` to add rows. |
| `--index-mode` | `dense` | Dense vector index by default; `hybrid` also builds BM25/FTS and `sparse` builds an FTS-only table. |
| `--index-mode` | `auto` | Recommended: leave this unset. `auto` creates a hybrid vector + BM25/FTS configuration for new tables and preserves an existing table on append. Use `dense`, `hybrid`, or `sparse` only for explicit experiments or specialized deployments. |
| `--method` | planner default | PDF extraction method such as `pdfium` or `nemotron_parse`. |
| `--extract-text`, `--extract-tables`, `--extract-charts` | planner default | Enable or disable extraction families. |
| `--ocr-version` | planner default | OCR engine version for local extraction. |
Expand Down
9 changes: 9 additions & 0 deletions nemo_retriever/helm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,20 @@ The retriever service picks up the in-cluster ASR endpoint when `nimOperator.aud
| `serviceConfig.agentic.requestTimeoutS` | `1800` | Gateway and MCP timeout for the multi-step agentic retrieval call. |
| `serviceConfig.vectordb.enabled` | `true` | Deploy the LanceDB vectordb Pod. When `true` the chart **requires** a resolvable embed endpoint (refer to [VectorDB and the embed endpoint](#vectordb-and-the-embed-endpoint)); `helm install` / `helm upgrade` fails fast otherwise. |
| `serviceConfig.vectordb.lancedbUri` | `/data/vectordb` | LanceDB on the vectordb Pod's PVC. |
| `serviceConfig.vectordb.indexMode` | `auto` | `auto`, `dense`, or `hybrid`. Fresh `auto` storage creates FTS and uses hybrid retrieval; persistent dense storage remains dense until `hybrid` is requested explicitly. |
| `serviceConfig.vectordb.embedModel` | `nvidia/llama-nemotron-embed-vl-1b-v2` | Passed to vectordb + worker `embed_model_name`. |
| `serviceConfig.vectordb.embedModelProviderPrefix` | `""` | Optional LiteLLM provider prefix prepended to the remote embed model name. |

#### VectorDB and the embed endpoint { #vectordb-and-the-embed-endpoint }

The VectorDB storage default is `indexMode: auto`; most users should leave it
unchanged. A fresh table creates and waits for its FTS index after the first
write, while an existing dense table is left dense. Set
`serviceConfig.vectordb.indexMode=hybrid` only when you explicitly want to
upgrade an existing dense table. Incremental rows remain searchable through
LanceDB's unindexed-tail scan; the service performs incremental FTS maintenance
automatically and reports FTS and maintenance state from `/v1/health`.

The vectordb Pod's `/v1/query` handler embeds the incoming query text
before searching LanceDB. It needs a NIM embedding endpoint to do that,
and rendering the Deployment with an empty `--embed-endpoint` produces a
Expand Down
6 changes: 6 additions & 0 deletions nemo_retriever/helm/templates/deployment-vectordb.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
{{- $embedURL := include "nemo-retriever.nim.endpointURL" (dict "context" . "key" "vlm_embed" "serviceName" .Values.nimOperator.vlm_embed.nimServiceName "configKey" "embedInvokeUrl" "invokePath" "/v1/embeddings") -}}
{{- $localEmbed := include "nemo-retriever.localEmbed.enabled" . | eq "true" -}}
{{- $localModels := .Values.serviceConfig.localModels -}}
{{- $indexMode := .Values.serviceConfig.vectordb.indexMode | default "auto" -}}
{{- if not (has $indexMode (list "auto" "dense" "hybrid")) -}}
{{- fail "serviceConfig.vectordb.indexMode must be one of: auto, dense, hybrid" -}}
{{- end -}}
{{- $agentic := .Values.serviceConfig.agentic -}}
{{- $internalAuth := .Values.serviceConfig.vectordb.internalAuth -}}
{{- /*
Expand Down Expand Up @@ -77,6 +81,8 @@ spec:
- {{ .Values.serviceConfig.vectordb.lancedbUri | quote }}
- --table-name
- {{ .Values.serviceConfig.vectordb.tableName | quote }}
- --index-mode
- {{ $indexMode | quote }}
{{- if $embedURL }}
- --embed-endpoint
- {{ $embedURL | quote }}
Expand Down
3 changes: 3 additions & 0 deletions nemo_retriever/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ serviceConfig:
enabled: true
lancedbUri: "/data/vectordb"
tableName: "nemo_retriever"
# auto creates hybrid storage when fresh and preserves existing physical indexes.
# Use hybrid explicitly to upgrade an existing dense table by adding FTS.
indexMode: "auto"
embedModel: "nvidia/llama-nemotron-embed-vl-1b-v2"
embedModelProviderPrefix: ""
# Optional dedicated gateway/worker-to-VectorDB authentication. When
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def _graph_ingest_command(
dedup_iou_threshold: opts.DedupIouThresholdOption = None,
store_images_uri: opts.StoreImagesUriOption = None,
overwrite: opts.OverwriteOption = True,
index_mode: opts.IndexModeOption = "dense",
index_mode: opts.IndexModeOption = "auto",
ray_address: opts.RayAddressOption = None,
ray_log_to_driver: opts.RayLogToDriverOption = None,
page_elements_invoke_url: opts.PageElementsInvokeUrlOption = None,
Expand Down
5 changes: 3 additions & 2 deletions nemo_retriever/src/nemo_retriever/cli/ingest/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@
typer.Option(
"--index-mode",
help=(
"LanceDB index mode: dense, hybrid, or sparse. Dense is vector-only; hybrid also builds "
"BM25/FTS; sparse skips dense embedding and writes an FTS-only table."
"Recommended: leave unset. Auto creates a hybrid table for new indexes and preserves an existing "
"table on append. Dense, hybrid, and sparse are advanced overrides for experiments or specialized "
"deployments."
),
),
]
Expand Down
4 changes: 2 additions & 2 deletions nemo_retriever/src/nemo_retriever/cli/query/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@
typer.Option(
"--retrieval-mode",
help=(
"Expert LanceDB retrieval mode: auto, dense, hybrid, or sparse. Default auto inspects the table "
"and chooses the supported mode."
"Advanced override: auto, dense, hybrid, or sparse. Leave at auto to inspect the table and use "
"the supported default mode."
),
),
]
Expand Down
4 changes: 2 additions & 2 deletions nemo_retriever/src/nemo_retriever/common/vdb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ For `vdb_op="lancedb"`, **`LanceDB.retrieval`**:

- Opens the table with `lancedb.connect(table_path).open_table(table_name)`.
- For dense retrieval, each query vector uses **`table.search([vector], vector_column_name=..., **search_kwargs)`**, optional **`.where(where_clause)`** (Lance / DataFusion SQL; `metadata` / `source` are stored as JSON strings), then **`.limit(top_k).refine_factor(...).nprobes(...)`**.
- For hybrid retrieval, callers pass `hybrid=True` plus `query_texts` aligned with the vectors. LanceDB uses **`table.search(query_type="hybrid", vector_column_name=..., fts_columns="text").vector(vector).text(query_text)`** before applying the same `where`, limit, refine, probe, and select handling.
- For hybrid retrieval, callers pass `hybrid=True` plus `query_texts` aligned with the vectors. LanceDB uses **`table.search(query_type="hybrid", vector_column_name=..., fts_columns="text").vector(vector).text(query_text)`** before applying the same `where`, limit, refine, probe, and select handling. Product query paths also pass the shared weighted-RRF policy (`candidate_depth=50`, `dense_weight=0.8`, `rrf_k=10`), then truncate the fused ranking to `top_k`. Direct low-level callers opt into that behavior explicitly with `hybrid_fusion=HybridFusionPolicy(...)`.

Notable kwargs: `top_k`, `refine_factor`, `n_probe` / `nprobes`, `where` or `_filter`, `table_path`, `table_name`, `search_kwargs`, `hybrid`, and `query_texts`. `query_texts` is stripped from constructor kwargs and forwarded only for retrieval calls whose effective mode is hybrid.
Notable kwargs: `top_k`, `refine_factor`, `n_probe` / `nprobes`, `where` or `_filter`, `table_path`, `table_name`, `search_kwargs`, `hybrid`, `query_texts`, and `hybrid_fusion`. `query_texts` is stripped from constructor kwargs and forwarded only for retrieval calls whose effective mode is hybrid.

Example of **direct** operator use (you supply vectors):

Expand Down
70 changes: 70 additions & 0 deletions nemo_retriever/src/nemo_retriever/common/vdb/hybrid_fusion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
# All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Typed rank-fusion policy for LanceDB hybrid retrieval."""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass
from typing import Any

import pyarrow as pa
from lancedb.rerankers.base import Reranker


@dataclass(frozen=True)
class HybridFusionPolicy:
"""Candidate depth and weighted-RRF parameters for one hybrid query."""

candidate_depth: int
dense_weight: float
rrf_k: int

def __post_init__(self) -> None:
if self.candidate_depth <= 0:
raise ValueError("candidate_depth must be greater than zero")
if not 0.0 <= self.dense_weight <= 1.0:
raise ValueError("dense_weight must be between zero and one")
if self.rrf_k <= 0:
raise ValueError("rrf_k must be greater than zero")


DEFAULT_HYBRID_FUSION_POLICY = HybridFusionPolicy(candidate_depth=50, dense_weight=0.8, rrf_k=10)


class WeightedRRFReranker(Reranker):
"""Fuse dense and FTS ranks while preferring dense order for score ties."""

def __init__(self, policy: HybridFusionPolicy) -> None:
super().__init__(return_score="relevance")
self.policy = policy

def rerank_hybrid(
self,
query: str,
vector_results: pa.Table,
fts_results: pa.Table,
) -> pa.Table:
del query
vector_ids = vector_results["_rowid"].to_pylist() if len(vector_results) else []
fts_ids = fts_results["_rowid"].to_pylist() if len(fts_results) else []
scores: defaultdict[Any, float] = defaultdict(float)
for weight, row_ids in (
(self.policy.dense_weight, vector_ids),
(1.0 - self.policy.dense_weight, fts_ids),
):
for rank, row_id in enumerate(row_ids, start=1):
scores[row_id] += weight / (self.policy.rrf_k + rank)

combined = self.merge_results(vector_results, fts_results)
row_ids = combined["_rowid"].to_pylist()
combined = combined.append_column(
"_relevance_score",
pa.array([scores[row_id] for row_id in row_ids], type=pa.float64()),
)
combined = combined.append_column("_fusion_order", pa.array(range(len(combined)), type=pa.int64()))
combined = combined.sort_by([("_relevance_score", "descending"), ("_fusion_order", "ascending")])
combined = combined.drop_columns(["_fusion_order"])
return self._keep_relevance_score(combined)
Loading
Loading