Source: http://www.poma-ai.com/docs/optimal-chunks-weaviate

# The Optimal Chunks for the Best Retrieval in Weaviate

<ByAuthor />

**The short answer:** the optimal chunks for Weaviate are **chunksets** — self-explanatory units that carry their full heading lineage — stored in one collection with hierarchy metadata (`file_id`, `page`, `depth`) as filterable properties and retrieved with a **hybrid query** (BM25F + vector, blended by `alpha`). Weaviate is one of the few databases where hybrid search is built in rather than bolted on; what decides your retrieval quality is what you put *into* it. Chunksets feed both halves of the hybrid at once: complete thoughts for the embedding, heading terms for the keyword index.

This page covers what Weaviate does brilliantly, what it cannot do for you, and the collection/property design that gets the most out of it.

## Weaviate ships hybrid search out of the box — over whatever you feed it

Weaviate's primitives are a gift to RAG engineers:

- **Built-in hybrid search** — BM25F keyword scoring and vector similarity fused server-side in a single query, weighted by `alpha` (0 = pure keyword, 1 = pure vector).
- **Collections with named vectors** — several embeddings per object when one model isn't enough.
- **Filterable properties** — scope any query by document, tenant, page, or depth without leaving the search.
- **Cross-references** — typed links between collections, so relations like chunkset ↔ member chunks can be modeled as data.
- **Generative and reranker modules** — post-retrieval steps live next to the index instead of in your glue code.

None of these primitives, however, know anything about your documents. Hybrid search fuses two rankings *over whatever objects you inserted*. If those objects are context-free fragments — paragraphs cut loose from the sections that give them meaning — Weaviate will fuse two rankings of context-free fragments, at any scale, with excellent latency. **The ceiling on retrieval quality is set before the first object is inserted.**

## What "optimal chunks" means for Weaviate, concretely

Four properties separate a Weaviate collection that answers questions from one that returns trivia:

1. **Every object is self-explanatory.** A chunkset is a root-to-leaf path through the document — `Master Services Agreement → Termination clauses → Early termination → "Either party may…"`. Retrieved alone, it still tells the LLM (and the human reading citations) exactly where it sits. A bare 512-token window does not. See [POMA chunksets](/learn/chunking/chunksets).
2. **Both hybrid halves get fed.** This is Weaviate-specific leverage: the heading breadcrumbs in a chunkset are exact, high-signal terms, so BM25F matches section vocabulary ("Termination", "§ 7.2") that a fragment never contains — while the same self-contained text gives the embedding a complete thought to encode. One unit, two better rankings, one fusion.
3. **No overlap, no near-duplicates.** Overlap is a patch for splitters that cut mid-thought — and in Weaviate it pollutes *two* indexes: near-identical vectors on the dense side, repeated term statistics on the BM25F side. Chunksets carry context structurally, so your `limit=5` returns five *different* candidates.
4. **Hierarchy in filterable properties.** `file_id`, `page`, `depth`, `chunk_index` as properties turn Weaviate filters into document-aware operations: scope a hybrid query to one contract, exclude appendices by depth, cite pages in the answer.

## The pattern: one chunkset collection, hybrid query, cheatsheets client-side

No plugin needed — the Weaviate v4 Python client and the POMA SDK wire together in a few lines:

```bash
pip install poma weaviate-client
```

```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 poma import PrimeCut

# 1. Chunk any document — PDF, DOCX, HTML, or a bring-your-own-OCR result JSON.
client = PrimeCut()  # reads POMA_API_KEY
result = client.ingest("contract.pdf")

# 2. One collection of chunksets; hierarchy fields as filterable properties.
wv = weaviate.connect_to_weaviate_cloud(
    cluster_url=os.environ["WEAVIATE_URL"],
    auth_credentials=Auth.api_key(os.environ["WEAVIATE_API_KEY"]),
)  # or weaviate.connect_to_local() for a local instance

chunksets = wv.collections.create(
    name="Chunkset",
    vectorizer_config=Configure.Vectorizer.none(),  # bring your own vectors
    properties=[
        Property(name="content", data_type=DataType.TEXT),  # BM25F searches this
        Property(name="file_id", data_type=DataType.TEXT),
        Property(name="page", data_type=DataType.INT),
        Property(name="depth", data_type=DataType.INT),
        Property(name="chunk_index", data_type=DataType.INT),
    ],
)

for cs, vec in zip(result.chunksets, embed([c.to_embed for c in result.chunksets])):
    chunksets.data.insert(
        properties={
            "content": cs.to_embed,
            "file_id": cs.file_id,
            "page": cs.page,
            "depth": cs.depth,
            "chunk_index": cs.chunk_index,
        },
        vector=vec,
    )

# 3. Hybrid query: BM25F + vector fused by alpha, scoped to one document.
question = "What does the contract say about early termination?"
response = chunksets.query.hybrid(
    query=question,                    # feeds the BM25F side
    vector=embed([question])[0],       # feeds the vector side
    alpha=0.5,                         # 0 = pure keyword, 1 = pure vector
    limit=5,
    filters=Filter.by_property("file_id").equal(result.chunksets[0].file_id),
)

# 4. Assemble a cheatsheet client-side: retrieved chunksets share ancestors,
#    so deduplicate the common heading lineage and merge into one context block.
hits = [obj.properties["content"] for obj in response.objects]
```

`embed(...)` is your embedding model of choice; if you prefer, configure a Weaviate vectorizer module instead of `Vectorizer.none()` and let the cluster embed `content` for you — the collection design stays the same.

