Skip to content

TIKA-4766: typed Document parse contract for tika-grpc - #2921

Closed
krickert wants to merge 3 commits into
apache:mainfrom
ai-pipestream:TIKA-4766-document-contract
Closed

TIKA-4766: typed Document parse contract for tika-grpc#2921
krickert wants to merge 3 commits into
apache:mainfrom
ai-pipestream:TIKA-4766-document-contract

Conversation

@krickert

@krickert krickert commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #2916, reshaped per the review there. Instead of mirroring Tika's open metadata taxonomy in protobuf (~5k lines of proto, per-format messages), this PR types the thing that is actually stable: the parsed document. One small contract — document.proto is 208 lines — and format specifics live in per-parser mapping code, never in the wire.

FetchAndParseReply.fields (map<string,string>, field 2, now reserved) is replaced by FetchAndParseReply.document.

How this answers the #2916 review

Concern from #2916 Where it landed
"11k lines to nail down maybe 80% of an open set" 208-line contract; whole PR is ~3.9k lines, most of it mapper code + tests
Clients rebuild when metadata definitions change A metadata key is now data (extra tail), not schema — add/rename/retype a Tika Property and no client regenerates anything
Lossless catch-all as source of truth Document.extra carries every Tika key, multivalue-preserving; a test asserts nothing is dropped
Special handling only for DC + core props DocumentMetadata types only the bounded cross-format fields (title, authors, dates as Timestamp, counts, dimensions, rights)
Break it into individually reviewable tasks This PR is the contract only; see "Deliberately not in this PR"

