Source: http://www.poma-ai.com/docs/pipelines/azure-document-intelligence-to-turbopuffer

# The Missing Link Between Azure Document Intelligence and Optimal Retrieval in Turbopuffer

<ByAuthor />

**The short answer:** Azure Document Intelligence's `prebuilt-layout` model recovers reading order and tables better than almost any parser on the market; Turbopuffer ranks whatever you write to a namespace at very large scale. Wired together with a fixed-size splitter, the pair still produces mediocre RAG, because nothing rebuilds the cross-page heading hierarchy Azure discards or keeps Turbopuffer's rows content-free. The missing link is POMA: `PrimeCut().ingest()` consumes the raw analyze result (auto-detected), emits [chunksets](/learn/chunking/chunksets) into a portable `.poma` archive, and `poma.vektoria` writes content-free rows to Turbopuffer while your document content lives on a volume. Retrieval runs Turbopuffer's own query, then `assemble()` reconstructs prompt-ready context.

## What each end of the pipeline actually provides

**Azure Document Intelligence** returns one of two shapes: markdown mode (`outputContentFormat="markdown"`) — one reading-order string with inline HTML tables and `<!-- PageBreak -->` delimiters — or JSON mode's `paragraphs[]`, ordered by span offset with `role` fields for headings. Both de-interleave multi-column layouts server-side. What it never provides: cross-page hierarchy, chunking, or figure bytes. Details: [The Optimal Chunker for Azure Document Intelligence](/optimal-chunker-azure-document-intelligence).

**Turbopuffer** provides namespace-per-tenant storage, an inline write-time schema, native hybrid `ANN`/`SparseKNN`/`BM25` ranking, and server-side RRF fusion via `multi_query()`. What it doesn't provide: any opinion about what a row should contain, or where the actual document content lives — vektoria's contract keeps the namespace holding only `(id, vector, attributes)`. Details: [Turbopuffer Chunking Strategy for RAG](/optimal-chunks-turbopuffer).

## The naive wiring, and where it breaks

The common shortcut — split Azure's `content` string on `<!-- PageBreak -->`, run a fixed-size splitter over each page, embed, `ns.write()` — breaks in a pair-specific way. The `PageHeader`/`PageFooter`/`PageNumber` furniture comments Azure leaves inline survive the split and land inside every row's embedded text. With `full_text_search: true` set on that attribute (the natural choice for hybrid search), Turbopuffer's BM25 ranking now treats repeated boilerplate like `PageHeader="Contoso Ltd. — Confidential"` as a real lexical signal, so a query for an actual defined term surfaces five near-identical header rows before the clause that answers it. Compounding this, Azure never returns cropped figure bytes — any `![figure](...)` reference left in the naive split becomes a dead ref, embedded and written as a real row that nothing can ever retrieve usefully.

## The pipeline, end to end

```bash
pip install azure-ai-documentintelligence turbopuffer
```

```python
import json
import os

from azure.ai.documentintelligence import DocumentIntelligenceClient
from azure.core.credentials import AzureKeyCredential
from poma import PrimeCut
from poma.vektoria import assemble, records_from_archive, Volume
from poma.embeddings import get_embedder
import turbopuffer

# 1. Azure Document Intelligence — your existing call, unchanged.
di = DocumentIntelligenceClient(
    endpoint=os.environ["AZURE_DI_ENDPOINT"],
    credential=AzureKeyCredential(os.environ["AZURE_DI_KEY"]),
)
with open("contract.pdf", "rb") as f:
    poller = di.begin_analyze_document(
        "prebuilt-layout",
        body=f,
        output_content_format="markdown",
    )
analysis = poller.result()
with open("result.azure-di.json", "w") as f:
    json.dump(analysis.as_dict(), f)

# 2. The missing link — raw result JSON in, .poma archive out (auto-detected Azure shape).
client = PrimeCut()  # reads POMA_API_KEY
client.ingest("result.azure-di.json", download_dir="archives", filename="doc.poma")

embedder = get_embedder("local:BAAI/bge-small-en-v1.5")
vol = Volume("s3://your-bucket/poma")
tpuf = turbopuffer.Turbopuffer(api_key=os.environ["TURBOPUFFER_API_KEY"], region="gcp-us-central1")
ns = tpuf.namespace("contracts")

# 3. Content-free ingest — Turbopuffer holds only routing metadata; content lives on the volume.
records = records_from_archive("archives/doc.poma")  # -> list[Record] (id, text, payload)
for r in records:
    vol.write_doc(r.payload["file_id"], {"file_id": r.payload["file_id"],
                                          "chunks": r.payload["chunks"], "text": r.text})
    ns.write(
        upsert_rows=[{
            "id": r.id,
            "vector": embedder.embed([r.text])[0],
            "file_id": r.payload["file_id"],
            "chunkset_index": r.payload["chunkset_index"],
        }],
        distance_metric="cosine_distance",
        schema={"file_id": {"type": "string"}, "chunkset_index": {"type": "int"}},
    )

# 4. Retrieval — Turbopuffer's normal ANN query, then assemble.
qv = embedder.embed(["early termination conditions"])[0]
res = ns.query(rank_by=("vector", "ANN", qv), top_k=10,
               include_attributes=["file_id", "chunkset_index"])
context = assemble(res, volume=vol)  # -> [{"file_id", "content"}, ...]
```

