tika-eval & Extraction Quality
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Baseline date: 2026-10-13. This page was written against Apache Tika 4.1.0 (the latest release on Maven Central on that
date) with the 3.3.x differences noted, Java 21 and later for the examples (Tika 4.x needs Java 17), Spring Boot 4.x on
Spring Framework 7 and the Docker image apache/tika:4.1.0. Every other page of this section tells you how to get text
out of a document. This one answers the question that follows: is the text any good, and did my last change make it
better or worse? Tika’s answer is tika-eval, the project’s own toolkit for profiling one extraction run and comparing
two. The Tika committers use it for regression testing before each release; you can use it before you upgrade Tika, change
an encoding-detector chain, switch on OCR or replace a parser.
The page explains why extraction quality has to be measured, the artifacts that make up tika-eval in 4.1.0, the
Profile and Compare tools with every metric they record, the official regression recipe applied to the Bookshelf
corpus, a real 3.3.2 to 4.1.0 upgrade comparison, querying the H2 database, the crash ledger, text-quality scores at
parse time with TikaEvalMetadataFilter, the new structure-comparison tool and the pitfalls.
Related pages: Tika App (Command Line & GUI) (which writes the
extracts), Tika Pipes (the reporters behind the crash ledger),
Character Encoding Detection (the JunkDetector behind
languageness), Language Detection & Translation
(CharSoup) and What’s Changed & Migration (what an upgrade
changes).
How this page was checked: the tika-eval-app-4.1.0.zip distribution was downloaded from downloads.apache.org
(SHA-512 verified), the tika-app-4.1.0.zip and the 3.3.2 runnable tika-app-3.3.2.jar were used to write extracts,
and every Profile, Compare, Report, H2 query, custom report, ledger join and structure comparison shown below was run on
macOS with Java 25 against a small Bookshelf corpus. The option names were read from the tika-eval-app 4.1.0 and
3.3.2 source jars on Maven Central, and the metric definitions from the tika-eval-core 4.1.0 source jar. The section
What was verified and what was not lists what could not be checked.
Why measure extraction quality
Extraction rarely fails loudly. A parser that throws is easy to see; the expensive failures are the quiet ones:
-
Mojibake. A legacy file decoded with the wrong charset still produces text, so the index accepts it, but
CatálogobecameCat�logoorCatálogoand nobody finds the document by its words. -
Empty or missing text. A scanned PDF without OCR, an encrypted file, a parser that skips a part: zero tokens and no exception.
-
Junk text. OCR of a poor scan, a PDF with broken font mappings, or binary noise decoded as text.
-
Lost attachments. A container whose embedded documents stop being extracted after a change.
-
New exceptions, timeouts and out-of-memory crashes that only appear on a few files in a large corpus.
-
Detection drift. Files that changed media type or charset between two versions.
There is usually no ground truth: nobody has a hand-checked transcript of every document in a corpus of a million files. tika-eval therefore measures proxies that do not need one. The main proxy is the share of common words: real text in a language contains many of that language’s most frequent words, while mojibake and junk do not. Others are the rate of U+FFFD replacement characters, a learned text-quality score, the detected language, the amount of text per page and the exceptions recorded during the parse. Compared between two runs over the same files, these proxies become a regression test.
The Tika wiki lists the questions tika-eval was built for: comparing two different tools, two versions of the same tool ("should we upgrade?"), two runs with different settings (for example OCR at 200 against 300 DPI), and a tool against a gold standard. The project runs it against about three million files as part of its regression testing; the wiki also states that the design does not scale to hundreds of millions of files.
Source: TikaEval, Tika wiki (written in the 1.x to 2.x era, last updated 2022) and Apache Tika 4.1.x, Regression evaluation with tika-app + tika-eval.
The tika-eval artifacts in 4.1.0
| Artifact | Where | What it is |
|---|---|---|
|
Maven Central ( |
The parent POM of the modules below. In 1.x (1.15 to 1.28.5) |
|
Maven Central (jar) |
The metrics library: tokenization, common-token lists for 204 languages (as Bloom filters), CharSoup language id, the
|
|
Distribution zip |
The command-line tools |
|
Maven Central only (jar with a |
A standalone comparison of block order, segmentation and word boundaries between two sets of XHTML extracts. |
Like tika-app and tika-server-standard, tika-eval-app 4.x is a thin launcher: the jar on Maven Central has a
manifest class path that points at lib/ and fails on its own. Download the zip, check it and unpack it into a new
directory (the zip has no top-level folder):
curl -O https://downloads.apache.org/tika/4.1.0/tika-eval-app-4.1.0.zip
curl -O https://downloads.apache.org/tika/4.1.0/tika-eval-app-4.1.0.zip.sha512
shasum -a 512 tika-eval-app-4.1.0.zip # compare with the .sha512 file
unzip -q tika-eval-app-4.1.0.zip -d tika-eval
cd tika-eval
java -jar tika-eval-app-4.1.0.jar
Output:
Must specify one of the following tools in the first parameter:
Profile
Compare
Report
StartDB
The zip (41 MB) holds the launcher jar, LICENSE, NOTICE and 43 jars in lib/, among them tika-core,
tika-eval-core, tika-langdetect-charsoup, tika-ml-junkdetect, tika-encoding-detector-mojibuster,
tika-pipes-core, tika-pipes-file-system (the directory crawler), tika-serialization, h2 and poi-ooxml. It does
not contain parsers: tika-eval never parses your documents. It reads the extracts another tool wrote. The 3.x line ships
tika-eval-app-3.3.2.jar as a runnable fat jar instead.
Source: Apache Tika, Download and
Maven Central, org.apache.tika (artifact listings checked on
2026-10-13).
The evaluation flow
Every tika-eval run has the same three stages: write extracts, load them into a database, write reports from the database. Profile loads one set of extracts; Compare loads two sets of extracts of the same corpus and pairs them file by file.
Two consequences of this design matter in practice. First, tika-eval evaluates any tool that can write the extract
format, not only Tika: a directory of UTF-8 .txt files from another extractor can be one side of a comparison. Second,
the database survives the run (when you name it with -d), so you can write new reports or run your own queries long
after the extraction finished.
Writing the extracts
The extract format
tika-eval reads, for each original file, an extract with the same relative path and the suffix .json (the list of
metadata objects that RecursiveParserWrapper, /rmeta and tika-app -J produce, one object per document with the
text under tk:content) or .txt (plain UTF-8 text). Files can be at any depth; the crawler walks the tree. The JSON
form is far more useful: it carries the media type, the parse exceptions, the detected encoding, the embedded documents
and the parse time.
The fields tika-eval reads from a JSON extract include tk:content, Content-Type, tk:parsed-by,
tk:embedded-depth, tk:embedded-resource-path, tk:parse-time-millis, tk:detected-encoding,
tk:encoding-detector, the exception keys and tk:digest:SHA-256 (or MD5). Extracts written by 3.x or by a 4.0
pre-release use the old X-TIKA: and camelCase spellings; the 4.1.0 reader normalizes X-TIKA:content,
X-TIKA:embedded_depth, X-TIKA:parse_time_millis, X-TIKA:detectedEncoding, X-TIKA:digest:<alg> and the other
legacy keys to their tk: names before computing anything, so a 3.x run and a 4.x run can be compared directly. See
Metadata for the full key mapping.
Extracts with tika-app 4.1.0
In 4.x, tika-app -i <in> -o <out> runs Tika Pipes: it forks parser JVMs, writes one <file>.json per input and
survives crashes. The regression recipe needs a digester in both configurations, because tika-eval aligns embedded
documents across runs by their digest:
{
"parse-context": {
"commons-digester-factory": {
"digests": [{ "algorithm": "SHA256" }],
"skipContainerDocumentDigest": false
}
}
}
cd tika-app # the unpacked tika-app-4.1.0.zip
java -jar tika-app-4.1.0.jar -i ~/bookshelf/corpus -o ~/bookshelf/extracts-A \
-c ~/bookshelf/configs/tika-config-A.json -n 2
Last line of the output for the eight Bookshelf files:
INFO [main] 09:51:28,372 tika.pipes Successfully finished processing 8 files in 10276 ms
The extracts carry tk:digest:SHA-256 for every document (the config value is the enum name SHA256; the emitted key
uses the JCA spelling SHA-256). In pipes mode the default content handler of 4.1.0 is Markdown
(tk:content-handler-type is MARKDOWN); pass --handler t for plain text or --handler x for XHTML. Use the same
handler on both sides of a comparison, or every token that the Markdown markup adds (#, |) shows up as a difference.
Extracts with tika-app 3.3.x
The 3.x batch mode writes the same layout:
# 3.x only
java -jar tika-app-3.3.2.jar -J -t -i ~/bookshelf/corpus -o ~/bookshelf/extracts-332
-J selects the recursive JSON, -t plain text. In the run for this page the 3.3.2 batch mode rejected --digest=sha256 with
UnrecognizedOptionException; it wrote X-TIKA:digest:MD5 on its own, which tika-eval uses as the fallback digest.
Source: TikaEval, Tika wiki, Quick Start Usage (a 3.x-era
page; the tika-eval-X.Y.jar commands on it no longer match 4.x, see the CLI reference).
The Bookshelf corpus used on this page
The examples run on eight original files in ~/bookshelf/corpus: catalogue.pdf (one page),
manuscript.docx, publisher-email.eml (with samples.zip, a docx, a PDF, a PNG and a short cover-note.txt inside),
renamed-invoice.png (a PDF with a misleading name), legacy-catalogue.txt (431 bytes of Spanish in windows-1252, with
accents and two € signs), reseñas-fr.txt (French, UTF-8), reviews-es.html (Spanish, UTF-8 with a <meta charset>)
and empty.bin (zero bytes). This page uses its own, longer variant of legacy-catalogue.txt (431 bytes); the
Character Encoding Detection page uses a 234-byte one and
Testing Tika Integrations a 151-byte fixture, so their numbers
are not comparable with the ones here. The corpus is tiny on purpose, so every number can be checked by hand. Its counts are real,
but its parse times are noise; see Memory, performance and scaling.
Profile: one extraction run
Profile answers "what did this run produce?". It needs the extracts directory (-e) and a database name (-d); the
original files (-i) are optional but recommended, because with them tika-eval also records the files that produced no
extract at all.
cd tika-eval
java -jar tika-eval-app-4.1.0.jar Profile \
-i ~/bookshelf/corpus -e ~/bookshelf/extracts-A -d bookshelf-profile -n 2
java -jar tika-eval-app-4.1.0.jar Report -d bookshelf-profile -rd profile-reports
Profile creates bookshelf-profile.mv.db in the current directory and logs:
INFO ... org.apache.tika.eval.app.StatusReporter There are 0 file processors still active.
INFO ... org.apache.tika.eval.app.StatusReporter The crawler has enqueued 8 files.
INFO ... org.apache.tika.eval.app.StatusReporter Completed successfully.
Report then writes 16 workbooks:
| Directory | Workbooks |
|---|---|
|
|
|
|
|
|
Each workbook has the result on its first sheet and, because the report definitions set includeSql="true", the SQL
that produced it on a second sheet: a good way to learn the schema. A profile database gets no summary.md; the log
says Not a comparison database; skipping markdown summary.
The per-document numbers behind those workbooks, read straight from the database (the query is in Querying the H2 database):
FILE_PATH | IS_EMBEDDED | MIME_STRING | NUM_TOKENS | NUM_COMMON_TOKENS | OOV | LNG | LANG_ID_1
catalogue.pdf | FALSE | application/pdf | 10 | 9 | 0.1 | 2.37 | eng
empty.bin | FALSE | application/octet-stream | null | null | null | null | null
legacy-catalogue.txt | FALSE | text/plain; charset=windows-1252 | 67 | 44 | 0.323 | 2.19 | spa
manuscript.docx | FALSE | application/vnd.openxmlformats-... | 11 | 10 | 0.091 | 2.2 | eng
publisher-email.eml | FALSE | message/rfc822 | 15 | 14 | 0.067 | 2.39 | eng
publisher-email.eml | TRUE | image/png | null | null | null | null | null
publisher-email.eml | TRUE | application/vnd.openxmlformats-... | 20 | 14 | 0.222 | 1.68 | eng
publisher-email.eml | TRUE | application/pdf | 10 | 9 | 0.1 | 2.37 | eng
publisher-email.eml | TRUE | text/plain; charset=windows-1252 | 19 | 17 | 0.105 | 2.41 | eng
publisher-email.eml | TRUE | application/zip | 9 | 7 | 0.222 | 1.03 | eng
renamed-invoice.png | FALSE | application/pdf | 10 | 9 | 0.1 | 2.37 | eng
reseñas-fr.txt | FALSE | text/plain; charset=UTF-8 | 48 | 35 | 0.271 | 2.17 | fra
reviews-es.html | FALSE | text/html; charset=UTF-8 | 40 | 30 | 0.25 | 2.33 | spa
(The long Office media type is shortened here.) Thirteen documents from eight files: the e-mail contributes its five
embedded documents as their own rows, each with its own metrics. empty.bin has a profile row but no content row, and
an exception row with type RUNTIME (the ZeroByteFileException the parse recorded). Language ids are ISO 639-3
codes from CharSoup: spa, fra, eng.
Source: TikaEval, Tika wiki, Single Output from One Tool (Profile);
command forms from the ExtractProfileRunner options in the tika-eval-app 4.1.0 source jar.
The metrics
tika-eval computes the same text profile for every document in Profile and for each side in Compare, then adds the
pairwise contrast in Compare. The column names are those of the contents, profiles and content_comparisons tables
(contents_a and contents_b in a comparison database).
Text profile (table contents)
| Column | Meaning |
|---|---|
|
Tokens after the tika-eval analyzer (lower-casing, ICU folding of accents, so |
|
Tokens with at least one alphabetic or ideographic character; numbers and punctuation do not count. |
|
Alphabetic tokens found in the common-token list of the detected language ( |
|
Out-of-vocabulary rate, \( 1 - \frac{\text{common tokens}}{\text{alphabetic tokens}} \). Lower is better. Real prose sits roughly between 0.1 and 0.4 on this corpus; junk tends towards 1. |
|
The |
|
The two most likely languages and their scores, from the CharSoup detector in 4.x (OpenNLP in 3.x). |
|
Count of U+FFFD replacement characters and of non-ASCII characters. Their ratio is the cleanest decode-failure signal. |
|
Characters of text, and whether the text was cut at the configured maximum before the statistics were computed. |
|
Token-distribution statistics. Very short mean token lengths or a very low entropy rate point at broken text (single letters, repeated characters). |
|
The most frequent tokens with their counts, and the Unicode blocks the text uses with their counts. |
The profiles table adds one row per document with file_name, mime_id, length, is_embedded, embedded_depth,
embedded_file_path, attachment_type, elapsed_time_millis, num_attachments, num_metadata_values, num_pages,
num_ocr_pages and has_content. The encodings table records detected_encoding, encoding_detector and the
declared_metadata charset.
Contrast between A and B (table content_comparisons)
Compare tokenizes both texts of a pair and computes:
Both are 1.0 for identical token sets and fall towards 0 as the texts diverge. Dice looks at the vocabulary, overlap at the
counts, so a document whose text is duplicated in B keeps a dice of 1.0 but its overlap drops. The table also stores the
top ten tokens unique to each side (top_10_unique_token_diffs_a, _b) and the top ten tokens that are more frequent on
each side (top_10_more_in_a, _b); these lists are often the fastest way to see what changed.
Exceptions and failures
| Reference table | Values |
|---|---|
|
|
|
|
|
|
The stack traces are stored twice: as recorded (orig_stack_trace) and normalized for grouping (sort_stack_trace),
which is what lets the reports count "the same bug" across thousands of files.
Source: the TokenContraster, CommonTokenResult and TikaEvalMetadataFilter classes and the common_tokens_bloom
resources in the tika-eval-core 4.1.0 source jar; table and column names read from a 4.1.0 database with
information_schema.columns; TikaEval, Tika wiki, Evaluating Success via Common Words.
Compare: regression between two configurations
This is the official worked example of the 4.1.x documentation, applied to the Bookshelf corpus. The documentation compares the default encoding-detector chain against an explicit chain over 29,000 HTML pages from Common Crawl. Here the variant is deliberately bad, so that the reports have something to find: configuration B replaces the detector chain with a BOM check and an override that always says UTF-8. The two configurations differ in exactly one thing, the field under test; the digester is identical.
tika-config-A.json is the digester-only file shown above. tika-config-B.json:
{
"encoding-detectors": [
{ "bom-detector": {} },
{ "override-encoding-detector": { "charset": "UTF-8" } }
],
"parse-context": {
"commons-digester-factory": {
"digests": [{ "algorithm": "SHA256" }],
"skipContainerDocumentDigest": false
}
}
}
An explicit encoding-detectors array replaces the SPI-discovered chain completely; see
Character Encoding Detection for the detector names.
cd tika-app
java -jar tika-app-4.1.0.jar -i ~/bookshelf/corpus -o ~/bookshelf/extracts-A -c ~/bookshelf/configs/tika-config-A.json -n 2
java -jar tika-app-4.1.0.jar -i ~/bookshelf/corpus -o ~/bookshelf/extracts-B -c ~/bookshelf/configs/tika-config-B.json -n 2
cd ../tika-eval
java -jar tika-eval-app-4.1.0.jar -n 2 -r -d bookshelf-A-vs-B \
-i ~/bookshelf/corpus -a ~/bookshelf/extracts-A -b ~/bookshelf/extracts-B
The Compare keyword is optional: with both -a and -b the CLI infers it. -r runs Report right after Compare and
packs the reports directory into reports.tgz; -d keeps the database as bookshelf-A-vs-B.mv.db (94 KB here). The whole
comparison took 11 seconds of wall time, most of it JVM start-up and the 67 report queries.
The quickest overview is reports/summary.md, new in 4.x. Extracts from the run for this page (empty sections and the long paths of the
provenance tables removed):
## Overview
- **Total containers**: 8
- **Total files (A)**: 13
- **Total files (B)**: 13
- **Exceptions (A)**: 1
- **Exceptions (B)**: 1
### Top 20 Individual OOV Increases
| FILE | MIME_A | OOV_A | OOV_B | OOV_DELTA | LANG_A | LANG_B | FFFD_A | FFFD_B | LANG_ID_A | LANG_ID_B |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| legacy-catalogue.txt | text/plain; charset=windows-1252 | 0.3231 | 0.5641 | 0.241 | 2.19 | 0.96 | 0 | 16 | spa | spa |
## Token Count Changes by Mime Type
| MIME_A | MIME_B | TOKENS_A | TOKENS_B | PCT_CHANGE | COMMON_A | COMMON_B |
| --- | --- | --- | --- | --- | --- | --- |
| text/plain; charset=windows-1252 | text/plain; charset=UTF-8 | 86 | 99 | 15.1 | 61 | 51 |
| application/pdf | application/pdf | 30 | 30 | 0.0 | 27 | 27 |
## Mime Type Changes (A -> B)
| CHANGE | COUNT |
| --- | --- |
| text/plain; charset=windows-1252 -> text/plain; charset=UTF-8 | 2 |
## Top 20 Content Regressions (lowest dice)
| FILE | MIME_A | DICE | OVERLAP | ONLY_IN_A | ONLY_IN_B |
| --- | --- | --- | --- | --- | --- |
| legacy-catalogue.txt | text/plain; charset=windows-1252 | 0.6607 | 0.7211 | catalogo: 1 \| edicion: 1 \| electronico: 1 \| ... | a: 3 \| env: 2 \| an: 1 \| ... |
Every signal points at the same file, and each one tells a different part of the story:
-
U+FFFD. B decoded the 16 non-ASCII bytes of
legacy-catalogue.txtas UTF-8 and got 16 replacement characters (FFFD_B); A had none. -
OOV and common tokens. The broken words (
Cat�logosplits intocatandlogo) are not common Spanish words, so OOV rose from 0.32 to 0.56 and its common tokens fell from 44 to 34, while its raw token count rose from 67 to 80 (about 19 %) because every broken word became two tokens. (The 61 to 51 and 15 % figures in the summary are totals for the media type, which also include the unchangedcover-note.txt.) More tokens is not better text. -
Languageness fell from 2.19 to 0.96. It stayed positive: most of the file is still clean ASCII Spanish.
-
Language id stayed
spa: the detector is robust to a few broken words, so a stable language is not proof of a clean decode. -
Dice and the token lists show the cause in words:
catalogo,edicion,electronicoonly in A; fragments such asenv,contemporandelectronly in B. -
Media type. Two documents changed from
charset=windows-1252tocharset=UTF-8: the file itself and the embeddedcover-note.txtin the e-mail. The second one is pure ASCII, so its text is identical (dice 1.0). A charset flip is only a regression when the text changed with it, which is why thecharset/reports join the flips to the text metrics.
The same summary also lists a parse-time table. Ignore it on a corpus this small; see Memory, performance and scaling.
The comparison reports
A comparison database gets 67 workbooks plus summary.md in 4.1.0:
| Directory | What to look at first |
|---|---|
|
|
|
|
|
|
|
|
|
Embedded documents and containers missing on either side, attachment-count differences with and without exceptions,
and |
|
|
|
|
The 4.1.x documentation also lists an md5/ directory; the 4.1.0 run for this page produced none, so do not rely on it.
Report -d <db> -rd <dir> re-runs the reports on an existing database without repeating Compare. Report overwrites an
existing reports directory without warning.
Source: Regression evaluation, Step 5: read the reports
and the report definitions comparison-reports.xml and profile-reports.xml in the tika-eval-app 4.1.0 jar.
Before you upgrade Tika: 3.3.2 against 4.1.0
The question tika-eval was first built for is "should we upgrade?". The recipe is the same as above, with the version as the variable instead of the configuration. Use the same corpus, the same output form (plain text on both sides here, since 3.x has no Markdown handler) and, where possible, the same digests and timeouts.
# A: the version you run today (3.x batch mode)
java -jar tika-app-3.3.2.jar -J -t -i ~/bookshelf/corpus -o ~/bookshelf/extracts-332
# B: the candidate (4.x pipes mode), plain text to match A
cd tika-app
java -jar tika-app-4.1.0.jar -i ~/bookshelf/corpus -o ~/bookshelf/extracts-410 \
-c ~/bookshelf/configs/tika-config-A.json --handler t -n 2
cd ../tika-eval
java -jar tika-eval-app-4.1.0.jar Compare -n 2 -d tika-332-vs-410 -r -rd upgrade-reports \
-i ~/bookshelf/corpus -a ~/bookshelf/extracts-332 -b ~/bookshelf/extracts-410
Thanks to the legacy-key normalization, the 3.3.2 extracts (X-TIKA:content, X-TIKA:digest:MD5) and the 4.1.0
extracts (tk:content, tk:digest:SHA-256) were paired without any preprocessing. What the comparison found:
## Mime Type Changes (A -> B)
| CHANGE | COUNT |
| --- | --- |
| text/plain; charset=ISO-8859-1 -> text/plain; charset=windows-1252 | 2 |
A query for the two flipped documents (the SQL is in the next section) shows that the change is a fix:
FILE_NAME | FFFD_A | FFFD_B | OOV_A | OOV_B | LANG_A | LANG_B | ENC_A | ENC_B
legacy-catalogue.txt | 2 | 0 | 0.323 | 0.323 | 2.0 | 2.14 | ISO-8859-1 | windows-1252
cover-note.txt | 0 | 0 | 0.105 | 0.105 | 2.21 | 2.14 | ISO-8859-1 | windows-1252
3.3.2 decoded legacy-catalogue.txt as ISO-8859-1. The decoder itself does not fail: it maps byte 0x80 of each €
sign to the C1 control character U+0080. Tika’s XHTMLContentHandler treats U+007F to U+009F as invalid in XHTML and
writes U+FFFD in their place, so the two € signs came out as replacement characters (checked by decoding the file
with AutoDetectReader, which returns U+0080, and with tika-app-3.3.2.jar -t, which prints U+FFFD). 4.1.0 detected windows-1252 and decoded them correctly. Note what did not
move: OOV, the token counts and the dice coefficient (1.0) are identical, because € is not a word token. Token metrics
are blind to symbols, currency signs and punctuation; the replacement-character count and the charset reports are not.
Read them together.
Three more observations from the same run, each of them a lesson for real upgrades:
-
Metadata value counts went up for every document (for example 47 to 57 for
catalogue.pdf), because 4.x writes moretk:keys (tk:encoding-detection-trace,tk:content-type-magic-detectedand others). Themetadata_value_count_diffs.xlsxreport flags all of them; it is a key-set change, not a content change. Check the keys your application reads against the mapping table on the Metadata page. -
All 13 documents paired, with no attachments missing on either side: the embedded-document handling of the e-mail is unchanged.
-
Parse times differed by factors of two to twenty in both directions. This is a single run of eight tiny files on two very different execution models (3.x in-process batch, 4.x forked pipes with cold JVMs), and it means nothing.
A practical pre-upgrade checklist:
-
Build a corpus that represents production: every media type you accept, the legacy charsets you receive, scans, password-protected files, the largest files, and the files that caused incidents before. A few thousand files are enough for most decisions; tika-eval comfortably handles far more.
-
Extract with the current version and the candidate using the same output form, digests, timeouts and fork count.
-
Run Compare with
-d(keep the database) and-r. -
Read, in order: new catastrophic exceptions and crashes, content lost, new exceptions by media type, media-type and charset flips, OOV and languageness decreases, dice quintiles, attachments missing.
-
For every surprise, open the two extracts side by side and decide: regression, fix or harmless change.
-
Keep the reports and the database next to the upgrade ticket, and repeat the run on the next release.
Source: TikaEval, Tika wiki, Background (the upgrade use
case) and the ExtractReader legacy-key table in the tika-eval-app 4.1.0 source jar. See
What’s Changed & Migration for what else to check when you
move from 3.x to 4.x.
Querying the H2 database
The reports are pre-canned views. For anything else, query the database directly. The rules, all confirmed in the runs for this page:
-
The database has no user name and no password. Pass empty credentials;
-user safails with "Wrong user name or password". -
Leave the
.mv.dbsuffix out of the JDBC URL:jdbc:h2:./bookshelf-A-vs-B. -
Append
;IFEXISTS=TRUE(a typo then fails instead of creating a new, empty database) and;ACCESS_MODE_DATA=rfor a read-only open. -
H2 is single-writer: query only after Profile, Compare or Report has finished.
-
Use the
h2-2.5.250.jarshipped in thelib/directory oftika-eval-app, so the file format matches. -
Pass SQL to
-sqlas a single line; a multi-line argument leaves the shell waiting on standard input.
H2=lib/h2-2.5.250.jar
DB='jdbc:h2:./bookshelf-A-vs-B;IFEXISTS=TRUE;ACCESS_MODE_DATA=r'
java -cp "$H2" org.h2.tools.Shell -url "$DB" -user '' -password '' -sql "SHOW TABLES"
A comparison database has 29 tables: containers, profiles_a/_b, contents_a/_b, content_comparisons,
encodings_a/_b, exceptions_a/_b, exceptions_compared, extract_exceptions_a/_b, emb_path_a/_b,
tags_a/_b, mimes, pair_names, parse_time_compared, token_counts_compared, pipes_class_a/_b,
run_info/_a/_b and the three reference tables. A profile database has the same tables without the suffixes (14 in
all). A and B are paired by id: the same id is the same document in both runs, and every join between the two sides
uses it.
The OOV direction count from the official documentation, on the encoding comparison:
SELECT SUM(CASE WHEN cb.oov < ca.oov THEN 1 ELSE 0 END) AS oov_better,
SUM(CASE WHEN cb.oov > ca.oov THEN 1 ELSE 0 END) AS oov_worse
FROM contents_a ca JOIN contents_b cb ON ca.id = cb.id;
OOV_BETTER | OOV_WORSE
0 | 1
The documents whose text changed, or that gained replacement characters, with all the signals side by side:
SELECT pa.file_name, ROUND(ca.oov,3) oov_a, ROUND(cb.oov,3) oov_b,
ROUND(ca.languageness,2) lang_a, ROUND(cb.languageness,2) lang_b,
ca.num_replacement fffd_a, cb.num_replacement fffd_b, cb.num_non_ascii,
ROUND(cc.dice_coefficient,3) dice
FROM profiles_a pa
JOIN contents_a ca ON ca.id = pa.id
JOIN contents_b cb ON cb.id = pa.id
JOIN content_comparisons cc ON cc.id = pa.id
WHERE cc.dice_coefficient < 1.0 OR cb.num_replacement > 0;
FILE_NAME | OOV_A | OOV_B | LANG_A | LANG_B | FFFD_A | FFFD_B | NUM_NON_ASCII | DICE
legacy-catalogue.txt | 0.323 | 0.564 | 2.19 | 0.96 | 0 | 16 | 16 | 0.661
Sixteen replacement characters out of sixteen non-ASCII characters: a 100 % decode-failure rate for the non-ASCII part.
Divide by num_non_ascii, not by the content length, which dilutes the rate to almost zero on mostly-ASCII text.
The charset flips with the detector that made each decision:
SELECT pa.file_name, ea.detected_encoding enc_a, ea.encoding_detector det_a,
eb.detected_encoding enc_b, eb.encoding_detector det_b
FROM profiles_a pa
JOIN encodings_a ea ON ea.id = pa.id
JOIN encodings_b eb ON eb.id = pa.id
WHERE ea.detected_encoding <> eb.detected_encoding;
FILE_NAME | ENC_A | DET_A | ENC_B | DET_B
legacy-catalogue.txt | windows-1252 | MojibusterEncodingDetector | UTF-8 | OverrideEncodingDetector
cover-note.txt | windows-1252 | MojibusterEncodingDetector | UTF-8 | OverrideEncodingDetector
The common-token headline ("did B recover more real text?"):
SELECT SUM(ca.num_common_tokens) AS common_a, SUM(cb.num_common_tokens) AS common_b,
SUM(cb.num_common_tokens) - SUM(ca.num_common_tokens) AS delta
FROM contents_a ca JOIN contents_b cb ON ca.id = cb.id;
COMMON_A | COMMON_B | DELTA
198 | 188 | -10
And, on the profile database, the per-document listing shown in the Profile section:
SELECT c.file_path, p.is_embedded, m.mime_string, ct.num_tokens, ct.num_common_tokens,
ROUND(ct.oov,3) oov, ROUND(ct.languageness,2) lng, ct.lang_id_1
FROM profiles p
JOIN containers c ON p.container_id = c.container_id
JOIN mimes m ON p.mime_id = m.mime_id
LEFT JOIN contents ct ON ct.id = p.id
ORDER BY c.file_path, p.id;
The documentation adds a caution worth repeating: OOV is one signal, not the verdict. A language-id shift (for example a CJK text that B decodes correctly for the first time) can raise OOV although B is right, and a wrong decode that fragments words into short common-looking tokens can lower it. Languageness has the opposite blind spots. When the two disagree on a file, look at the file.
StartDB starts the H2 web console (org.h2.tools.Console -web, by default on http://localhost:8082) for browsing a
database in a browser; enter the JDBC URL with the full path and empty credentials. It runs until you stop it.
Custom reports
A report file is an XML list of SQL queries, each written to its own workbook; the built-in profile-reports.xml and
comparison-reports.xml inside the tika-eval-app jar are the best examples (with optional <before> and <after> SQL
blocks that build and drop helper tables). A Bookshelf report that lists suspicious decodings in B:
<?xml version="1.0" encoding="UTF-8"?>
<reports>
<report reportName="Suspect decodings in B"
reportFilename="bookshelf/suspect_decodings_b.xlsx"
format="xlsx"
includeSql="true">
<sql>
select pa.file_name, ea.detected_encoding enc_a, eb.detected_encoding enc_b,
cb.num_replacement fffd_b, ca.oov oov_a, cb.oov oov_b,
ca.languageness lang_a, cb.languageness lang_b
from profiles_a pa
join contents_a ca on ca.id = pa.id
join contents_b cb on cb.id = pa.id
left join encodings_a ea on ea.id = pa.id
left join encodings_b eb on eb.id = pa.id
where cb.num_replacement > ca.num_replacement
or cb.languageness < ca.languageness - 0.5
order by cb.num_replacement desc
</sql>
</report>
</reports>
java -jar tika-eval-app-4.1.0.jar Report -d bookshelf-A-vs-B -rf bookshelf-reports.xml -rd custom-reports
The run wrote custom-reports/bookshelf/suspect_decodings_b.xlsx with one row (legacy-catalogue.txt,
windows-1252, UTF-8, 16, 0.32, 0.56, 2.19, 0.96) and the SQL on its second sheet. Escape < as < inside the XML.
On a comparison database Report writes summary.md as well, whichever report file you pass.
Source: TikaEval, Tika wiki, Reports.
The crash ledger and run provenance
A crash (OOM, TIMEOUT, UNSPECIFIED_CRASH) leaves no extract, so on its own tika-eval reports it as
NO_EXTRACT_FILE, the same as a file that never ran. Since 4.1.0 two inputs close the gap:
-
the per-document JSONL that the
file-system-jsonl-reporterof Tika Pipes writes during the batch (-pron Profile,-paand-pbon Compare); -
a
run-info.jsonthat describes what ran (-rion Profile,-raand-rbon Compare).
When these flags are absent, tika-eval looks in <extracts>/.run-info/ for crashes-*.jsonl and run-info-*.json
(it refuses to start if that directory holds more than one of either, since that means two runs wrote into the same
extracts directory). The .run-info directory is skipped by the crawler and excluded from the extracts fingerprint. A
reporter block that writes the ledger where tika-eval looks for it, with successes left out:
{
"pipes-reporters": {
"file-system-jsonl-reporter": {
"path": "/home/bookshelf/extracts-L/.run-info/crashes-bookshelf.jsonl",
"onExists": "REPLACE",
"excludes": ["PARSE_SUCCESS"]
}
},
"parse-context": {
"commons-digester-factory": {
"digests": [{ "algorithm": "SHA256" }],
"skipContainerDocumentDigest": false
},
"timeout-limits": { "totalTaskTimeoutMillis": 60000, "progressTimeoutMillis": 30000 }
}
}
On the Bookshelf corpus nothing crashed, so the ledger held a single line, for the zero-byte file:
{"id":"empty.bin","status":"PARSE_SUCCESS_WITH_EXCEPTION","category":"SUCCESS","message":null,"elapsedMs":10116,"timestamp":"2026-10-13T07:59:23.292358Z"}
Profile found and joined it without any flag (loaded 1 pipes report rows (0 pipeline error lines) in the log), and
the result lands in containers.pipes_status and pipes_message (_a and _b on Compare), and in run_info as
pipes_report.rows = 1 and pipes_report.joined = 1. A joined of zero with a non-empty ledger means the wrong ledger,
or a crawl without -i, which never sees the files that crashed. The reports classify each container:
| Classification | Meaning |
|---|---|
|
|
any other status |
Passed through as recorded ( |
|
A ledger was supplied but has no line for this file: the batch recorded no failure for it. |
|
A run-info was supplied but the batch wrote no ledger. |
|
Neither was given. |
The run_info tables also record, without being asked, the tika-eval version, the SHA-256 of its jar, Java, host, user,
the arguments, the effective EvalConfig, start and end times, and for each extracts directory the file count and a
fingerprint (SHA-256 over the sorted names and sizes). summary.md opens with them, so a report always says what produced
it. Password-like values in the JDBC string, arguments and configuration are masked, since these tables travel with the
reports.
The Tika source tree has a helper, .skills/devs/tika-eval-compare/run-batch.sh, that runs tika-app and writes both
files (run-info-<run.id>.json with the SHA-256 of every jar and of the configuration, and the ledger). It is not part of
the distribution zips and was not run for this page. If a run-info.json carries run.id, the ledger must be named
crashes-<run.id>.jsonl or tika-eval refuses to start.
Source: Regression evaluation, Joining the batch crash ledger and run provenance and Apache Tika 4.1.x, Pipes Reporters; reporter keys on the Tika Pipes page.
Text-quality scores at parse time: TikaEvalMetadataFilter
You do not need the whole tika-eval-app to use its metrics. TikaEvalMetadataFilter (package
org.apache.tika.eval.core.metadata, artifact tika-eval-core) is a metadata filter: it reads tk:content from a
Metadata object and writes the scores next to it. That turns quality into a per-document field your pipeline can act on
before the text reaches an index or an embedding model.
| Key | Type | Meaning |
|---|---|---|
|
integer |
All tokens. |
|
integer |
Distinct tokens. |
|
integer |
Tokens with a letter or ideograph. |
|
integer |
Distinct alphabetic tokens. |
|
integer |
Alphabetic tokens in the common-token list of the detected language. |
|
real |
Out-of-vocabulary rate; |
|
text, real |
Top language and its raw score from CharSoup. |
|
real |
The |
The filter does nothing when tk:content is blank. The 3.x filter writes the same keys except languageness, and uses
OpenNLP for the language.
From Java
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-bom</artifactId>
<version>4.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-eval-core</artifactId>
</dependency>
</dependencies>
dependencies {
implementation(platform("org.apache.tika:tika-bom:4.1.0"))
implementation("org.apache.tika:tika-parsers-standard-package")
implementation("org.apache.tika:tika-eval-core")
}
A quality gate for the Bookshelf intake: parse each file with RecursiveParserWrapper, score the container text and
flag the documents a human should look at. The thresholds are illustrations chosen for this corpus; set yours from a
Profile of your own documents.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import org.apache.tika.eval.core.metadata.TikaEvalMetadataFilter;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.RecursiveParserWrapper;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.RecursiveParserWrapperHandler;
import org.xml.sax.SAXException;
public class QualityGate {
// Thresholds for this corpus; tune them on your own documents.
private static final double MAX_OOV = 0.50;
private static final double MIN_LANGUAGENESS = 1.0;
public static void main(String[] args) throws IOException {
RecursiveParserWrapper wrapper = new RecursiveParserWrapper(new AutoDetectParser());
TikaEvalMetadataFilter quality = new TikaEvalMetadataFilter();
try (Stream<Path> files = Files.list(Path.of(args[0]))) {
for (Path file : files.sorted().toList()) {
RecursiveParserWrapperHandler handler = new RecursiveParserWrapperHandler(
new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));
try (TikaInputStream tis = TikaInputStream.get(file)) {
wrapper.parse(tis, handler, new Metadata(), new ParseContext());
} catch (SAXException | TikaException e) {
// RecursiveParserWrapper records the exception in the container metadata
}
List<Metadata> list = handler.getMetadataList();
Metadata container = list.get(0);
quality.filter(container); // reads tk:content, writes tika-eval:* keys
report(file.getFileName().toString(), container);
}
}
}
private static void report(String name, Metadata m) {
Double oov = m.getDouble(TikaEvalMetadataFilter.OUT_OF_VOCABULARY);
Double languageness = m.getDouble(TikaEvalMetadataFilter.LANGUAGENESS);
String verdict;
if (oov == null) {
verdict = "NO_TEXT";
} else if (oov > MAX_OOV || languageness < MIN_LANGUAGENESS) {
verdict = "REVIEW";
} else {
verdict = "OK";
}
System.out.printf(Locale.ROOT, "%-29s tokens=%-4s common=%-4s oov=%-6s lang-ness=%-6s lang=%-4s %s%n",
name,
orDash(m.get(TikaEvalMetadataFilter.NUM_TOKENS)),
orDash(m.get(TikaEvalMetadataFilter.NUM_COMMON_TOKENS)),
fmt(oov), fmt(languageness),
orDash(m.get(TikaEvalMetadataFilter.LANGUAGE)),
verdict);
}
private static String orDash(String s) {
return s == null ? "-" : s;
}
private static String fmt(Double d) {
return d == null ? "-" : String.format(Locale.ROOT, "%.3f", d);
}
}
For this run the corpus directory also held two deliberately bad files: double-encoded-catalogue.txt (the catalogue
encoded to UTF-8, wrongly read as windows-1252 and saved again, the classic Catálogo mojibake) and
scanned-loan-form-ocr.txt (simulated junk OCR output). Output on Tika 4.1.0:
catalogue.pdf tokens=10 common=9 oov=0.100 lang-ness=1.989 lang=eng OK
double-encoded-catalogue.txt tokens=84 common=35 oov=0.563 lang-ness=0.793 lang=spa REVIEW
empty.bin tokens=- common=- oov=- lang-ness=- lang=- NO_TEXT
legacy-catalogue.txt tokens=67 common=44 oov=0.323 lang-ness=2.157 lang=spa OK
manuscript.docx tokens=11 common=10 oov=0.091 lang-ness=2.087 lang=eng OK
publisher-email.eml tokens=15 common=14 oov=0.067 lang-ness=2.176 lang=eng OK
renamed-invoice.png tokens=10 common=9 oov=0.100 lang-ness=1.989 lang=eng OK
reseñas-fr.txt tokens=48 common=35 oov=0.271 lang-ness=2.122 lang=fra OK
reviews-es.html tokens=40 common=30 oov=0.250 lang-ness=2.303 lang=spa OK
scanned-loan-form-ocr.txt tokens=108 common=0 oov=1.000 lang-ness=-1.713 lang=diq REVIEW
The junk OCR is unmistakable: no common tokens at all, OOV 1.0, a negative languageness and a nonsense language
(diq, Dimli). The double-encoded file is subtler: its languageness stays positive (0.79), and only the OOV jump
from 0.32 for the correct decoding to 0.56 catches it, with this threshold. That is the general lesson: no single score
is a reliable gate on its own, and thresholds must come from your data. The scores are not identical to those of the tika-eval-app
runs above (languageness 2.157 here, 2.19 in the Markdown Profile and 2.14 in the plain-text upgrade run for
legacy-catalogue.txt; 1.989 against 2.37 for catalogue.pdf): the extracted text and the scoring path differ between
the two tools, so compare scores only within one tool and one output form. The example scores only the container; for the e-mail, call filter on every element of the list
to score each attachment.
TikaEvalMetadataFilter is thread-safe in practice for this use (its calculators and the JunkDetector are static and
shared); create one instance and reuse it.
Source: TikaEvalMetadataFilter, Javadoc 4.1.0,
Apache Tika 4.1.x, Metadata Filters and
TikaEval, Tika wiki (which points to the filter for "many of
these statistics at parse time").
From tika-config.json: not in 4.1.0
The 4.1.x documentation lists the filter as tika-eval-metadata-filter for the metadata-filters array, and the
tika-app 4.1.0 distribution bundles tika-eval-core. In the run for this page this configuration failed at start-up:
{
"metadata-filters": [
{ "tika-eval-metadata-filter": {} }
]
}
Exception in thread "main" org.apache.tika.exception.TikaConfigException: Failed to load MetadataFilter: tika-eval-metadata-filter
...
Caused by: java.lang.ClassNotFoundException: Component 'tika-eval-metadata-filter' is not registered. ...
The cause is in the 4.1.0 jar: tika-eval-core-4.1.0.jar has no META-INF/tika/metadata-filters.idx component index
(the class carries the @TikaComponent annotation, but the index was not generated), while, for comparison,
tika-langdetect-charsoup-4.1.0.jar registers charsoup-metadata-filter that way. The 4.x loader refuses class names that
are not registered, so there is no workaround in JSON. Until a later release fixes the index, apply the filter in Java as
above, or run Profile on the extracts. Re-test this when you upgrade.
In 3.x the filter is configured by class name in tika-config.xml:
<!-- 3.x only -->
<properties>
<metadataFilters>
<metadataFilter class="org.apache.tika.eval.core.metadata.TikaEvalMetadataFilter"/>
</metadataFilters>
</properties>
The 3.x XML form was taken from the 3.x configuration conventions and was not run for this page.
Using the scores downstream
-
Search. Index
tika-eval:oovandtika-eval:languagenessas numeric fields, so you can find and re-process junk later, and keep junk out of the default result set. See Search Indexing with Lucene, Solr & Elasticsearch. -
RAG. Gate chunks before embedding them: junk text wastes tokens and poisons retrieval. See RAG & LLM Ingestion.
-
OCR. A scanned PDF with a negative languageness is a candidate for re-OCR at a higher DPI or with another engine; see OCR with Tesseract.
-
Charsets. For a direct comparison of candidate decodings, use
JunkDetector.compareas shown on Character Encoding Detection, which also documents the score’s limits between close Latin charsets. -
Tests. Turn a small Profile or the filter into an assertion in CI; see Testing Tika Integrations.
Structure comparison: tika-eval-structure
Token metrics cannot see order. A parser change that reorders the columns of a PDF, splits a paragraph or glues two words
keeps dice near 1.0. tika-eval-structure (new in 4.1.0, Maven Central only) compares two directories of XHTML extracts
block by block: it matches paragraphs, headings, list items and cells one to one by token overlap and reports Kendall
tau and adjacent-pair agreement for the order, splits and merges, and words glued on one side and split on the other. Its
summary groups files by a metadata key of A (-g, default pdf:producer).
The jar expects its dependencies in lib/ next to it; the lib/ directory of tika-eval-app 4.1.0 contains all of them
(tika-core, tika-serialization, commons-cli, Jackson, log4j), which is how it was run here:
cd tika-app
java -jar tika-app-4.1.0.jar -i ~/bookshelf/corpus -o ~/bookshelf/xhtml-A -c ~/bookshelf/configs/tika-config-A.json --handler x -n 2
java -jar tika-app-4.1.0.jar -i ~/bookshelf/corpus -o ~/bookshelf/xhtml-B -c ~/bookshelf/configs/tika-config-B.json --handler x -n 2
cd ../tika-eval # tika-eval-structure-4.1.0.jar copied here, next to lib/
java -jar tika-eval-structure-4.1.0.jar -a ~/bookshelf/xhtml-A -b ~/bookshelf/xhtml-B -o structure-report
# Structure: xhtml-A (A) vs xhtml-B (B)
Files: 7; scored for order (matched token share >= 0.500): 7
...
Missing in B: 0; unreadable or not XHTML: 1
As expected for a charset-only change, every file kept a tau of 1.000. The unreadable file is empty.bin, whose extract
has no XHTML. The options are -a, -b, -o, -g, -d (the least token dice for two blocks to match, default 0.5) and
-s (a suffix to drop from B names, for comparing against labels made from a source document). Use the tool when the
variable under test is a PDF parser, layout or reading-order setting; see PDF Parsing.
Source: Regression evaluation, Structure: order, segmentation and word boundaries
and the --help output of tika-eval-structure-4.1.0.jar.
CLI reference (tika-eval-app 4.1.0)
The first argument names the tool. When it is a flag instead, the tool is inferred: -a with -b means Compare, -e
means Profile, -d alone means Report.
| Tool | Purpose |
|---|---|
|
Load one extracts directory into a database. |
|
Load two extracts directories of the same corpus and pair them. |
|
Write the |
|
Start the H2 web console. |
| Option | Tools | Meaning |
|---|---|---|
|
Profile |
The extracts directory (required). |
|
Compare |
The two extracts directories (required). |
|
Profile, Compare |
The original files. Without it the extracts directory is crawled, and files without an extract are invisible. |
|
all |
H2 database name or path, without |
|
Profile, Compare |
tika-eval JSON configuration (see below). |
|
Profile, Compare |
Worker threads (default 4). |
|
Profile, Compare |
Skip extracts longer than this many bytes (default 100,000,000). |
|
Compare |
Run Report afterwards and pack the reports into |
|
Compare, Report |
Reports directory (default |
|
Compare |
Gzip the database to |
|
Profile; Compare |
Crash ledger JSONL. |
|
Profile; Compare |
Run-info JSON. |
|
Report |
XML file of custom reports. |
|
Report |
Expert options for another database. |
The -c file maps onto EvalConfig. Its keys are minExtractLength (default 0), maxExtractLength (100,000,000),
numWorkers (4) and jdbcDriverClass; the class also declares jdbcString, forceDrop, maxFilesToAdd, maxTokens
and maxContentLength, but in the 4.1.0 source nothing reads them. Command-line -n and -m override the file:
{ "minExtractLength": 10, "maxExtractLength": 20000000, "numWorkers": 2 }
The run with this file recorded EvalConfig{minExtractLength=10, maxExtractLength=20000000, ..., numThreads=2, ...} in
run_info.
Options of the 3.x tools that no longer exist in 4.1.0: -drop, -alterExtract (as_is, first_only,
concatenate_content), -minExtractLength as a flag, -maxTokens, -maxContentLength, -maxContentLengthForLangId,
-defaultLangCode, -bc and -numConsumers (tika-batch), -tablePrefixA/-tablePrefixB on Compare, and the
FileProfile tool. See Pitfalls for what happens if you still type them.
Source: the option definitions of TikaEvalCLI, ExtractProfileRunner, ExtractComparerRunner and ResultsReporter
in the tika-eval-app 4.1.0 and 3.3.2 source jars on Maven Central; the Report usage text printed by the 4.1.0 jar.
Memory, performance and scaling
-
Cost profile. tika-eval does not parse documents; its cost is tokenizing each extract (twice in Compare), language id, the junk score and database writes. On the eight-file corpus Compare took 11 seconds of wall time, almost all of it start-up and the 67 report queries. The 4.1.x documentation reports 2 to 5 minutes for Compare over 29,000 HTML extracts on an 8-fork workstation, and 45 to 60 seconds for each extraction.
-
Memory. Each worker holds one extract in memory.
-m/maxExtractLength(100 MB of JSON by default) is the guard; lower it on a small heap and pass-Xmxto thejavacommand for large corpora. Oversized extracts are recorded asEXTRACT_FILE_TOO_LONG, not silently dropped. -
Database. H2 is embedded and single-writer. The wiki’s guidance (millions of files fine, hundreds of millions not) still describes the design. Report also accepts
-jdbcfor another database, and the jar contains acomparison-reports-pg.xmlfor PostgreSQL; that path was not tested here. -
Parse times are not benchmarks.
parse_times/compares thetk:parse-time-millisvalues the extraction recorded. They mix JVM warm-up, fork start-up and machine load; only trust differences that are large, consistent and measured on a big corpus with the samenumClients. Use Performance & Scaling for real throughput work. -
Same settings on both sides. Different fork counts or timeouts change which files time out, and every such file shows up as an exception difference.
Pitfalls and security
-
Old flags parse into something else. The wiki and 3.x scripts use
-drop. In 4.1.0 there is no such option, but Apache Commons CLI reads-dropas-d rop: a run ofCompare -drop -extractsA ... -extractsB ...succeeded and silently wrote a database calledrop.mv.db. The single-dash long names-extractsAand-extractsBstill work. -
Existing tables stop the run. Profile and Compare refuse a database that already has their tables (
Table "PROFILES_A" already exists). Use a new-dname for every run, or delete the old.mv.dbfile yourself. -
No
-don Compare means no database afterwards. Without-dthe temporary database is deleted on exit; without-ras well, the run leaves nothing. -
Digest mismatch. If A digests and B does not (or with another algorithm), embedded documents are aligned by name and the attachment reports fill with false differences.
-
Different content handlers. Markdown on one side and text on the other shows up as a content regression everywhere.
-
Without
-icrashes are invisible. A file that crashed has no extract; only a crawl of the original files, plus the ledger, reports it as a crash. -
More tokens is not better. Mojibake splits words and raises token counts; compare common tokens and OOV, not raw counts.
-
Token metrics miss symbols. The 3.3.2 to 4.1.0 euro-sign fix left every token metric unchanged; only U+FFFD counts and the charset reports saw it.
-
Short texts are noisy. OOV, languageness and language id are unreliable on a title or a two-word cell. Filter on
num_alphabetic_tokensbefore you draw conclusions, and treat-99languageness and-1OOV as "not scored", not as scores. -
The tika-config.json filter name does not load in 4.1.0; see above.
-
Reports are overwritten. Report writes into an existing reports directory without warning.
-
The database and reports contain document text.
top_n_tokens, the token-difference lists, file names, paths and stack traces are all in the.mv.dbfile and the workbooks, andrun_inforecords host, user and paths. Treat them with the same care as the corpus: do not attach a comparison of confidential documents to a public issue. Run tika-eval on copies of sensitive corpora, as the documentation advises when investigating crashes. See Security. -
The H2 console listens on a port.
StartDBserves a web console that can open any database file the user can read; run it only on a trusted machine and stop it after use.
From the book to 4.x
| Version | tika-eval |
|---|---|
1.0 (Tika in Action, 2011) |
No tika-eval. The book predates it, so nothing in it covers quality measurement. |
1.15 to 1.28.5 |
|
2.x and 3.x |
Split into |
4.0 and 4.1 (checked on 4.1.0) |
|
The 4.0.0 release was not inspected separately; the "4.0 and 4.1" row describes 4.1.0.
What was verified and what was not
Verified on 2026-10-13 by running it:
-
the
tika-eval-app-4.1.0.zipcontents, its SHA-512, the tool list, theReportusage text and the 4.1.0 option names (also read from the source jar); -
Profile, Compare with
-rand-d, Report with the default and a custom report file, against extracts fromtika-app4.1.0 (Markdown, text and XHTML) and fromtika-app3.3.2 (-J -t); every output, count and query result on this page comes from those runs; -
the H2 rules (empty credentials,
IFEXISTS, read-only mode, single-line-sql), the table and column names; -
the crash-ledger auto-discovery and join with a
file-system-jsonl-reporterledger (with no real crash in the corpus); -
the
-dropmisparse, the "table already exists" failure and theEvalConfigJSON file; -
TikaEvalMetadataFilterfrom Java, and the failure oftika-eval-metadata-filterintika-config.jsonon 4.1.0; -
tika-eval-structure-4.1.0.jarrun with thelib/oftika-eval-app.
Not verified:
-
behaviour with a real crash (
OOM,TIMEOUT) in the ledger, theCRASHclassification and therun-batch.shhelper; -
-z,-jdbcwith PostgreSQL or another database, andStartDBbeyond confirming that it starts and blocks; -
the 29,000-file timings, which are quoted from the documentation;
-
the 3.x XML configuration of the metadata filter, and the 1.x and 2.x details in the history table, which come from the wiki and the 3.3.2 source;
-
any 4.0.0-specific behaviour.
References
-
Apache Tika 4.1.x, Regression evaluation with tika-app + tika-eval
-
TikaEval, Tika wiki (1.x to 3.x era)
-
Maven Central,
tika-eval-app,tika-eval-coreandtika-eval-structure -
Mattmann, C. and Zitting, J., Tika in Action, Manning, 2011: predates tika-eval; no chapter applies.