The shape

  1. Content tree: markdown (the same render ToMarkdownContentHandler already produces since TIKA-4730) plus blocks — that markdown parsed once, format-agnostically, into a structured tree of headings/paragraphs/lists/tables/code blocks/inline runs (CommonMark + GFM, a spec that does not churn). This is what a downstream NLP/RAG/embeddings consumer actually wants: typed tables and sections, not a string to re-parse.
  2. Typed common metadata: DocumentMetadata, grouped by concern, not by source format. Dates are Timestamps, counts are ints — not strings that 12 language clients each re-parse.
  3. Tagged tail: extra — every remaining key, typed only where Tika's own Property declares a type (integer/real/boolean/date), string otherwise, never guessed.
  4. embedded recurses: a PDF with an embedded image is a parent Document with a fully typed child — no forcing two formats into one bucket (this was the oneof problem from TIKA-4766: Typed parse response grpc #2916).
  5. Adding a format = adding a DocumentTransformer (see tika-grpc-mapper/docs/EXTENSIONS.md); PdfDocumentTransformer is 65 lines and the wire contract does not move.

Deliberately not in this PR (follow-ups, each its own PR)

  • Pluggable external parsers: registering a third-party gRPC service whose output rides along on the Document as a google.protobuf.Any — so wildly different result shapes (e.g. a document-layout model's tree) never require Tika to model them. Built and tested on a branch; kept out to keep this reviewable.
  • A Markdown parser for .md input files (separate JIRA).
  • Richer typed fields, if and only if real cross-format demand appears — they'd be additive optional fields, compatible both directions.

Open decisions where reviewer preference wins

  1. Tail shape: repeated MetadataField with a typed value oneof (as implemented) vs the map<string, StringList> suggested in TIKA-4766: Typed parse response grpc #2916. The typed-where-declared tail preserves types without guessing; the map is maximally churn-proof. Swapping is a one-message change — happy to go either way.
  2. markdown + blocks both: today both ship (string render + structured tree). If payload size matters, a per-request flag choosing one is easy.
  3. Hard removal vs staged: fields is hard-removed (4.0, nothing consumes it yet); can switch to deprecate-then-remove if preferred.

Client migration

Before After
fields["X-TIKA:content"] document.markdown (or walk document.blocks)
fields["Content-Type"] document.content_type
Ad hoc title/author/date strings document.metadata.title / .authors / .created (Timestamp)
Any other key document.extra (typed by declared Property type, string otherwise)

Test plan

  • ./mvnw -pl tika-grpc-api,tika-grpc-mapper,tika-grpc test — green (transformer tests against real parse fixtures per format, block-tree tests, DocumentBuilder envelope/status/embedded tests, server tests reading FetchAndParseReply.document)
  • tika-grpc-api jar bundles META-INF/org.apache.tika.grpc.v1.descriptors (verified: contains document.proto)
  • e2e tika-grpc-e2e-test compiles against the new API
  • CI

Downstream context: this contract is what the OpenNLP gRPC work (OPENNLP-1833) will consume as input — Tika parse → typed document → NLP/embeddings without re-parsing strings.

@krickert
krickert force-pushed the TIKA-4766-document-contract branch 2 times, most recently from c1de042 to 1d2d5bd Compare July 1, 2026 23:55
@krickert
krickert force-pushed the TIKA-4766-document-contract branch from 1d2d5bd to da51be9 Compare July 2, 2026 01:41
@krickert

krickert commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up work is broken out into TIKA-4771 (pluggable external parsers) and TIKA-4772 (document event streaming) rather than growing this PR.

@krickert

krickert commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@nddipiazza @tballison what do you think of this design instead? far less fields - ability to roll your own protobuf model in the future. Best of both worlds. Document structure is very markdown-friendly. I'll make the output of this be able to be the input of the grpc OpenNLP grpc server.

@nddipiazza nddipiazza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: e2e test coverage for the new typed Document contract

Reviewed the design overall — the shift from mirroring Tika's metadata taxonomy in protobuf to a small, stable document.proto (208 lines) plus per-parser mapper code is a solid answer to the #2916 feedback. A few things worth addressing before merge:

Testing gap: e2e coverage does not exercise the new contract

The only change in tika-e2e-tests/tika-grpc is a 2-line fix in HandlerTypeTest.java:

- String htmlContent = htmlReply.getFieldsMap().get("X-TIKA:content");
+ String htmlContent = htmlReply.getDocument().getMarkdown();

(same for the TEXT handler case). This is a compile-fix to keep the pre-existing assertion working against the new API — it only proves document.markdown is non-empty over a real gRPC round-trip for two handler types.

It does not exercise, end-to-end, through the live server:

  • DocumentMetadata typed fields (title/authors/dates/counts) — no e2e assertion anywhere
  • extra tagged-tail typing (int/bool/date/string) — no e2e assertion
  • blocks structured content tree (headings/tables/lists/code) — untouched by any e2e test
  • embedded recursion for container formats (e.g. an Office doc with an embedded image) — no e2e coverage at all
  • format_category routing hint — untouched

The other e2e tests (FileSystemFetcherTest, IgniteConfigStoreTest, ExternalTestBase) only ever inspect getFetchKey()/getStatus() on FetchAndParseReply — none of them look at getDocument(), so the bulk-corpus/streaming/ignite e2e paths give zero signal on the new typed contract.

The real proof of correctness for the mapping logic lives entirely in tika-grpc-mapper's unit tests (DocumentBuilderTest, MarkdownBlockTreeBuilderTest, one test class per transformer), which feed fixture-derived Metadata/markdown into DocumentBuilder in-process. That's good for the mapping logic itself, but it bypasses the actual gRPC wire serialization, the live server (TikaGrpcServerImpl), the pipes client, and fetcher plumbing entirely.

Ask: add (or extend HandlerTypeTest) at least one e2e case per format that fetches a real file through the live gRPC server and asserts on document.metadata (a couple of typed fields), document.extra (at least one tagged key), and one document.embedded case (e.g. an Office/PDF file with an embedded image) — not just that markdown is non-empty. Right now this new, larger surface area has no live-server coverage at all.

Other findings from code inspection

  1. Likely-dead error path: DocumentBuilder.build()'s primary == null branch (returns FAILED with "No metadata returned from parse") looks unreachable in production — the only caller, TikaGrpcServerImpl, always passes a non-null Metadata (tikaMetadata = new Metadata() as fallback when emitData()/metadata list is empty). Worth double-checking this is intentional, since a real fetch failure with no emit data currently produces an "empty-but-success-shaped" Document rather than hitting this explicit error branch.
  2. Duplication: 7 of 8 format transformers (Generic, Pdf, Html, Image, Office, Rtf, Epub) repeat an identical block mapping TITLE/DESCRIPTION/CREATOR/SUBJECT/LANGUAGE/CREATED/MODIFIED. Consider factoring this into a shared helper in TransformSupport called by each transformer, keeping only the format-specific additions per-transformer.
  3. Minor: parseTimeMs in TikaGrpcServerImpl measures fetch+parse combined (timer starts before pipesClient.process()), though the field's doc/intent reads as parse-only time.

Nice work on the overall shape of the contract — the main blocker from my read is the e2e coverage gap above.

@nddipiazza nddipiazza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is a solid design and a much better-scoped answer to the #2916 feedback: a small stable 208-line proto, format logic pushed into per-parser Java transformers, and a lossless typed+tagged tail. Requesting changes mainly on test coverage for the new contract, plus a couple of correctness/maintainability issues found while reading the mapper code (see inline comments).

Comment thread tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServerImpl.java Outdated
@krickert

krickert commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@nddipiazza great feedback..

Review: e2e test coverage for the new typed Document contract

Reviewed the design overall — the shift from mirroring Tika's metadata taxonomy in protobuf to a small, stable document.proto (208 lines) plus per-parser mapper code is a solid answer to the #2916 feedback. A few things worth addressing before merge:

Testing gap: e2e coverage does not exercise the new contract

The only change in tika-e2e-tests/tika-grpc is a 2-line fix in HandlerTypeTest.java:

- String htmlContent = htmlReply.getFieldsMap().get("X-TIKA:content");
+ String htmlContent = htmlReply.getDocument().getMarkdown();

(same for the TEXT handler case). This is a compile-fix to keep the pre-existing assertion working against the new API — it only proves document.markdown is non-empty over a real gRPC round-trip for two handler types.

It does not exercise, end-to-end, through the live server:

  • DocumentMetadata typed fields (title/authors/dates/counts) — no e2e assertion anywhere
  • extra tagged-tail typing (int/bool/date/string) — no e2e assertion
  • blocks structured content tree (headings/tables/lists/code) — untouched by any e2e test
  • embedded recursion for container formats (e.g. an Office doc with an embedded image) — no e2e coverage at all
  • format_category routing hint — untouched

The other e2e tests (FileSystemFetcherTest, IgniteConfigStoreTest, ExternalTestBase) only ever inspect getFetchKey()/getStatus() on FetchAndParseReply — none of them look at getDocument(), so the bulk-corpus/streaming/ignite e2e paths give zero signal on the new typed contract.

The real proof of correctness for the mapping logic lives entirely in tika-grpc-mapper's unit tests (DocumentBuilderTest, MarkdownBlockTreeBuilderTest, one test class per transformer), which feed fixture-derived Metadata/markdown into DocumentBuilder in-process. That's good for the mapping logic itself, but it bypasses the actual gRPC wire serialization, the live server (TikaGrpcServerImpl), the pipes client, and fetcher plumbing entirely.

Ask: add (or extend HandlerTypeTest) at least one e2e case per format that fetches a real file through the live gRPC server and asserts on document.metadata (a couple of typed fields), document.extra (at least one tagged key), and one document.embedded case (e.g. an Office/PDF file with an embedded image) — not just that markdown is non-empty. Right now this new, larger surface area has no live-server coverage at all.

Other findings from code inspection

  1. Likely-dead error path: DocumentBuilder.build()'s primary == null branch (returns FAILED with "No metadata returned from parse") looks unreachable in production — the only caller, TikaGrpcServerImpl, always passes a non-null Metadata (tikaMetadata = new Metadata() as fallback when emitData()/metadata list is empty). Worth double-checking this is intentional, since a real fetch failure with no emit data currently produces an "empty-but-success-shaped" Document rather than hitting this explicit error branch.
  2. Duplication: 7 of 8 format transformers (Generic, Pdf, Html, Image, Office, Rtf, Epub) repeat an identical block mapping TITLE/DESCRIPTION/CREATOR/SUBJECT/LANGUAGE/CREATED/MODIFIED. Consider factoring this into a shared helper in TransformSupport called by each transformer, keeping only the format-specific additions per-transformer.
  3. Minor: parseTimeMs in TikaGrpcServerImpl measures fetch+parse combined (timer starts before pipesClient.process()), though the field's doc/intent reads as parse-only time.

Nice work on the overall shape of the contract — the main blocker from my read is the e2e coverage gap above.

My bad; the e2e suite really was only asserting "markdown is non-empty". Added three live-server cases to HandlerTypeTest:

  • PDF (testPDF.pdf): asserts typed document.metadata (title, page count), a boolean-typed document.extra key (pdf:encrypted), document.blocks, and format_category.
  • HTML (sample.html): asserts typed title, FORMAT_WEB, and a string-typed extra key.
  • Embedded (test_recursive_embedded.docx): asserts FORMAT_OFFICE, typed created/modified dates, and that document.embedded recurses with per-child metadata.

Two things fell out of writing these:

  1. The extra-tail typing was actually broken over the live server - MetadataTagger relies on Property.get(key), but Tika's property registry is populated lazily by class init, so in the server JVM pdf:encrypted came back as a string, not a boolean. The unit tests never caught it because they happen to load the vocabulary classes first. Fixed by force-loading the tika-core metadata vocabularies in a static initializer in MetadataTagger, and the new e2e test now pins the behavior.
  2. The e2e configs needed grpc.allowComponentManagement / allowPerRequestConfig opt-ins after TIKA-4764 landed on main, otherwise every test dies with PERMISSION_DENIED - updated the three config JSONs.

Note the CI e2e job doesn't currently execute these (the -pl tika-e2e-tests -am verify invocation only builds the aggregator POM and no plugins are provisioned) - that's a pre-existing infra gap, but the suite passes locally against a live server.

@krickert

krickert commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @nddipiazza, all four points addressed:

  • e2e tests now exercise the typed contract over a live server (PDF/HTML/embedded DOCX), which flushed out and fixed a real bug: lazy Property registry init made typed extra keys degrade to strings in the server JVM.
  • The primary == null branch is now reachable (server passes null on empty output) and correctly treats EMPTY_OUTPUT as success.
  • Common field mapping deduped into TransformSupport.mapCommonFields.
  • Renamed parse_time_ms to fetch_parse_time_ms to match what's actually measured.

Also merged latest main (the lombok removal and split-packages changes). Full build plus mapper, grpc, and e2e suites pass locally.

@krickert
krickert force-pushed the TIKA-4766-document-contract branch from 234247b to 8b79b26 Compare July 10, 2026 03:14
One Document shape for parsed output instead of a message per source
format. Content is a block tree anchored to the markdown block model;
the tree is canonical and a flat markdown rendering is returned only
when the request asks, so a reply never carries the content twice.
Format specifics come from per-parser transformer code, not new wire
messages: adding a parser never touches the proto.

The typed metadata follows the Dublin Core element set (title, authors,
description, keywords, languages, publishers, identifiers, dates,
rights), with a tagged tail for the rest: typed where Tika declares a
type, string otherwise, never guessed. Status lives in one place, the
typed ParseStatus for branching plus the raw pipes result name for
diagnostics. SourceOrigin records the SHA-256 of the parsed bytes and
ParseStatus the producing Tika version.

The shape stays small. What it adds is metadata consumers end up
parsing or inferring on their own anyway: provenance, typed fields,
and structure that does not need a second parse.
@krickert
krickert force-pushed the TIKA-4766-document-contract branch from 8b79b26 to a0ff002 Compare July 10, 2026 03:53
krickert added 2 commits July 10, 2026 01:04
DocumentTextFlattener walks Document.blocks depth-first and emits plain
text, recording an exact (block path, char range) anchor for every
text-bearing block as it goes, so a consumer that annotates the flat
text can project spans back onto blocks without re-parsing anything.
Tables linearize row-wise with an anchor per cell; code blocks and raw
html are skipped for analysis and reported as skipped paths; blocks are
separated by a blank line so sentence boundaries cannot cross blocks.
FlattenedText carries the document id, the source digest, and a
flattening version, since offsets are only meaningful against the exact
flattening that produced them.
Parses bytes the caller already has: no fetcher registration, no
client-visible server state, and it works with component management
locked down. The parse runs in the same isolated forked worker as
FetchAndParse. Runtime config-store writes do not reach the forked
worker, so the server bakes an internal file-system fetcher, rooted at
a server temp directory, into an augmented copy of its config at
construction. The payload is written there under a unique name, parsed,
and deleted, and the reply carries the caller's request_id as the fetch
key and Document id.
@krickert
krickert requested a review from nddipiazza July 10, 2026 11:25
@krickert

Copy link
Copy Markdown
Contributor Author

@nddipiazza @tballison -

I figured I'd explain why and what I'm trying to do. I view tika is a primary input plane and think the gRPC server is where I'd want it to integrate.

So a Tika Document shape first, including any revisions you think it needs before it becomes a contract. I would use this as my input.

The OpenNLP gRPC server is deliberately following OpenNLP 3.0 as it develops. As features are merged to OpenNLP main, I add the corresponding capabilities to the server. My goal is for the gRPC server to be fully compliant with the OpenNLP 3.0 API when I release it and have the ability to extend it (you can see the OpenVINO and TEI extensions already and they're working prototypes).

If Tika adopts this Document contract, I will make it a first-class input to that server. The remaining integration details include flattening the block tree, preserving block-to-text offsets, mapping annotations back to source blocks, and deciding which operations should stream. Those can be worked out after the contract is settled. I can also contribute to the fetching, parsing, and performance work here - which I'd love to do.

I want to avoid defining and implementing a second document input format before we settle this boundary. If Tika later chooses a different shape, OpenNLP would have an unused input contract and both projects would carry unnecessary compatibility surface.

So I'm curious: Is this Document direction an acceptable shared boundary for Tika output and downstream consumers such as OpenNLP?

If not, which parts need to change before it can serve that role?

I'm in no rush to get the answer - but I've never had an opportunity explicit about the vision and goal. By agreeing to a model, it can allow both projects to launch with an amazing open source story to tell.

The current OpenNLP gRPC work is here:

https://github.com/apache/opennlp-sandbox/tree/OPENNLP-1833-grpc-expansion

Once we agree on a shape, I can make the OpenNLP server conform to it as the rest of the 3.0 features land.

This is not going to happen right away - we're still going to tease more releases so I'd expect both servers to go through many iterations. What I'm trying to do on our side is to have a generic enough output shape so we can survive a lot of new features for point releases. With Tika - I want to help integrate and transform a lot of parsing surfaces and connectors (as you saw with the markdown parser).

But it all starts with a document shape - then there's no rush to add on more features.

Thoughts?

@krickert

Copy link
Copy Markdown
Contributor Author

@nddipiazza up for another look? If you land this shape, I can demo how this would work w/opennlp 3's grpc server + make an enhancement to tika to integrate it directly.

Let me know what you think -

Another suggestion is to make this a separate build under the tika umbrella since you'll need to keep up with CVEs and stuff for creating a docker image. I'd help regardless. Up to you..

@nddipiazza

Copy link
Copy Markdown
Contributor

Follow-up after digging further into the tika-grpc/tika-grpc-api contract specifically (cc @tballison since this bears directly on your #2916 feedback).

Two scope concerns in the current diff

1. ParseBytesRequest/ParseBytes RPC is new scope, not described anywhere in this PR.

tika.proto adds a whole new RPC (rpc ParseBytes(ParseBytesRequest) returns (FetchAndParseReply)) for parsing inline bytes without registering a fetcher. It isn't mentioned in the PR description, the "what's in / deliberately not in this PR" table, or the client migration table — it has nothing to do with the typed Document contract.

The implementation in TikaGrpcServerImpl is also a real workaround, not a thin RPC:

  • On every server start it creates a temp directory (Files.createTempDirectory("tika-grpc-parse-bytes")) and rewrites the loaded Tika config to inject a synthetic file-system-fetcher rooted there (augmentWithParseBytesFetcher), so the forked pipes worker can "fetch" bytes the caller pushed inline.
  • Neither shutdown() nor postShutdown() clean up parseBytesDir, nor the temp config file written by augmentWithParseBytesFetcher (Files.createTempFile("tika-grpc-config", ".json")). Every server start leaks a temp dir + temp file that's never removed.
  • It changes config-loading behavior for every request (the augmented config becomes the one config path the server ever loads) for the sake of one RPC.

Given the whole point of this PR (and #2916) is keeping tika-grpc small and reviewable, I'd pull ParseBytes into its own PR/JIRA with its own review.

2. FormatCategory re-bakes per-format buckets into the wire contract.

document.proto's FormatCategory enum (PDF/OFFICE/IMAGE/HTML/RTF/EPUB/WARC/GENERIC) plus its detector (FormatCategoryDetector, mime/extension string matching) reintroduces the pattern called out in #2916 point 3 — re-deriving something Tika already told us (content_type already carries the detected MIME type) via a hand-maintained second classification. Every new "coarse category" is a public proto enum change — the same wire churn this PR's premise is meant to avoid. RTF/EPUB/WARC already getting dedicated enum values shows the pull. The docs pitch "adding a format = adding a DocumentTransformer, wire contract doesn't move" isn't quite true here.

Aside from those two, the actual mapper transformers (PdfDocumentTransformer, OfficeDocumentTransformer, EpubDocumentTransformer, WarcDocumentTransformer, etc.) are appropriately thin now — 40-70 lines each, no format-structure-extraction logic living in tika-grpc-mapper — and the module's main-scope deps are clean (tika-core + tika-grpc-api + commonmark + protobuf-java; every parser module is test-scoped only). That part of the #2916 "wrong altitude" concern looks resolved.

Proposal: stage this as a sequence of small PRs

Given @tballison's core objection was scale/maintenance risk of a big bang change, I think this whole effort would merge faster broken into independently reviewable, independently testable PRs — mostly a split of what already exists here, not a rewrite:

  1. Minimal typed contract. document.proto trimmed to: Document envelope (id, content_type, parsed_at, status, origin), DocumentMetadata with DC fields only (title/authors/description/keywords/languages/publishers/identifiers/created/modified/rights), the MetadataField/MetadataValue tagged tail, ParseStatus. No blocks/markdown, no FormatCategory, no embedded. Pair with GenericDocumentTransformer only, wired into tika-grpc (FetchAndParseReply.document) with a real e2e test through the live server. This is close to the ~80-line proto Tim said he'd be okay with, and directly answers the top TIKA-4766: Typed parse response grpc #2916 concern on its own.
  2. Per-format typed transformers, one PR (or 2-3 grouped): PdfDocumentTransformer, OfficeDocumentTransformer, HtmlDocumentTransformer, ImageDocumentTransformer. Additive only, no proto change — review is just "is this mapping correct."
  3. Content tree (blocks/markdown) as its own PR — a genuinely separate feature (commonmark parsing, tables/lists/inline runs), with its own live-server e2e round-trip test.
  4. embedded recursion as its own PR — distinct complexity (depth limits, cycles), with an Office/PDF-with-embedded-image e2e test.
  5. Niche formats (Epub, Warc, Rtf, Creative Commons) last, only once the mechanism is proven on common formats — each small and additive.

Would drop or defer indefinitely:

  • FormatCategory — let clients bucket on content_type instead, unless a client has a concrete need.
  • ParseBytes — separate JIRA/PR entirely, unrelated to this contract.
  • External pluggable parsers, .md parser — already correctly deferred in the description.

Each stage above is independently testable (unit + e2e) and independently mergeable, which should make review a lot less daunting than reviewing ~3.9k lines at once.

@krickert

Copy link
Copy Markdown
Contributor Author

This sounds great and goes exactly in the direction I'm hoping. Multiple tickets = real collaboration and proper pacing

I'll close this and post the first suggested change but keep the overall thread alive here unless you can do something in jira after a couple tickets land.

I like the feedback's instinct because shaping the API is by far the most important part.

Honestly I understand this is a big feature and I appreciate the time you're taking to look into it. seriously.

krickert added a commit to ai-pipestream/tika that referenced this pull request Jul 21, 2026
First slice of the typed Document contract, per review on apache#2921: the
Document envelope, Dublin Core DocumentMetadata, the lossless tagged
metadata tail, and ParseStatus. FetchAndParseReply.document is additive:
the legacy fields/status stay untouched, so no client breaks.

Tail values mirror Tika's String[]-backed metadata model: every entry is
a typed array, tagged by the declared Property element type, so declared
integer sequences (pdf:charsPerPage, tiff:BitsPerSample) arrive as
int64s per element and dates as Timestamps. Untyped keys stay strings,
never guessed; if any element refuses its declared type the whole entry
falls back to strings.

Deferred to follow-up PRs: per-format transformers, the structured
content tree, embedded-document recursion. Dropped per review:
FormatCategory, the ParseBytes RPC.
@krickert

Copy link
Copy Markdown
Contributor Author

Closing in favor of #2961

@krickert krickert closed this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants