Source: http://www.poma-ai.com/docs/pipelines/marker-to-qdrant

# The Missing Link Between Marker and Optimal Retrieval in Qdrant

<ByAuthor />

**The short answer:** Marker gives you fast, local PDF parsing with real recovered structure; Qdrant gives you excellent hybrid vector search. Wired together naively — take `rendered.markdown`, split, embed, upsert — the pipeline drops every figure and every page number before Qdrant sees a single vector. The missing link is POMA: `PrimeCut().ingest()` consumes the saved Marker JSON (the Document block tree auto-detects), splices the side-channel image bytes back in, rebuilds the cross-page hierarchy into [chunksets](/learn/chunking/chunksets), and `PomaQdrant.upsert_poma_points()` writes them as hybrid dense+sparse points with indexed hierarchy payloads. Retrieval comes back as assembled, prompt-ready cheatsheets.

## What each end of the pipeline actually provides

**Marker** (by Datalab / VikParuchuri) runs on your own hardware and turns PDFs into clean structured output. With the JSON renderer (`--output_format json`) you get a Document block tree — Page children, per-block HTML, full `<table>` elements with row and column spans. What it doesn't provide: cross-page hierarchy (a `## Termination clauses` heading on page 41 has no machine link to the `# Master Services Agreement` that opened on page 3), retrieval units, or inline images — figure bytes live in a side-channel `images` dict keyed by name, with only `![](name)` references left in the text. Details: [The Optimal Chunker for Marker](/optimal-chunker-marker).

**Qdrant** provides named dense + sparse vectors on one point, payload indexes for filtered ANN, and HNSW with quantization options, in the cloud or self-hosted — a natural fit for a Marker pipeline that already keeps everything local. What it doesn't provide: any opinion about what a point should contain. It retrieves nearest neighbors of whatever you embedded — including figure-free, context-free fragments, if that's what you gave it. Details: [The Optimal Chunks for the Best Retrieval in Qdrant](/optimal-chunks-qdrant).

## The naive wiring, and where it breaks

The common recipe — run Marker with the default markdown output, `RecursiveCharacterTextSplitter` on `rendered.markdown`, embed, `client.upsert(...)` — breaks in a way specific to this pair:

- **Every figure vanishes before Qdrant.** Marker's images live in the side-channel `images` dict; the markdown carries only `![](name)` references. A markdown-only pipeline embeds those dangling references as literal text and leaves the bytes on disk — so the chart that answers "what does Q3 revenue look like" simply does not exist in the collection. Qdrant can't retrieve a point that was never written.
- **Page numbers never make it into payloads.** Marker's markdown output has no page boundaries, so no Qdrant payload can cite a page and per-document filters are all you have left. The JSON renderer keeps Page blocks — but only if your chunker actually reads them.
- **Overlap inflates the collection.** Splitter overlap embeds every boundary span twice; in Qdrant that means a larger HNSW graph and top-k results where hits 2 and 3 are near-duplicates of hit 1, crowding out the passage that answers the question.
- **Heading levels are discarded**, so retrieved fragments arrive without lineage and the LLM answers out of context — the failure Marker's parsing quality can't fix.

## The pipeline, end to end

```bash
pip install 'poma[qdrant]'

# Your existing Marker run — unchanged. The JSON renderer keeps the
# Document block tree that POMA auto-detects, pages included.
marker_single contract.pdf --output_format json --output_dir out/
```

```python
import os
from poma import PrimeCut
from poma.integrations.qdrant import PomaQdrant

# 1. The missing link — raw Marker JSON in, chunks + chunksets out.
#    Image bytes from the side-channel dict are spliced back into their
#    ![](name) refs as data URIs and described into searchable text.
poma = PrimeCut()  # reads POMA_API_KEY
result = poma.ingest("out/contract/contract.json")  # block tree auto-detected

# 2. Qdrant — hybrid points with hierarchy payloads, one call.
qdrant = PomaQdrant(
    url=os.environ["QDRANT_URL"],
    api_key=os.environ["QDRANT_API_KEY"],
    cloud_inference=True,
    collection_name="contracts",
    dense_model="sentence-transformers/all-MiniLM-L6-v2",
    sparse_model="Qdrant/bm25",
    dense_size=384,
    auto_create_collection=True,
)
qdrant.upsert_poma_points(result)

# 3. Retrieve prompt-ready context.
cheatsheets = qdrant.get_cheatsheets(
    query="What are the early termination conditions?",
    limit=3,
)
print(cheatsheets[0]["content"])
```

