Chunk Strategies for Search Engines
Tika 4.x introduces a unified chunking and embedding pipeline (tika-inference) that puts a
tk:chunks array on each document’s metadata. This page describes how those chunks reach
Elasticsearch and OpenSearch, and why the shipped strategy is the one it is.
The tk:chunks Field
All chunk data lands in one metadata field, tk:chunks, as a JSON array. There is no
separate field for image embeddings versus text embeddings — a chunk is a chunk, identified
by which locators it carries:
| Chunk kind | Produced by, and what it carries |
|---|---|
Text |
|
Image |
|
SpatialLocator (bounding box) and TemporalLocator (millisecond range) are already defined
and round-tripped by ChunkSerializer; no component produces them yet.
A single chunk looks like:
{
"text": "Revenue grew 15% year-over-year...",
"vector": "base64-encoded-float32-le",
"locators": {
"text": [{"start_offset": 0, "end_offset": 120}],
"paginated": [{"page": 1}]
}
}
When several pipeline components produce chunks for the same document — the image embedder
during parsing, the text embedder as a metadata filter — they merge into the same array via
ChunkSerializer.mergeInto().
What Tika Emits Today
Each file — the container and each embedded file — is a separate Elasticsearch or
OpenSearch document, matching the existing SEPARATE_DOCUMENTS attachment strategy. Chunks
ride along inside their file’s document as a structured JSON array: not exploded into
separate documents, and not a stringified blob.
{"index":{"_id":"email.msg"}}
{"title":"Re: Q4 report","mime":"message/rfc822","tk:chunks":[
{"text":"Hi team, see attached...","vector":"...","locators":{"text":[{"start_offset":0,"end_offset":35}]}}
]}
{"index":{"_id":"email.msg-<uuid>"}}
{"title":"Q4-report.pdf","mime":"application/pdf","parent":"email.msg","tk:chunks":[
{"vector":"...","locators":{"paginated":[{"page":1}]}},
{"vector":"...","locators":{"paginated":[{"page":2}]}},
{"text":"Revenue grew...","vector":"...","locators":{"text":[{"start_offset":0,"end_offset":120}]}},
{"text":"Operating costs...","vector":"...","locators":{"text":[{"start_offset":121,"end_offset":300}]}}
]}
Mechanically:
-
The emitter’s
AttachmentStrategy(SEPARATE_DOCUMENTSorPARENT_CHILD) decides how embedded files are emitted; each embedded file becomes its own document either way. -
tk:chunkson each document holds all of that document’s chunks, text and image alike. -
The emitter clients (
ESClient,OpenSearchClient) recognizetk:chunksand write it as raw JSON rather than an escaped string, so the nested objects are indexable.
Why this shape:
-
One document per file matches how users think about documents, and an embedded file (a PDF inside an email) gets its own document with its own chunks.
-
Structured JSON lets Elasticsearch/OpenSearch index the vectors and locators natively.
-
No join field and no routing are needed for the chunk relationship.
-
It stays compatible with
nestedkNN when you want to search within a document’s chunks.
The trade-offs are the nested kNN caveats below, and large documents for files with many
pages.
Elasticsearch/OpenSearch Mapping
With nested kNN support, a mapping like this works:
{
"mappings": {
"properties": {
"title": {"type": "text"},
"mime": {"type": "keyword"},
"content": {"type": "text"},
"parent": {"type": "keyword"},
"tk:chunks": {
"type": "nested",
"properties": {
"text": {"type": "text"},
"vector": {
"type": "dense_vector",
"dims": 1024,
"index": true,
"similarity": "cosine"
},
"locators": {
"properties": {
"text": {
"type": "nested",
"properties": {
"start_offset": {"type": "integer"},
"end_offset": {"type": "integer"}
}
},
"paginated": {
"type": "nested",
"properties": {
"page": {"type": "integer"}
}
}
}
}
}
}
}
}
}
vector holds a base64-encoded float32 array throughout Tika processing, and the
emitter writes it as-is. To use a dense_vector mapping you need an ingest pipeline or
custom serialization to decode it at index time; alternatively, map vector as keyword
and decode at query time.
|
Alternatives Considered
| Option | Approach | Why not |
|---|---|---|
A |
One document per file with |
|
B |
Each chunk becomes its own document with a |
Many more documents, and parent metadata must be either denormalized onto every chunk or fetched in a second lookup. Still wanted — see Future Work. |
C |
Chunks are children of the container document via the ES/OpenSearch
join field,
so parent metadata is never duplicated and |
Join queries are expensive, kNN combined with |
Future Work
-
Option B support — a
ChunkStrategy.SEPARATE_DOCUMENTSthat explodes chunks into individual ES/OpenSearch documents at emit time, for simpler kNN search withoutnestedqueries. -
Hybrid search — combining kNN vector search on chunks with BM25 text search on the parent document’s
contentfield. -
Chunk-level metadata — propagating selected parent metadata onto each chunk for filtering during vector search.