Xberg 1.0 released: document extraction for a world of tooling

Xberg 1.0 has been released. Xberg is a document extraction engine, describing itself as a "document intelligence framework." It's written in Rust, with fifteen generated language bindings, and a very descriptive data model that provides a lot of flexibility. There are other content extraction frameworks; for Java, it'd be compared to Tika, and against Unstructured for Python. I didn't run a comparison against Unstructured, but I do have some comparison points for Tika.

Measuring the Overlap

Xberg is a rebrand of the Kreuzberg project; Kreuzberg still exists, but is expected to be supported (for critical fixes only) through the end of 2026, with future development moving to "Xberg." The rebrand changes project coordinates throughout (to io.xberg:xberg), so old references to Kreuzberg aren't going to magically get Xberg instead; it is a migration, and not a promotion, although migration should be pretty easy once you've changed the coordinates of the structures.

Xberg is written in Rust, with bindings for Java, Python, Ruby, Go, PHP, Elixir, C#, R, C, TypeScript across several runtimes, and more. Those bindings are not fifteen hand-maintained ports: they are generated from the Rust core by an in-house binding generator. The engine covers more than ninety document formats, does code intelligence across three hundred-odd programming languages through tree-sitter, and runs OCR through pluggable backends, with Tesseract compiled in natively and PaddleOCR available through ONNX Runtime. You can consume all of it as a library, a CLI, a REST service, or an MCP server.

The output model is ambitious. Tika-style extraction gives you text and metadata; Xberg's result model carries elements, tables, images, chunking with pluggable tokenizers, and structured extraction, because the project is aimed at pipelines that want structure, not strings.

The Apache Tika project, probably the gold standard for such extraction for Java, predates the entire idea of a machine as the reader; it was built when extraction fed search indexes and human eyes, and its output shape fits that model well. Xberg assumes an agent might be the caller. The MCP server is a first-class surface, chunking is token-budget-aware down to letting you register the exact tokenizer your embedder uses, and structured extraction exists so whatever sits downstream receives shape rather than a wall of text. None of it is mandatory, and I actually used the plain text resultset for my tests: I ignored all of this machinery at no cost1. Xberg was built for now: a world in which the reader of an extraction result might not be human.

The view from one window

I replaced Tika in a Java application with Xberg, and retained A/B comparison data. I did not compare any other language bindings; my assumption, given how the bindings are generated, is that there will be a rough equivalence for other languages as appropriate for their design, but I can make no assertions about that for sure.

The application itself ran over a disparate corpus: images, documents in various formats like Word, PDF, even WordPerfect, and images with text, in various formats as well. It was a fairly wide scannable surface by design, intending to represent "general ingestion." This gave me compatibility information, efficiency comparison, and typical format conversion information.

On the JVM, Xberg requires Java 25 for the Foreign Function and Memory API. Packaged launchers require the native access flags, and java.awt.headless=true is required for sane server behavior on macOS2. The macOS ARM64 artifact carries its own Tesseract; it does not shell out to a system executable the way Tika does. It wants an explicitly located Tessdata directory and a writable cache directory, and it wants that cache directory even when its caches are disabled3. Tika can use Tesseract, but it's optional; Xberg embeds Tesseract but can use other OCR platforms if desired. There's not really a good answer to whether one approach is better than the other; it's just a design decision, where Xberg assumes that OCR shall be available whereas Tika assumes that it might be available.

It's possible that bugs like the cache reference and other such things are localized to the actual language binding, so Rust-native consumers of Xberg might not see the same problems, and the same might go for Python or any others. This is common for polyglot projects; specific behaviors can be difficult to determine without spanning many use cases.

Tika and Xberg don't quite compete

As I mentioned, my comparison was Apache Tika, but the comparison is... limited. Tika and Xberg are not competing projects. They overlap in very few regions, with a focus on getting text out of documents.

Tika is an aggregate project. It leverages the Java ecosystem, wiring PDFBox and POI and dozens of other parsers behind one interface, and that has been its strength for two decades. If Java has a rich parsing ecosystem, aggregation is the right approach: unify under a common set of interfaces, don't rebuild every wheel out there. But this isn't free: the ecosystem's struggles become Tika's struggles. If the Java ecosystem turns out to handle BigTIFF poorly, and it does, then Tika will handle BigTIFF poorly, through no fault of its own code.