POMA validates the payload shape up front — the JSON Document block tree is the strict auto-route fingerprint, and a corrupted or mislabeled upload 422s immediately instead of degrading the index. If what you saved is one of Marker's bare markdown shapes (`{markdown, images, metadata}` or the deprecated `{output, format}` envelope), declare it explicitly with `external_ocr_source="marker"`; to opt a look-alike JSON out of detection, pass `"none"`. Downstream, the same passes as native ingest run: image splicing and description, running-header/footer removal, heading enrichment, and the cross-page hierarchy rebuild. On our reference legal document, this chunking answers the same query with **337 tokens** of retrieved context instead of **1,542** for a recursive character splitter — methodology in [Document Ingestion & Chunking for RAG](/document-ingestion-chunking-rag).

## Metadata mapping: POMA fields → Qdrant primitives

| POMA chunk field | Qdrant primitive | What it enables |
| --- | --- | --- |
| `to_embed` | dense vector + BM25 sparse vector (named vectors, one point) | paraphrase + exact-term hybrid retrieval |
| `file_id` | payload field, **payload-indexed** | scope queries to one document |
| `page` (from Marker's Page blocks, JSON renderer) | payload field, **payload-indexed** | page-cited answers, page-range filters |
| `depth` | payload field | filter/re-rank by hierarchy level |
| `chunk_index` | payload field | stable ordering at assembly time |
| chunkset lineage | payload (`chunk_details`) | cheatsheet assembly without a second store |

## A fully local variant

Marker's whole appeal is that documents never leave your machine, and this pipeline preserves that property end to end: run Marker on your own hardware, point `PomaQdrant` at a self-hosted open-source Qdrant instead of Qdrant Cloud, and drop `cloud_inference=True` in favor of local embedding. The chunking step is the only remote call, and what travels is the parsed result — the pipeline shape, payload schema, and retrieval code stay identical across local and cloud deployments, so a laptop prototype promotes to a managed cluster without rewriting anything.

Two knobs worth knowing on the Qdrant side:

- **Payload indexes are opt-in on plain `QdrantClient`** — `PomaQdrant` handles collection setup for you with `auto_create_collection=True`, including the named dense+sparse vector configuration, so filtered queries on `file_id` and `page` are fast from the first upsert.
- **Quantization** (scalar, product, or binary) shrinks the memory footprint of large collections; because POMA chunksets are overlap-free, the collection is already smaller than a splitter-with-overlap ingest of the same corpus before quantization is even switched on.

## Frequently asked questions

### How do I get Marker output into Qdrant for RAG?

Run Marker with the JSON renderer (`marker_single --output_format json`), hand the saved result to `PrimeCut().ingest()` (auto-detected, images spliced, hierarchy rebuilt), then `PomaQdrant.upsert_poma_points(result)` — hybrid points with `file_id`/`page`/`depth` payloads. Retrieve with `get_cheatsheets(query=...)`.

### Do Marker's extracted images make it into Qdrant?

Only if something reunites them with the text. Marker parks figure bytes in a side-channel `images` dict and leaves `![](name)` refs behind — markdown-only pipelines ship a figure-free collection. POMA splices the bytes back in as data URIs and describes each figure into searchable text, so figures become retrievable Qdrant points. Refs without bytes are neutralized and counted in `content_metadata`.

### Which Marker output format should I use for a Qdrant pipeline — markdown or JSON?

The JSON renderer. Its Document block tree keeps per-page blocks and full HTML tables, and it is the shape POMA auto-detects — so page numbers survive into Qdrant payloads. The default markdown output loses page boundaries and needs an explicit `external_ocr_source: "marker"` declaration.

### What Qdrant payload fields should Marker chunks carry?

`file_id`, `page`, `depth`, `chunk_index` plus content; payload-index `file_id` and `page`. `PomaQdrant` writes these by default.

### Does PomaQdrant use hybrid search for Marker-parsed documents?

Yes, by default. Locally parsed contracts, papers, and manuals are full of exact tokens (clause numbers, part codes, defined terms) that dense embeddings blur. `PomaQdrant` stores a BM25 sparse vector next to the dense vector and fuses both at query time.

## Related recipes

Same parser, different store: [Marker → Pinecone](/pipelines/marker-to-pinecone) · [Marker → Weaviate](/pipelines/marker-to-weaviate) · [Marker → pgvector](/pipelines/marker-to-pgvector)

Same store, different parser: [Mistral OCR → Qdrant](/pipelines/mistral-ocr-to-qdrant) · [Docling → Qdrant](/pipelines/docling-to-qdrant) · [Textract → Qdrant](/pipelines/textract-to-qdrant)

Foundations: [The Optimal Chunker for Marker](/optimal-chunker-marker) · [The Optimal Chunks for Qdrant](/optimal-chunks-qdrant) · [All pipeline recipes](/pipelines/) · [RAG chunking guide](/guides/rag-chunking/)