Retain dense image-only VDB records#2312
Conversation
Signed-off-by: jioffe502 <jioffe@nvidia.com>
Greptile SummaryThis PR fixes a data-loss bug where image-backed pages with no native text layer were silently discarded during dense LanceDB ingestion, even when the VL embedder produced a valid vector. The fix threads a new
|
| Filename | Overview |
|---|---|
| nemo_retriever/src/nemo_retriever/common/vdb/records.py | Adds _text_from_graph_row, _is_image_backed_row helpers and an image_only branch in _client_record_from_graph_row to admit blank-text, image-backed rows as canonical document_type=image records; sparse path (require_embedding=False) correctly stays closed. |
| nemo_retriever/src/nemo_retriever/common/vdb/lancedb.py | Dense writer now admits canonical image rows with text="" when both document_type and content_metadata.type are "image"; sparse writer tightens the blank-text check to not isinstance(text, str) or not text.strip() but otherwise unchanged. |
| nemo_retriever/src/nemo_retriever/query/evidence.py | Splits projected hits into evidence (answer-ready) and omitted (blank-text) lists; n_docs_seen counts all projected sources; adds visual-only and generic omission thin-spots to coverage; uses resolve_hit_content_type / parse_hit_content_metadata for consistent modality resolution. |
| nemo_retriever/src/nemo_retriever/query/shaping.py | Renames _hit_content_type to public resolve_hit_content_type and reuses it in shape_query_hits; no logic change beyond visibility promotion. |
| nemo_retriever/tests/test_nv_ingest_vdb_operator.py | Adds parametrized tests covering: blank image-only row admission/normalization, non-canonical image payloads (bytes/numpy) fail-closed, stored_image_uri field variants, and ineligible blank row rejection. |
| nemo_retriever/tests/test_lancedb_write_policy.py | Adds write-policy tests: canonical image rows stored with correct LanceDB shape, missing canonical fields cause rejection, invalid embeddings dropped, sparse ingestion rejects image-only and whitespace-only rows. |
| nemo_retriever/tests/test_lancedb_retrieval_where.py | Adds hybrid-retrieval test verifying image-only rows surface through dense fusion but are excluded from FTS hits. |
| nemo_retriever/tests/query/test_evidence.py | New test file for build_evidence_result covering visual-only omission, non-visual blank-text omission, and mixed evidence+omitted coverage reporting. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Graph row\n(text='', _image_b64='...')"] --> B["_client_record_from_graph_row\n(require_embedding=True)"]
B --> C{embedding\npresent?}
C -- No --> DROP1["return None\n(dropped)"]
C -- Yes --> D{"text\nnonblank?"}
D -- Yes --> TEXT["Normal text/table/etc path\ncontent_type from row"]
D -- No --> E{"_is_image_backed_row?\n(string _image_b64\nor stored_image_uri)"}
E -- No --> DROP2["return None\n(dropped)"]
E -- Yes --> IMG["image_only=True\ndocument_type='image'\ncontent_metadata.type='image'\ncontent=''"]
TEXT --> RECORD["Client VDB record\n{document_type, metadata}"]
IMG --> RECORD
RECORD --> F["_create_lancedb_results\n(dense writer)"]
F --> G{"text empty &\nis_canonical_image?"}
G -- "Yes" --> STORE["Store row\ntext='', vector=embedding"]
G -- No --> DROP3["dropped_no_text++"]
STORE --> H["LanceDB table"]
H --> I["Dense/hybrid retrieval\nreturns hit with text=''"]
I --> J["build_evidence_result"]
J --> K["_is_answer_ready_evidence\n(text.strip() is falsy)"]
K -- False --> OMIT["omitted list\ncoverage.thin_spots:\n'visual-only matches omitted'"]
K -- True --> EVID["evidence list"]
%%{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"}}}%%
flowchart TD
A["Graph row\n(text='', _image_b64='...')"] --> B["_client_record_from_graph_row\n(require_embedding=True)"]
B --> C{embedding\npresent?}
C -- No --> DROP1["return None\n(dropped)"]
C -- Yes --> D{"text\nnonblank?"}
D -- Yes --> TEXT["Normal text/table/etc path\ncontent_type from row"]
D -- No --> E{"_is_image_backed_row?\n(string _image_b64\nor stored_image_uri)"}
E -- No --> DROP2["return None\n(dropped)"]
E -- Yes --> IMG["image_only=True\ndocument_type='image'\ncontent_metadata.type='image'\ncontent=''"]
TEXT --> RECORD["Client VDB record\n{document_type, metadata}"]
IMG --> RECORD
RECORD --> F["_create_lancedb_results\n(dense writer)"]
F --> G{"text empty &\nis_canonical_image?"}
G -- "Yes" --> STORE["Store row\ntext='', vector=embedding"]
G -- No --> DROP3["dropped_no_text++"]
STORE --> H["LanceDB table"]
H --> I["Dense/hybrid retrieval\nreturns hit with text=''"]
I --> J["build_evidence_result"]
J --> K["_is_answer_ready_evidence\n(text.strip() is falsy)"]
K -- False --> OMIT["omitted list\ncoverage.thin_spots:\n'visual-only matches omitted'"]
K -- True --> EVID["evidence list"]
Reviews (2): Last reviewed commit: "Merge branch 'main' into codex/dense-ima..." | Re-trigger Greptile
Summary
Retain image-backed page records in dense LanceDB ingestion when the VL embedder produced a valid vector but extracted text is blank.
document_type=imageandcontent_metadata.type=imageRoot cause
The page-level
text_imagepipeline can successfully embed a scanned page with no native text layer. Both the graph-to-VDB adapter and the LanceDB writer previously required truthy text, so they discarded the valid visual vector before it could be retrieved.Impact
On ViDoRe v3 Finance FR, the fix restored 235 pages, including 69 uniquely judged pages. In the controlled comparison, Recall@5 improved from
0.325283to0.327425, Recall@10 from0.426938to0.432171, and nDCG@10 from0.345894to0.350014. A Computer Science control retained full coverage; its two newly eligible blank-text image rows did not enter any query's top 10.Scope
This does not add a LanceDB column, OCR behavior, model-name enforcement, ViDoRe configuration, or a multimodal answer-generation contract. Raw retrieval can still return a visual hit with
text=""; defining how/v1/answerconsumes visual-only hits remains follow-up work.Validation
68 passedin the focused adapter, writer, hybrid retrieval, evidence, and pipeline-helper suite254 passedin the broader VDB/query suite