Retrieval-Engine Input Specification¶
What the latence-framework pipeline delivers, as a build contract for a separate retrieval-engine team.
Branch this spec was written against: w13-context-enrichment (the branch that carries ChunkRecord.context_header; wave-integration and main do not yet). Written from a dedicated latence-framework-w13 worktree of that branch.
Schema-version constants, read from source at time of writing:
| Constant | Value | Defined in |
|---|---|---|
SCHEMA_VERSION (all record contracts) |
21 | packages/latence-core/src/latence_core/contracts.py:242 |
QUALITY_SCHEMA_VERSION (Quality Report) |
20 | packages/latence-core/src/latence_core/quality.py:123 |
PIPELINE_SCHEMA_VERSION |
2 | packages/latence-core/src/latence_core/pipeline.py:25 |
Every record carries its schema_version field (serialized with the record), and every contract's field defaults to the SAME module-level SCHEMA_VERSION — so a record dumped today stamps 21 everywhere, whatever version first introduced the field it sits beside.
Contracts are usually additive: a new field defaults to None/empty so an older consumer keeps validating (v11 offset_map, v14 induced_labels, v15 context_header, v16 masked_content/pii_spans, v17 guard_types/raw_label are all optional additive fields).
v21 is additive (ADR-0060): PageMap and PageSlice gain origin — "parser" (the Parser segmented the source), "sidecar" (boundaries supplied out of band beside the document and cross-checked on ingest), or "assumed_single_page" (the document carried NO page structure, so page_number: 1 is a convention, not a measurement). A retrieval engine that cites pages must read this: before v21 an assumed page 1 was indistinguishable from a measured one, which is how an entire OCR'd corpus shipped citing page 1. ParserInput also gains page_map_sidecar. origin defaults to "parser", so a v20 record deserialises unchanged and page NUMBERS are unaffected.
v20 is additive (ADR-0057): GraphScope gains HYPEREDGE with the optional GraphHyperedge payload on GraphRecord, CanonicalEntity gains member_link_confidences, and a Graph-Assembly stack that opts in additionally emits graph-hyperedges.parquet (see §KG export). A v19 record deserialises unchanged; a stack that never configures hyperedges emits streams identical to v19 apart from the version number.
v19 is BREAKING for ChunkRecord. page_map (the parent document's whole page map, carried by value) was REPLACED by page_slice (only the page spans overlapping this chunk). ChunkRecord is extra="forbid", so a v18 chunk record carrying page_map is now REJECTED, not ignored — an integrator holding v18 chunk JSONL must re-run Chunk from the Parse checkpoint. See the changelog banner at contracts.py:18-241 for the per-version detail.
1. Overview¶
Per corpus run, the pipeline emits three deliverables as open columnar/graph/JSON files on any fsspec backend — never a live database (ADR-0017):
- A chunk-level RAG corpus — redacted text +
Provenance+Classification+ per-chunk risk markers / induced labels / a KGcontext_header+ (optional) a single denseembeddingvector. Files:records.jsonl+records.parquet. - An evidence-linked knowledge graph — nodes + edges, every edge citing the source mentions/documents that justify it. Files:
graph-nodes.parquet,graph-edges.parquet,graph.ttl,graph.graphml. - A Quality Report — per-Stage metrics, corpus KG statistics, contract-completeness, optional gold-set precision/recall. Files:
quality-report.json+quality-report.md.
Exact output tree (Storage layout; run_dir = runner.py:385-391, _export_dir = runner.py:419-420):
<storage_uri>/_latence/runs/<run_id>/
├── export/
│ ├── records.jsonl # RAG corpus (streaming, full record dump)
│ ├── records.parquet # RAG corpus (flat columnar)
│ ├── graph-nodes.parquet # KG nodes
│ ├── graph-edges.parquet # KG edges
│ ├── graph.ttl # KG as RDF Turtle
│ └── graph.graphml # KG as GraphML (yEd/Gephi/networkx)
├── quality-report.json # machine-readable report (runner.py:1075)
├── quality-report.md # human-readable report (cli.py:112)
├── checkpoints/<stage>.jsonl # run state (not a deliverable)
├── screening/<stage>.jsonl # screening findings sidecar (not a deliverable)
├── quarantine/<stage>.jsonl # withheld records (not a deliverable)
├── manifest.json / wal.log # run state (not a deliverable)
File-name note: the corpus basename defaults to
records(JsonlParquetExport.__init__, config keybasename) and the graph basename tograph(KnowledgeGraphExport.__init__, config keybasename). This document uses the defaultsrecords.*/graph-*. Both exporters write into the sameexport/directory, so they coexist without collision. (This spec's task brief calls themcorpus.jsonl/corpus.parquet; on disk they arerecords.jsonl/records.parquetunless the operator setsbasename: corpus.)
The corpus Export materializes whatever inter-Stage Record stream is fed to the Export sink (runner.py:1427-1434) — on a Parse→Chunk→…→Export pipeline that is the ChunkRecord stream (optionally the RedactionRecord masked variant). EntityMention/RelationMention/DisambiguationRecord are themselves exportable Records but are not part of the default chunk corpus file; the entity/relation layer is delivered to the engine through the knowledge-graph files and the per-chunk context_header join keys (see §3, §5). See §8 for the boundary.
2. Every output artifact¶
All nested contract fields (Provenance, Classification, ContextHeader, Evidence, …) are serialized as sorted-key JSON strings inside Parquet columns to keep the Parquet schema flat (ADR-0006). JSONL rows are the full Pydantic model_dump(mode="json") — every field, nested objects intact.
2.1 records.jsonl — RAG corpus (JSONL)¶
Writer: stages/export.py JsonlParquetExport.export / _batched. One JSON object per line, json.dumps(row, sort_keys=True, ensure_ascii=False), exactly one trailing \n per line. The object is the record's full dump, so for a ChunkRecord it contains every field below (nested objects as JSON objects, not strings):
| Field | Type | Optional? | Meaning (source: contracts.py ChunkRecord / Record) |
|---|---|---|---|
schema_version |
int | no | Contract version (21). |
record_id |
string | no | The chunk's stable id (content-addressed). Primary key. |
provenance |
object | no | Provenance — source lineage + offsets (see 2.9). |
classification |
object | no | Classification — language/category/sensitivity (see 2.10). |
document_record_id |
string | no | Parent DocumentRecord.record_id. Join key. |
index |
int | no | 0-based chunk ordinal within the document. |
content |
string | no (default "") |
Markup-stripped, PII-redacted retrieval text. Byte-unchanged by W13. |
media_type |
string | no (default text/markdown) |
How to interpret content. |
token_count |
int | no | Budgeted token count for the chunk text. |
page_slice |
object|null | yes | THIS chunk's own page spans (PageSlice) — exactly the parent document's PageSpans overlapping [char_start, char_end), in document coordinates — so a sub-chunk offset resolves to its own page. Renamed from page_map in schema v19, which carried the parent document's WHOLE map (on every chunk, then on the document's first chunk only). Since v21 it also carries origin — read it before citing a page: assumed_single_page means the document had no page structure and page_number: 1 is a convention (ADR-0060). |
offset_map |
object|null | yes | OffsetMap: run-length map stripped-content→original-markdown offsets (v11). |
risk_markers |
array |
no (default []) |
Content-Screening flags that survive into the corpus (filterable — exclude flagged chunks). |
induced_labels |
object|null | yes | InducedLabels the schema_induction Stage attached (v14). |
context_header |
object|null | yes | ContextHeader — the KG projected onto the chunk (v15, W13). See 2.11. null when the context_enrichment Stage was absent. |
embedding |
array |
yes | Present only when an Embedder is wired (see 2.3). |
Example line (abridged, chunk with a context header, no embedding):
{"classification":{"category":"contract","language":"en","schema_version":21,"sensitivity":"confidential"},"content":"Acme Corporation entered into a master services agreement with Beta LLC on 3 March 2024.","context_header":{"entities":["Acme Corporation","Beta LLC"],"entity_types":["Organization","Organization"],"kg_node_ids":["b1c2...","d3e4..."],"neighbor_node_ids":["f5a6..."],"neighbor_triples":["Acme Corporation —acquired→ Gamma Inc"],"situating_sentence":null,"model_id":null,"schema_version":21},"document_record_id":"sha256:9a…","index":4,"media_type":"text/markdown","offset_map":{"length":88,"segments":[{"doc_start":512,"local_start":0}],"schema_version":21},"page_slice":{"origin":"sidecar","pages":[{"char_end":640,"char_start":420,"page_number":2,"schema_version":21}],"schema_version":21,"total_chars":1180},"provenance":{"char_end":600,"char_start":512,"document_id":"sha256:9a…","file_name":"msa.pdf","file_size":48210,"file_type":"pdf","page_end":2,"page_start":2,"schema_version":21,"source_system":null,"source_uri":"file:///corpus/msa.pdf"},"record_id":"sha256:1f…","risk_markers":[],"induced_labels":null,"schema_version":21,"token_count":21}
2.2 records.parquet — RAG corpus (Parquet / Arrow)¶
Writer: JsonlParquetExport._parquet_schema / _record_batch (export.py:309-364). Flat schema, streamed as row-groups of 1024 rows. Nested fields become JSON strings.
| Column | Arrow type | Always present? | Meaning |
|---|---|---|---|
record_id |
string |
yes | Chunk id. |
schema_version |
int64 |
yes | Contract version. |
provenance |
string (JSON) |
yes | Provenance, sorted-key JSON. |
classification |
string (JSON) |
yes | Classification, sorted-key JSON. |
content |
string |
yes | Retrieval text (redacted, markup-stripped). |
media_type |
string |
yes | e.g. text/markdown. |
embedding |
list<double> |
only if an Embedder is wired | Dense vector, fixed width (see 2.3). |
context_header |
string (JSON) |
only if context_columns: true |
The chunk's ContextHeader as sorted-key JSON, "" when the row has no header. |
neighbor_entity_ids |
list<string> |
only if context_columns: true |
The header's neighbor_node_ids (query-time KG-expansion join keys), [] when absent. |
Genuinely-optional guarantee: without embedder and without context_columns, the Parquet schema is the first six columns and is byte-identical to a pre-W13 run (export.py:310-333). An all-empty corpus still writes the schema-only Parquet so a reader always gets the column set (export.py:279-282).
The flat Parquet projection deliberately drops
document_record_id,token_count,risk_markers,offset_map,page_slice,induced_labels— those live fully inrecords.jsonl. A retrieval engine that needs them either reads the JSONL or projectsprovenance(which carriesdocument_id). Flag: if the engine needsdocument_record_id/risk_markersas first-class Parquet columns, that is a small additive change to_parquet_schema— the seam exists but is not wired today.
2.3 The embedding column (opt-in dense vector)¶
JsonlParquetExport resolves an Embedder Provider only when the Export config names one under embedder: {"provider": <entry-point>, "config": {...}} (export.py:186-228). Then every row gets an embedding of the Embedder's fixed dimension.
- What is embedded (
_corpus_text,export.py:77-100): the row's corpus text =masked_contentfor aRedactionRecord(the PII-handled variant) elsecontent; W13 prepends the renderedcontext_header(header_text + "\n" + base) to the embedding input only — the storedcontentnever changes. - Reference Embedders / dims (entry points,
latence.providers): embedding.hashing— in-core, deterministic, dependency-free; defaultdimension= 64 (embedding.py:26). The offline reference.embedding.endpoint— served OpenAI-compatible model; dim by config (e.g. 768).embedding.st— sentence-transformers; dim read from the model. Default IBM Granite Embedding r2 (ADR-0045): GPU tier 311m = 768 (declared default), CPU tier 97m = 384 (latence-embedder-st/provider.py).
Absent an Embedder, the corpus still ships full text + Provenance/Classification, so any vector DB can embed later (ADR-0017).
2.4 graph-nodes.parquet — KG nodes¶
Writer: stages/graph_export.py _write_nodes_parquet (graph_export.py:140-177). One row per GraphNode.
| Column | Arrow type | Meaning (contracts.py GraphNode) |
|---|---|---|
node_id |
string |
Deterministic content-addressed id = sha256(corpus_id + canonical-entity id). Node primary key. |
entity_id |
string |
Source CanonicalEntity.record_id. |
type |
string |
Entity type (the record's label; column named type — W12 — to match GraphML type and TTL lg:entityType). |
canonical_name |
string |
Canonical surface string. |
kb_id |
string |
External KB id, "" when unlinked. |
confidence |
float64 |
Node (cluster) confidence 0..1. |
member_mention_ids |
string (JSON array) |
record_ids of every EntityMention behind this node. Join key → mentions. |
source_document_ids |
string (JSON array) |
Distinct source documents this node appears in. |
properties |
string (JSON) |
Open attribute bag (mention_count, document_count, …). |
evidence |
string (JSON) |
Evidence (mention_ids, document_ids, snippet, confidence). |
2.5 graph-edges.parquet — KG edges¶
Writer: _write_edges_parquet (graph_export.py:179-208). One row per GraphEdge.
| Column | Arrow type | Meaning (contracts.py GraphEdge) |
|---|---|---|
edge_id |
string |
Deterministic content-addressed id = sha256(corpus_id + source-relation id). |
relation_id |
string |
Source NormalizedRelation.record_id. |
source_node_id |
string |
Head GraphNode.node_id. Join key → nodes. |
target_node_id |
string |
Tail GraphNode.node_id. Join key → nodes. |
label |
string |
Normalized relation label (edge label). |
confidence |
float64 |
Edge confidence 0..1. |
properties |
string (JSON) |
Open bag (inverted, source_document_ids; inferred/scorer for W2 predicted edges). |
evidence |
string (JSON) |
Per-edge Evidence — the mentions/documents justifying the edge. Every edge carries Evidence (S9 AC). |
Head and tail node are always distinct (no self-loop; contract-enforced).
2.6 graph.ttl — KG as RDF Turtle¶
Writer: _render_ttl (graph_export.py:212-244). Hand-emitted, no rdflib dep. Prefixes:
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix lg: <urn:latence:graph:> .
- Node → subject
<urn:latence:graph:node:NODE_ID>:a lg:Entity ; rdfs:label "…" ; lg:entityType "…" ; [lg:kbId "…"] ; lg:confidence "0.xxxxxx"^^xsd:decimal . - Edge → a direct triple
<node:SRC> <rel:LABEL> <node:TGT> .plus a reified statement<edge:EDGE_ID> a rdf:Statement ; rdf:subject … ; rdf:predicate … ; rdf:object … ; lg:confidence "…"^^xsd:decimal ; lg:evidenceCount N .whereN = len(edge.evidence.mention_ids).
IRIs percent-escape RFC-3987-forbidden chars; literals scrub XML/Turtle-illegal control chars. Base IRI is a non-dereferenceable urn: (self-contained, no network).
2.7 graph.graphml — KG as GraphML¶
Writer: _render_graphml (graph_export.py:279-315). XML, edgedefault="directed". Declared <key>s:
| key id | for | attr.name | attr.type | Source |
|---|---|---|---|---|
label |
node | label |
string | canonical_name |
type |
node | type |
string | node label (entity type) |
kb_id |
node | kb_id |
string | kb_id (omitted when empty) |
confidence |
node | confidence |
double | node confidence |
rel |
edge | label |
string | edge label |
econfidence |
edge | confidence |
double | edge confidence |
evidence |
edge | evidence_count |
int | len(edge.evidence.mention_ids) |
<node id=node_id> and <edge id=edge_id source=source_node_id target=target_node_id> reuse the same ids as the Parquet/TTL, so the three graph formats are the same graph under the same keys.
2.8 quality-report.json (+ .md)¶
Writers: runner.py:1075 (report.to_json() → quality-report.json); cli.py:112 (report.render() → quality-report.md). Contract: quality.py QualityReport (schema_version = 20 — the Quality Report versions INDEPENDENTLY of the record contracts). Top-level fields:
| Field | Type | Present when | Meaning |
|---|---|---|---|
schema_version |
int | always | 20. |
pipeline_name / run_id |
string | always | Run identity. |
document_count |
int | always | Distinct source documents. |
total_duration_seconds |
float | always | Sum of Stage durations. |
stages |
array |
always | Per-Stage in/out counts, duration, from_checkpoint, error_category. |
completeness |
CompletenessCheck | always | records_checked / with_provenance / with_classification / offset_bearing_records / with_aligned_offsets. |
contracts_complete |
bool | always | Completeness pass/fail. |
parse |
ParseQuality|null | Parse Stage present | Page-map drift, parse errors, bytes_after_parse. |
chunk |
ChunkQuality|null | Chunk Stage | chunks_total, tokens_total, offset_preserving, chunks_flagged. |
screening |
ScreeningQuality|null | Screening Stage | intake/content screened, quarantined, flagged, findings. |
entities |
EntityQuality|null | Entity/fused Stage | per_type counts, confidence dist, mentions_offset_resolving. |
relations |
RelationQuality|null | Relation/fused Stage | per_type, confidence, fan-out caps. |
redaction |
RedactionQuality|null | Redaction Stage | per_type / per_action PII counts, documents_with_pii, redaction_disabled_for_sensitive. |
profiling |
ProfilingQuality|null | Profiling Stage | avg readability/density/…, corpus aggregates, coverage maps, salted co-occurrence fingerprints. |
disambiguation |
DisambiguationQuality|null | Disambiguation Stage | mentions_in vs entities_out, merges_by_strategy, linked/unlinked. |
graph |
GraphQuality|null | Graph Assembly Stage | nodes/edges totals, by type/label, evidence_coverage, inferred-edge counts. |
context_enrichment |
ContextEnrichmentQuality|null | context_enrichment Stage (W13) |
chunks_enriched, avg_neighbors_per_chunk, hub_entities_capped. |
drift |
DriftDiagnostics|null | page-map chunks present | Provenance-integrity drift roll-up. |
goldset |
GoldSetEvaluation|null | gold_set_uri supplied |
Per-Stage precision/recall/F1. |
delta |
DeltaChurn|null | incremental delta run | docs ±, entities created/merged/split, edges ±, drift. |
conformance |
ConformanceReport|null | conformance sweep attached | Per-Provider C1–C6 verdict. |
The .md is the same numbers rendered deterministically (QualityReport.render, quality.py:705). Safe-to-share caveat: co-occurrence entity surfaces are replaced by a salted sha256 fingerprint in the report (not irreversible; raw pairs remain only in the exported CorpusFeatures data artifact) — see quality_report_builder.py:101-138.
2.9 Provenance (embedded in every record)¶
contracts.py:245-266, frozen, extra="forbid".
| Field | Type | Optional? | Meaning |
|---|---|---|---|
schema_version |
int | no | 20. |
source_uri |
string (min 1) | no | Storage URI the source was read from. |
file_name |
string (min 1) | no | Source file name. |
file_type |
string (min 1) | no | Lowercased extension, no dot. |
file_size |
int ≥0 | no | Source size in bytes. |
document_id |
string (min 1) | no | Content-addressed source doc id. Join key across the whole run. |
source_system |
string|null | yes | Originating system. |
page_start / page_end |
int ≥0 | null | yes | Source page(s) the record spans. |
char_start / char_end |
int ≥0 | null | yes | Half-open char span in the parent document's assembled markdown. |
2.10 Classification (embedded in every record)¶
contracts.py:267-288, frozen.
| Field | Type | Optional? | Meaning |
|---|---|---|---|
schema_version |
int | no | 20. |
language |
string (min 1) | no | BCP-47-ish tag; und if unknown. |
category |
string|null | yes | e.g. contract. |
sensitivity |
string | no (default unknown) |
public/internal/confidential/… — the ACL/filter key. |
2.11 ContextHeader (W13; on ChunkRecord.context_header)¶
contracts.py:825-890, frozen. Built deterministically (sorted, capped, content-addressed ids) from the run's EntityMention + GraphRecord streams.
| Field | Type | Optional? | Meaning |
|---|---|---|---|
schema_version |
int | no | 20. |
entities |
list |
no (default []) |
The chunk's canonical entity names (sorted, deduped). |
entity_types |
list |
no (default []) |
Entity type per name (same length + order as entities; validator-enforced). |
neighbor_triples |
list |
no (default []) |
Top-k KG-neighbor relations rendered A —label→ B, confidence-sorted and capped (max_neighbors_per_entity, max_triples). |
kg_node_ids |
list |
no (default []) |
GraphNode.node_ids of the chunk's own entities. Join key → nodes. |
neighbor_node_ids |
list |
no (default []) |
GraphNode.node_ids the triples reach (query-time KG-expansion keys; sorted, deduped). Exported as the Parquet column neighbor_entity_ids. |
situating_sentence |
string|null | yes | OPTIONAL LLM Contextual-Retrieval sentence; null in the deterministic reference build. |
model_id |
string|null | yes | LLM that wrote the situating sentence; null otherwise. |
2.12 Other exportable record contracts (reference)¶
These are inter-Record carriers (each carries Provenance + Classification + record_id) that thread the DAG and can be exported, but are delivered to the engine primarily via the KG files:
EntityMention(contracts.py:782):document_record_id,chunk_record_id,index,label,text,confidence; Provenance carries the mention's char + page span in the parent doc.RelationMention(contracts.py:838):document_record_id,label,head_mention_id/tail_mention_id,head_text/tail_text,confidence.CanonicalEntity(contracts.py:1329):canonical_text,entity_type,member_mention_ids,source_document_ids,kb_id,merge_decisions,evidence.NormalizedRelation(contracts.py:1387):source_relation_id,label,head_entity_id/tail_entity_id,inverted,evidence.RedactionRecord(contracts.py:984):masked_content(the PII-handled corpus variant),pii_spans(PIISpan: type/detector/offset/action/placeholder — never the raw value),policy,redaction_disabled_for_sensitive.Evidence(contracts.py:1237):mention_ids,document_ids,snippet,confidence— justification for an inference (distinct fromProvenance, which is source lineage).
3. The join model¶
Content-addressed, stable ids stitch the corpus to the KG to the source. Keys:
| Relationship | From | To | Key |
|---|---|---|---|
| chunk → parent document | ChunkRecord.document_record_id |
DocumentRecord.record_id |
record id |
| chunk / any record → source doc | record.provenance.document_id |
source manifest | content-addressed doc id |
| mention → chunk | EntityMention.chunk_record_id |
ChunkRecord.record_id |
record id |
| mention → document | EntityMention.document_record_id |
DocumentRecord.record_id |
record id |
| canonical entity → mentions | CanonicalEntity.member_mention_ids |
EntityMention.record_id |
record id list |
| node → canonical entity | GraphNode.entity_id |
CanonicalEntity.record_id |
record id |
| node → mentions | GraphNode.member_mention_ids |
EntityMention.record_id |
record id list |
| edge → endpoints | GraphEdge.source_node_id / target_node_id |
GraphNode.node_id |
node id |
| edge → source relation | GraphEdge.relation_id |
NormalizedRelation.record_id |
record id |
| edge → justification | GraphEdge.evidence.mention_ids / document_ids |
EntityMention.record_id / source docs |
id lists |
| chunk → its KG nodes | context_header.kg_node_ids (col context_header) |
GraphNode.node_id |
node id |
| chunk → KG neighbors | context_header.neighbor_node_ids (col neighbor_entity_ids) |
GraphNode.node_id |
node id |
| any record → source offsets | provenance.char_start/char_end, page_start/page_end |
source document | char/page offsets |
The W13 shortcut: context_header.kg_node_ids is the direct chunk→graph bridge, so the engine does not need EntityMention exported to connect a chunk to the KG.
Reconstruction path — chunk → its entities → its KG neighbors → back to source offsets:
records.parquet row (chunk)
└ context_header.kg_node_ids ─────────────► graph-nodes.parquet (node_id) # the chunk's entities
└ graph-edges.parquet where source_node_id ∈ kg_node_ids # expand 1 hop
└ target_node_id ──────────────► graph-nodes.parquet (neighbors) # = neighbor_node_ids
└ node.member_mention_ids / edge.evidence.mention_ids # justification
└ (EntityMention.provenance) char_start/char_end + page # exact source span
└ own provenance.document_id + char_start/char_end + page_start/page_end ──────► source_uri # chunk's own offsets
Every hop uses a content-addressed id, so two runs over the same corpus reproduce the same joins (determinism, §7).
4. Mapping to the retrieval engine's five layers¶
| Layer | Pipeline PROVIDES | Engine BUILDS |
|---|---|---|
| Dense / multi-vector (Qdrant + MUVERA) | Clean, bounded chunk content (+ optional single dense embedding) |
ColBERT token vectors + the MUVERA FDE; the vector index |
| SPLADE (learned sparse) | Chunk content (+ optional context_header prefix) |
SPLADE encoding + sparse index |
| BM25 (lexical) | Chunk content + filterable metadata |
The lexical index |
| KG | Nodes + edges + Evidence (pipeline owns the KG) | Graph expansion / entity-linked retrieval / rerank |
| Advanced metadata | provenance, classification, induced_labels, context_header, redaction status |
Filters, ACLs, rerank features |
Dense / multi-vector (Qdrant + MUVERA). The pipeline delivers clean, redacted, markup-stripped chunk content bounded to the extractor model window (the GLiNER-safe ≤768-token bound, DEFAULT_GLINER_MAX_LEN = 768, providers/perf.py:418), optionally with the W13 context_header folded into the embedding input, and optionally a single dense embedding (configurable model/dim — hashing-64 / e5-384 / endpoint-768). MUVERA (Multi-Vector Retrieval via Fixed Dimensional Encodings, NeurIPS 2024) asymmetrically maps a ColBERT-style set of token vectors into one Fixed-Dimensional Encoding whose inner product approximates the multi-vector (Chamfer) similarity — so multi-vector retrieval reduces to single-vector MIPS on an off-the-shelf ANN index. Design decision: the pipeline ships text (+ optional single dense vector); the engine owns ColBERT token-vector production and the MUVERA FDE. Seam: the exporter's Embedder is opt-in and single-vector today (export.py:186-228, _parquet_schema writes one list<double> column); a multi-vector export embedder would be an additive Embedder Provider + a list<list<double>> column later — the seam is named, not wired. (MUVERA def.: arXiv 2405.19504; Google Research.)
SPLADE (learned sparse). Pipeline delivers chunk content; the engine SPLADE-encodes it. The context_header rendering (_context_header_text, export.py:49-74) can be prepended for context-aware sparse encoding just as it is for dense — the header text is available from the context_header column.
BM25 (lexical). Chunk content for the lexical index; the metadata fields (§4 metadata row) are the filter predicates. Engine builds the index.
KG. graph-nodes.parquet / graph-edges.parquet (+ graph.ttl / graph.graphml) plus per-edge Evidence support graph expansion, entity-linked retrieval (HippoRAG-style), and evidence-grounded rerank. The pipeline owns the KG; the engine consumes it. The per-chunk context_header node-id columns let the engine expand/rerank without re-linking.
Advanced metadata management — enumerated by role:
| Field | Role |
|---|---|
classification.language, classification.category, classification.sensitivity |
FILTER / ACL |
risk_markers (chunk) |
FILTER (exclude flagged) |
provenance.source_system, provenance.file_type |
FILTER |
induced_labels.entity_types/relation_types/pii_types |
FILTER / RERANK |
context_header.entities, context_header.neighbor_triples, situating_sentence |
RERANK / context-aware encode |
context_header.kg_node_ids, context_header.neighbor_node_ids (col neighbor_entity_ids) |
JOIN KEYS (KG expansion) |
record_id, document_record_id, provenance.document_id |
JOIN KEYS |
provenance.char_start/char_end, page_start/page_end, source_uri, file_name |
PROVENANCE (cite/highlight source) |
redaction status (RedactionRecord.pii_spans, redaction_disabled_for_sensitive) |
FILTER / audit |
5. The W13 context-enrichment contribution¶
- The
context_enrichmentStage (optional) projects the already-computed KG back onto each chunk as a compactContextHeader(contracts.py:137-149, ADR-0039). - The Export prepends the rendered header (
Entities: name (type), … | Relations: A —label→ B; …) to the embedding input ONLY —header_text + "\n" + base(export.py:85-100). The storedcontent/masked_contentis byte-unchanged, so retrieval sees cross-chunk connections in the vector while the LLM window stays the clean chunk. - Two opt-in Parquet columns (
context_columns: true) expose the projection without bloating text:context_header(JSON) andneighbor_entity_ids(=neighbor_node_ids, node-id list). These let the engine do KG expansion + rerank straight off the corpus row (_context_columns_for_row,export.py:103-119). - Anti-bloat discipline: compact rendered triples (not neighbor prose), a top-k neighbor + confidence cap (hub-entity guard), and join keys not raw KG text — query-time KG expansion is the retriever's job (ADR-0017); W13 emits the keys.
6. Backend substitution¶
Everything is open columnar / RDF / JSON written through the single fsspec Storage seam (storage.py, ADR-0009). Default allowed schemes (DEFAULT_ALLOWED_SCHEMES, storage.py:28-30): file, memory, s3, gs, gcs, abfs, az. Network-fetch schemes (http/ftp/sftp) are deliberately excluded (anti-SSRF); credentials resolve through fsspec's own chains (the framework stores no secrets). The same URI + artifacts map onto any target:
| Target | Corpus text + dense | Metadata | KG |
|---|---|---|---|
| Qdrant | payload text + named/ dense vector; optional sparse (SPLADE the engine computes) | provenance/classification/risk_markers → payload filters |
separate collection or external graph store; neighbor_entity_ids as payload for expansion |
| Databricks | records.parquet → Delta table + Unity Catalog; embedding col → Vector Search |
Delta columns; sensitivity → UC governance/ACL | graph-nodes/graph-edges Parquet → Delta graph tables |
| Azure | records.parquet → AI Search index (vector + text fields) |
AI Search filterable fields | graph.ttl/edges → Cosmos DB Gremlin |
Nothing in the output is engine-specific: Parquet/JSONL for the corpus, Parquet + TTL + GraphML for the graph, JSON for the report.
7. Guarantees the retrieval engine can rely on¶
- Exact offset round-trip to source. Every record's
Provenancecarrieschar_start/char_end(+page_start/page_end) in the parent document's assembled-markdown coordinates;page_slice+offset_mapon the chunk let a sub-chunk offset resolve to its true original offset and page (OffsetMap,PageOffsetIndex), not a lower bound. - Redaction applied before export. The corpus text is the PII-handled variant — the Embedder embeds
masked_contentwhen present, andPIISpannever stores the raw value (contracts.py:929-945).redaction_disabled_for_sensitiveis a loud flag that an emptypii_spanson a sensitive doc means "control OFF", not "clean" (H-C1 §4). - Deterministic / reproducible. Content-addressed ids (chunk ids,
node_id= sha256(corpus+entity),edge_id= sha256(corpus+relation)); sorted-key JSON; capped/sorted collections; streamed Parquet byte-identical to the buffered path. A seeded run over the same corpus + config is byte-identical. - Evidence-linked edges. Every
GraphEdgecarriesEvidencenaming the source mentions/documents; the Quality Report'sgraph.evidence_coveragereports the fraction of edges so justified (1.0 = all). - Additive / versioned schemas.
schema_versionon every record; new fields default toNone/empty so older readers keep validating. - Atomic publish. Both corpus files and the report are written to temp paths and atomically renamed (
Storage.atomic_writersfor the streamed corpus file set,Storage.write_atomicfor the buffered report), so a reader never sees a torn file — and a failed run sweeps its own temp paths rather than leaving them on the store. On an object store "atomically renamed" means the destination object is written in one atomic, read-after-write-consistent PUT; the underlying copy-then-delete is two operations, so a crash mid-publish can still leave a temp object behind (seeStorage.atomic_writer).
8. Known boundaries — what the pipeline does NOT provide¶
- No multi-vector / SPLADE vectors by default. The pipeline ships text and (optionally) one dense vector; ColBERT token vectors, the MUVERA FDE, and SPLADE sparse encodings are the engine's job. A multi-vector export embedder is a named-but-unwired seam (§4).
- No live query API. Files only (ADR-0017); there is no server, no live graph DB, no query endpoint. The engine loads the files.
- Situating sentence is optional/future.
context_header.situating_sentence/model_idarenullin the deterministic reference build; the LLM Contextual-Retrieval mode is a documented follow-on (ADR-0039). - KG quality depends on extraction config. Nodes/edges are only as rich as the entity/relation extractors and label config; the KG (and thus
context_header) is empty on a Parse→Chunk→Export-only pipeline with no extraction / disambiguation / graph-assembly Stages, andcontext_headerisnullwithout acontext_enrichmentStage. - Default corpus Parquet is chunk-level and lean.
EntityMention/RelationMention/DisambiguationRecordstreams are exportableRecords but are not written intorecords.parquetby default; the entity/relation layer reaches the engine through the KG files + thecontext_headerjoin columns. Promoting mentions to their own export file, or addingdocument_record_id/risk_markersas first-class Parquet columns, is an additive change to the exporter (seam exists).
Fields that could NOT be resolved from the code (flagged, not guessed)¶
corpus.jsonl/corpus.parquetnaming. The task brief names the corpus filescorpus.*; the on-disk default isrecords.jsonl/records.parquet(JsonlParquetExportbasenamedefault ="records"). They becomecorpus.*only if the operator setsbasename: corpus. Documented as the default with the override noted — no field guessed.- "DenseOn 768" default. No single "DenseOn" preset sets a 768-dim default in core. The reference
embedding.hashingdefault dim is 64; the sentence-transformers default is now IBM Granite Embedding r2 — 768 (GPU tier 311m, the declared default) or 384 (CPU tier 97m), per ADR-0045. Stated explicitly rather than asserting a single default. - Whether
EntityMention/RelationMentionget their own export files in a shipped stack. This is pipeline-configuration-dependent (the Export sink materializes whatever stream is wired to it); no fixed mentions/relations file name is guaranteed by core, so none is asserted. Flagged in §1 and §8.