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

AbstractEmbeddingFilter subclasses (e.g. OpenAIEmbeddingFilter) split tk:content with the MarkdownChunker and send each piece to a text embeddings endpoint. Carries text, a vector, and a TextLocator (character offsets).

Image

OpenAIImageEmbeddingParser sends rendered PDF page images (PDFBox or Poppler) to a CLIP-like image embeddings endpoint, e.g. Jina CLIP v2. Carries a vector and a PaginatedLocator (page number), and no text.

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_DOCUMENTS or PARENT_CHILD) decides how embedded files are emitted; each embedded file becomes its own document either way.

  • tk:chunks on each document holds all of that document’s chunks, text and image alike.

  • The emitter clients (ESClient, OpenSearchClient) recognize tk:chunks and 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 nested kNN 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 tk:chunks mapped as a nested type — everything about a file in one place, updated atomically.

nested kNN needs Elasticsearch 8.11+ and has limitations; a document with many chunks gets expensive; individual chunks cannot be retrieved on their own. What shipped is this shape plus the per-file split and the structured-JSON requirement, and it inherits the same nested kNN caveats.

B

Each chunk becomes its own document with a parent_doc_id keyword field (a plain reference, not a join). This is the standard RAG pattern used by LangChain, LlamaIndex, and Haystack, and it gives one dense_vector per document.

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 has_parent queries work.

Join queries are expensive, kNN combined with parent_id queries is awkward, and routing becomes mandatory so that every child lands on the parent’s shard.

Future Work

  • Option B support — a ChunkStrategy.SEPARATE_DOCUMENTS that explodes chunks into individual ES/OpenSearch documents at emit time, for simpler kNN search without nested queries.

  • Hybrid search — combining kNN vector search on chunks with BM25 text search on the parent document’s content field.

  • Chunk-level metadata — propagating selected parent metadata onto each chunk for filtering during vector search.