Source: http://www.poma-ai.com/docs/pipelines/mistral-ocr-to-weaviate

# The Missing Link Between Mistral OCR and Optimal Retrieval in Weaviate

<ByAuthor />

**The short answer:** Mistral OCR gives you excellent per-page markdown; Weaviate gives you built-in hybrid search — BM25F plus vectors, blended by `alpha` — over filterable properties. Wired together naively — flatten, split, embed — they still produce mediocre RAG, because nothing in between rebuilds the document's hierarchy, and both halves of the hybrid suffer for it. The missing link is POMA: `PrimeCut().ingest()` consumes the raw `/v1/ocr` JSON (auto-detected), emits hierarchy-preserving [chunksets](/learn/chunking/chunksets), and each becomes one Weaviate object with `file_id`/`page`/`depth` properties and your embedding as its vector.

## What each end of the pipeline actually provides

**Mistral OCR** (`/v1/ocr`, `mistral-ocr-latest`) returns `pages[]`, each with an `index` and a `markdown` string — headings, lists, and tables recovered *within* each page, image bytes inline when you set `include_image_base64`. What it doesn't provide: cross-page hierarchy (page 41's `## Termination clauses` has no link to page 3's `# Master Services Agreement`) or retrieval units. Details: [The Optimal Chunker for Mistral OCR](/optimal-chunker-mistral-ocr).

**Weaviate** provides collections with named vectors, built-in hybrid search (BM25F + vector, blended by `alpha`), filterable properties, cross-references between collections, and generative/reranker modules. What it doesn't provide: any opinion about what an object should contain. BM25F scores whatever text you stored; the vector index ranks whatever you embedded — including context-free fragments. Details: [The Optimal Chunks for the Best Retrieval in Weaviate](/optimal-chunks-weaviate).

## The naive wiring, and where it breaks

The common recipe — `"\n".join(p["markdown"] for p in pages)` → `RecursiveCharacterTextSplitter` → embed → `data.insert(...)` — breaks in a pair-specific way:

- **The `page` property dies at the join.** Mistral's `pages[].index` is gone before splitting starts, so the one property Weaviate needs for page filters and page-cited answers holds nothing truthful.
- **Hybrid's keyword half is wasted on fragments.** Weaviate's BM25F is exactly right for OCR'd documents full of clause numbers and defined terms — but overlap splitting smears those exact tokens across fragment boundaries. The `alpha` knob then blends a blurred keyword signal with a blurred vector signal; no ratio fixes the units.
- **Overlap pollutes both result lists.** Every boundary span exists twice, so BM25F and vector rankings each surface near-duplicates that crowd out the passage that actually answers the question.

Chunksets fix this at the unit level: self-explanatory root-to-leaf paths, no overlap, exact tokens intact. On a reference legal document, assembled cheatsheets delivered 337 tokens of retrieved context versus 1,542 from a recursive character splitter, with zero information loss ([methodology](/document-ingestion-chunking-rag)).

## The pipeline, end to end

```bash
pip install mistralai poma weaviate-client sentence-transformers
```

```python
import os
import weaviate
from weaviate.classes.config import Configure, DataType, Property
from weaviate.classes.init import Auth
from weaviate.classes.query import Filter
from mistralai import Mistral
from poma import PrimeCut
from sentence_transformers import SentenceTransformer

# 1. Mistral OCR — your existing call, unchanged.
mistral = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
ocr = mistral.ocr.process(
    model="mistral-ocr-latest",
    document={"type": "document_url", "document_url": "https://example.com/contract.pdf"},
    include_image_base64=True,
)
with open("contract.mistral-ocr.json", "w") as f:
    f.write(ocr.model_dump_json())

# 2. The missing link — raw result JSON in, chunks + chunksets out.
poma = PrimeCut()  # reads POMA_API_KEY
result = poma.ingest("contract.mistral-ocr.json")  # Mistral shape auto-detected

# 3. One collection of chunksets, hierarchy as filterable properties.
client = weaviate.connect_to_weaviate_cloud(
    cluster_url=os.environ["WEAVIATE_URL"],
    auth_credentials=Auth.api_key(os.environ["WEAVIATE_API_KEY"]),
)
client.collections.create(
    "Chunkset",
    vectorizer_config=Configure.Vectorizer.none(),  # bring your own vectors
    properties=[
        Property(name="content", data_type=DataType.TEXT),
        Property(name="file_id", data_type=DataType.TEXT),
        Property(name="page", data_type=DataType.INT),
        Property(name="depth", data_type=DataType.INT),
        Property(name="chunkset_index", data_type=DataType.INT),
    ],
)
chunksets = client.collections.get("Chunkset")

model = SentenceTransformer("all-MiniLM-L6-v2")
file_id = result.chunks[0].file_id
for i, cs in enumerate(result.chunksets):
    chunksets.data.insert(
        properties={"content": cs.to_embed, "file_id": file_id,
                    "page": cs.page, "depth": cs.depth, "chunkset_index": i},
        vector=model.encode(cs.to_embed).tolist(),
    )

# 4. Hybrid query: BM25F on content + your vector, blended by alpha.
query = "What are the early termination conditions?"
res = chunksets.query.hybrid(
    query=query,
    vector=model.encode(query).tolist(),
    alpha=0.5,
    filters=Filter.by_property("file_id").equal(file_id),
    limit=3,
)
cheatsheet = "\n\n".join(o.properties["content"] for o in res.objects)
client.close()
```