POMA validates the payload shape up front (a mislabeled upload 422s immediately), splits markdown mode on `PageBreak` while dropping furniture comments, reconstructs JSON mode from span-ordered `paragraphs[]`, and counts every figure as offloaded content since Azure never returns figure bytes. To force or suppress detection, set `external_ocr_source` to `"azure_di"` or `"none"`.

## Metadata mapping: POMA fields → Turbopuffer primitives

| POMA field | Turbopuffer primitive | What it enables |
| --- | --- | --- |
| `id` (`chunkset_uuid`) | row `id` | stable identity across re-ingests |
| `to_embed` | `vector` | ANN ranking |
| `file_id` | string attribute, `Eq` filter | scope a query to one document |
| `chunkset_index` | int attribute, requested via `include_attributes` | `assemble()` match key |
| `page`, `depth`, `chunks`, `text` | volume document, not indexed | content-free retrieval, cheatsheet assembly |

## Frequently asked questions

### How do I get Azure Document Intelligence results into Turbopuffer for RAG?

Save the raw analyze result JSON, hand it to `PrimeCut().ingest()` (auto-detected, hierarchy rebuilt into a `.poma` archive), then loop `records_from_archive()` — write content to a volume, upsert vector + `file_id`/`chunkset_index` attributes with `ns.write()`. Retrieve with `ns.query()` plus `assemble()`.

### Why not just split Azure's markdown content string and write it straight into a Turbopuffer namespace?

Azure's `PageHeader`/`PageFooter`/`PageBreak` comments survive a naive split and get embedded into every row's `full_text_search` text, so BM25 surfaces repeated boilerplate ahead of real matches. Overlap also duplicates spans into redundant rows.

### What Turbopuffer attributes should Azure Document Intelligence chunksets carry?

`id`, `file_id`, and `chunkset_index` as typed, filterable attributes, plus the vector. Page, depth, and chunk text live on the volume — the namespace stays content-free per vektoria's contract.

### Do figures from Azure Document Intelligence show up in Turbopuffer retrieval?

No — Azure never returns cropped figure bytes, so there's nothing to index or fetch. POMA counts every figure as offloaded content in `content_metadata`, so the gap is visible in ingest stats rather than a silently missing answer.

### Does retrieval from Turbopuffer need anything special for POMA's assemble() to work?

Yes — request `include_attributes=["file_id", "chunkset_index"]` on the query. Turbopuffer doesn't return attributes by default, and without them `assemble()` gets nothing to match against the volume.

## Related recipes

Same parser, different store: [Azure Document Intelligence → Pinecone](/pipelines/azure-document-intelligence-to-pinecone) · [Azure Document Intelligence → Chroma](/pipelines/azure-document-intelligence-to-chroma) · [Azure Document Intelligence → Vespa](/pipelines/azure-document-intelligence-to-vespa)

Foundations: [The Optimal Chunker for Azure Document Intelligence](/optimal-chunker-azure-document-intelligence) · [Turbopuffer Chunking Strategy for RAG](/optimal-chunks-turbopuffer) · [All pipeline recipes](/pipelines/) · [RAG chunking guide](/guides/rag-chunking/)