Xberg's policy is self-containment. The Rust ecosystem is newer, which means recent specs tend to exist there as things that were always true rather than things retrofitted late, and the project's habit is to own its parsers rather than borrow them. The exception proves the rule: for legacy WordPerfect, Xberg calls out to C++ libwpd, and reaching for C++ is uncommon enough as Rust practice that it reads as a deliberate concession to a format nobody will ever write a fresh parser for, not as a habit.

Neither policy is virtue. Each is a hedge, and the hedges age differently. Aggregation means shared strengths and shared struggles; Drew Noakes' metadata parser uses ints for file offsets, and some file formats (e.g., BigTIFF) need long. Thus, Tika's going to struggle with those file formats. Self-containment means full credit and full blame.

So: what follows benchmarks the part of Xberg that looks like Tika, from Java. The rest of Xberg's model has no Tika counterpart to compete with.

The overlap, measured

The harness ran Tika 3.3.0 against Xberg 1.0.0 on Java 25, using GraalVM on Apple Silicon (as a standard JVM, not compiled to native code), with Tesseract 5.5.2 for OCR, over eighteen local files covering WordPerfect, DOCX, born-digital and scanned PDF, EPUB, raster images, and other office formats. Each extractor ran in a child JVM so a native crash could not take down the harness, engine order alternated, a warm-up pass ran first, and took the median of three measured runs with Xberg's caches disabled.

Basically, Xberg was about three times faster for the median file and about five times faster in aggregate, 4.4 seconds against 22.5 for the entire corpus. Again: eighteen files, one machine, one operating system. This is good directional evidence and a terrible universal conclusion. The counterweight is initialization. Tika warms up in about 0.4 seconds and Xberg in about 1.5, so the win assumes a persistent process rather than a JVM per document4.

Correctness was nearly a wash, with some caveats that I'll get to. Tika was the baseline, and since I had no BigTIFF inputs, it went eighteen for eighteen. Xberg refused one file outright and returned empty for another.

WordPerfect deserves its own paragraph, because legacy formats are where extraction engines go to get embarrassed5. The corpus had two WP5.1 files wearing nonstandard extensions, .529 and .1, plus one WP6 file, taken from an actual legal corpus with permission. Token-set similarity against Tika was 1.0 on all three, character counts matched within fractions of a percent, and Xberg ran about twice as fast. It also correctly excluded deleted-but-still-present WP6 revision text that a naive parse would have included, and it routed the oddly named files by content rather than extension, which is the only correct behavior, even though it'd be easy to exclude them by assumption.

The scanned PDF turned out to be a lesson in measurement rather than extraction. My first similarity number, Jaccard over unique token sets, came back at 0.76, which looks like a quarter of the content went missing. This was incorrect; unique-set comparison gives a one-off OCR artifact the same weight as a meaningful word that appears two hundred times. Switch to multiset metrics and the same two outputs score 0.96 Sørensen-Dice. Reading the exclusive tokens found it: Tika's exclusives were mostly OCR debris and reversed text from rotated pages ("noissiwwod" is "commission" after a 180-degree round trip, who knew!), while Xberg's exclusives were real words, including "commission" read right side up. The honest verdict is comparable overall, with Xberg apparently better on rotated content. If you ever compare extractors yourself, use marker phrases and multiset metrics. The joy of measurements, everyone! ... unique-set Jaccard will lie to you with a straight face.

It is still a new release!

My input dataset had one outright failure with Xberg: a generated DOCX file tripped Xberg's document-nesting security limit at 1,0246 levels. The file is pathological, the limit is doing its job, and I chose to keep the limit, keep the file as a regression test, and I declined to raise a security bound just to turn a red test green.