POMA validates the payload shape up front (a corrupted or mislabeled upload 422s immediately), handles all three Mistral image cases (base64 → described; annotation → spliced; neither → visible marker, counted in `content_metadata`), strips running headers/footers, and rebuilds the cross-page heading tree before chunking. To force or suppress detection, set `external_ocr_source` to `"mistral"` or `"none"`.

## Metadata mapping: POMA fields → Weaviate primitives

| POMA chunk field | Weaviate primitive | What it enables |
| --- | --- | --- |
| `to_embed` | object vector (bring-your-own) | paraphrase retrieval |
| `to_embed` text | `content` TEXT property, BM25F-indexed | exact-term half of hybrid search |
| `file_id` | TEXT property, `Filter.by_property` | scope queries to one document |
| `page` (from Mistral `pages[].index`) | INT property | page filters, page-cited answers |
| `depth` | INT property | filter/re-rank by hierarchy level |
| `chunkset_index` | INT property | stable ordering at assembly time |
| chunkset lineage | optional cross-reference to a `Chunk` collection | chunk-level provenance without a second store |

## Frequently asked questions

### How do I get Mistral OCR results into Weaviate for RAG?

Save the raw `/v1/ocr` JSON, run `PrimeCut().ingest()` on it (auto-detected, hierarchy rebuilt), create a collection with `file_id`/`page`/`depth` properties, insert one object per chunkset with your own vector, and retrieve with `query.hybrid`.

### What properties should a Weaviate collection have for Mistral OCR content?

`content` (TEXT, feeds BM25F), `file_id` (TEXT), `page` (INT, from `pages[].index`), `depth` (INT), and `chunkset_index` (INT). POMA emits all of these per chunkset, so the mapping is mechanical.

### What hybrid alpha should I use in Weaviate for OCR'd documents?

Start at `alpha=0.5` (0 = pure BM25F, 1 = pure vector). Exact tokens make the keyword half valuable — but only if chunking kept them intact. No blend ratio repairs fragments.

### Why do page filters break when importing Mistral OCR markdown into Weaviate?

Joining `pages[].markdown` before splitting destroys page boundaries, leaving the `page` property nothing truthful to hold. POMA keeps `pages[].index` per chunk, so `page` arrives as a real filterable INT.

### Should I use Weaviate cross-references to model Mistral OCR chunk hierarchy?

Optional. Cross-references model chunkset→chunk lineage cleanly for provenance UIs, but chunksets are already self-explanatory — retrieval needs no query-time graph traversal.

## Related recipes

Same parser, different store: [Mistral OCR → Qdrant](/pipelines/mistral-ocr-to-qdrant) · [Mistral OCR → Pinecone](/pipelines/mistral-ocr-to-pinecone) · [Mistral OCR → pgvector](/pipelines/mistral-ocr-to-pgvector)

Same store, different parser: [LlamaParse → Weaviate](/pipelines/llamaparse-to-weaviate) · [Azure Document Intelligence → Weaviate](/pipelines/azure-document-intelligence-to-weaviate) · [Unstructured.io → Weaviate](/pipelines/unstructured-to-weaviate)

Foundations: [The Optimal Chunker for Mistral OCR](/optimal-chunker-mistral-ocr) · [The Optimal Chunks for Weaviate](/optimal-chunks-weaviate) · [All pipeline recipes](/pipelines/) · [RAG chunking guide](/guides/rag-chunking/)