Step 4 is where the token savings land: retrieved chunksets from the same section repeat the same breadcrumbs, and merging them deduplicates the lineage into one coherent block — POMA calls this a **cheatsheet**. On our reference legal-document benchmark that meant **337 tokens** of context versus **1,542** for a recursive-splitter baseline, with zero information loss — [methodology here](/document-ingestion-chunking-rag).

## Do you need cross-references?

Weaviate can model the chunk ↔ chunkset relation explicitly: a `Chunkset` collection cross-referencing a `Chunk` collection lets you traverse from any retrieved unit to its exact members for auditing, highlighting, or re-chunking. It is a genuinely nice capability — and for the hot retrieval path you usually don't need it, because the chunkset's text already *contains* its lineage. Keep retrieval a single hybrid query on one flat collection; add cross-references when lineage must be traversable as data, not just readable as text. The `.poma` archive remains the portable source of truth either way.

## Chunk shapes in Weaviate, compared

| Property | Fixed 512-token chunks + overlap | Semantic chunks | **POMA chunksets** |
| --- | --- | --- | --- |
| Object is self-explanatory when retrieved alone | ✗ | Sometimes | **✓ (by construction)** |
| BM25F side sees section vocabulary | ✗ (fragment terms only) | Sometimes | **✓ (heading breadcrumbs)** |
| Near-duplicates polluting both hybrid rankings | Many (overlap) | Few | **None** |
| Page + hierarchy as filterable properties | Manual | Manual | **✓ (`file_id`, `page`, `depth`)** |
| Tables survive embedding | ✗ (cut mid-row) | Fragile | **✓ (HTML, never cut)** |
| Post-retrieval assembly | Concatenate raw hits | Concatenate raw hits | **Cheatsheets (deduped lineage)** |

## Frequently asked questions

### What is the optimal chunk size for Weaviate?

There is no universal token count — the optimal unit is a chunk that is self-explanatory without its neighbors. Chunksets achieve that at any size and feed both hybrid halves: dense meaning for the vector side, heading terms for the BM25F side. If you must fix a size, start at 512 tokens — but the size knob cannot restore missing context.

### What should I embed in Weaviate for RAG?

Embed the chunkset's `to_embed` text — leaf content plus ancestor headings — and store the same text in a `TEXT` property so BM25F can search it. Add `file_id`, `page`, `depth`, `chunk_index` as filterable properties. The vector, the keyword index, and the filters then agree on what a unit of meaning is.

### What alpha should I use for Weaviate hybrid search?

`alpha` blends the two rankings: 0 is pure BM25F, 1 is pure vector, 0.5 weights them equally. Corpora full of exact tokens (statute numbers, SKUs, defined terms) deserve meaningful keyword weight. Chunksets improve both sides simultaneously, so tuning alpha becomes a preference, not a rescue operation.

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

Only when lineage must be traversable as data — auditing, highlighting, re-chunking. For retrieval itself, the chunkset's text already contains its lineage, so a single hybrid query on one flat collection is faster and simpler than a graph hop.

### How does chunk overlap affect Weaviate hybrid search?

It pollutes both indexes: near-identical vectors on the dense side, repeated terms on the BM25F side — so after fusion, top-k is even more redundant than in a pure vector store. Chunks that carry hierarchy don't need overlap.

### Does POMA integrate with Weaviate?

Yes — via the standard SDK pattern shown above: `PrimeCut().ingest`, embed each chunkset's `to_embed`, insert into a collection with hierarchy properties, `collection.query.hybrid(...)`, then assemble cheatsheets client-side. No separate plugin required.

## Retrieval with vektoria's assemble()

POMA's `poma.vektoria` package keeps the collection content-free: Weaviate stores only `(id, vector, {file_id, chunkset_index})`, while the actual chunk content lives on a **volume** (a path, `s3://`, or `gs://` URL). After your normal Weaviate query, hand the raw result to `assemble()`:

```python
from poma.vektoria import assemble
from weaviate.classes.query import MetadataQuery

res = coll.query.near_vector(qv, limit=10, return_metadata=MetadataQuery(distance=True))
context = assemble(res, volume="s3://your-bucket/poma")  # -> [{"file_id", "content"}, ...]
```

`assemble()` auto-detects Weaviate's result shape (one of 12 supported stores) and returns deduplicated cheatsheets. Properties (including `file_id`/`chunkset_index`) come back automatically in Weaviate v4 — `return_metadata=MetadataQuery(distance=True)` is only needed for the distance score itself.

## Feed Weaviate from the parser you already run

Pair this page with your ingestion side — the same chunksets, straight from your OCR/parsing tool's raw output:

- [Mistral OCR → Weaviate](/pipelines/mistral-ocr-to-weaviate)
- [LlamaParse → Weaviate](/pipelines/llamaparse-to-weaviate)
- [Azure Document Intelligence → Weaviate](/pipelines/azure-document-intelligence-to-weaviate)
- [Unstructured.io → Weaviate](/pipelines/unstructured-to-weaviate)
- [Docling → Weaviate](/pipelines/docling-to-weaviate)
- [Marker → Weaviate](/pipelines/marker-to-weaviate)
- [AWS Textract → Weaviate](/pipelines/textract-to-weaviate)

Running a different store? The same chunk design applies: [Qdrant](/optimal-chunks-qdrant) · [Pinecone](/optimal-chunks-pinecone) · [pgvector](/optimal-chunks-pgvector) · [Milvus](/optimal-chunks-milvus) · [Chroma](/optimal-chunks-chroma).

Fundamentals: [RAG chunking guide](/guides/rag-chunking/) · [RAG architecture guide](/guides/rag-architecture/) · [POMA chunksets](/learn/chunking/chunksets).