The empty result was a genuine defect. A GIF with light text on a dark background came back as zero characters and no warning. The system's Tesseract reads the same image correctly (and Tika, since it used the system's Tesseract, read the image correctly as well.) Forcing OCR did not help. Lossless conversion to PNG did not help, scaling did not help. But inverting the image to dark-on-light produced the full expected text. The defect is polarity handling, not GIF decoding and not Tesseract7. Adjacent to it, the result's ocrUsed field reported false after OCR had demonstrably run, so that flag is not a reliable truth source in 1.0.0. I filed both issues upstream, which brings me to the part of the evaluation no benchmark captures.

Velocity, both faces

One of my acceptance criteria was WordPerfect; when I first started looking seriously at a test of Xberg, WordPerfect was not listed as being supported8. I mentioned this to the maintainers as a question, and by the time 1.0 came out days later, libwpd had been integrated and WordPerfect was officially supported. That is not "responsive maintainers" as a kind description; it's freaking velocity, and self-containment is what makes it possible: a project that owns its stack can land a format in a weekend, where an aggregate project shipping the same capability waits on an upstream release cycle it does not control.

But that velocity has costs, too: the speed that lands WordPerfect in a weekend is the speed that ships a 1.0 with a polarity bug and an incorrect status flag. There is no upstream to blame; the bugs are entirely theirs, and so are the fixes... which didn't even wait for this piece to publish. Both bugs were fixed within a day of being filed. Meanwhile, twenty years of aggregated hardening is why Tika went eighteen for eighteen on my corpus, and un-accumulated hardening looks like my defect list.

Neither project is wrong. They sit at different points on the same curve, and everything has costs and benefits.

The ledger

Ah, the SBOM: moving from Tika to Xberg drops the dependency count from 86 JARs to 5 and raises the payload from 61 MB to 199, because the main JAR carries native libraries for six platforms. For a server application that is a shrug; if you ship something size-sensitive, it's something to consider.

So where does the evaluation land? I measured an overlap between Tika and Xberg. In that region, Xberg was faster, held fidelity on the formats I cared about most, and failed on two files, once correctly9 and once in a way that generated a bug report. Everything beyond that region, the elements and tables and chunking and structured extraction, I can describe but have not yet formed an opinion on.

What I did earn is an expectation. After enough time against a result model that treats text as one facet of a document rather than the whole of it, "text plus some metadata" stops looking like an extraction result and starts looking like a lossy one. Tika defined what extraction meant on the JVM for twenty years, and defined it well, for readers who were people and simple indexes. Xberg 1.0 is interesting for a lot of reasons, but the ones that stood out to me are: it's measurably fast, and its data model is quite suitable for the foreseeable tool-oriented future.


  1. In fact, maybe it was a pure benefit: I can use the richer data when I'm ready to, but I can choose which representation makes sense. And as the benchmarks showed, I got the richer data access for free even though I didn't use it.

  2. The headless flag also fixed a Tika scanned-PDF failure I hit along the way, so consider it table stakes for either engine on a Mac rather than an Xberg quirk.

  3. This was filed as a bug against Xberg, and within one day it had been repaired.

  4. In my case, the "actual application" uses an event system, so it's definitely intended to ignore startup time for long-running performance over a much larger dataset. The eighteen files are representative of the input corpus' shape, not its size.

  5. Confession: I love WordPerfect. I haven't used it in forever, because Word took over, but it's still very much a useful format especially in legal environments. Ignoring WordPerfect is pretty standard... and unfortunate.

  6. Xberg reported that the DOCX had 1025 nesting levels. Analysis actually showed that it had 1100 document-nesting levels, not 1025, but Xberg stopped when it hit the limit, which is probably proper behavior. Sane documents are good, people.

  7. Thus, the fix was to assume an empty result was possibly a failure: if I retrieved no text from an image, try again with image inversion on, which retrieved the text properly.

  8. This is not to say that WordPerfect content could not be extracted; it means that WordPerfect content was not asserted as being properly supported. Naive retrieval, compared to proper retrieval, but it's fair to say that while naive retrieval is better than nothing, it's not correct; deleted text would be returned in a naive retrieval when it shouldn't be.

  9. In my problem scope, I do not care about pathologically-generated files. If my problem scope required them, I could have adjusted things. I did not, because my problem scope doesn't require them. Easy peasy! It's not a problem if you ignore it, right?

Comments (0)

Sign in to comment

No comments yet.