OPENNLP-1919: Expose dependency parses as a typed Document annotation layer - #1237
Draft
krickert wants to merge 97 commits into
Draft
OPENNLP-1919: Expose dependency parses as a typed Document annotation layer#1237krickert wants to merge 97 commits into
krickert wants to merge 97 commits into
Conversation
…yers over the original text Adds opennlp.tools.document to opennlp-api: Document (immutable, copy-on-add layer container over the original text), Annotation (a typed value on a Span), LayerKey (open, typed layer identity), and DocumentAnnotator (pipeline step declaring the layers it requires and provides). DocumentAnalyzer assembles annotators into a pipeline validated at build time. Standard keys in Layers cover sentences, tokens, part-of-speech tags, and entities, populated through thin adapters over the existing SentenceDetector, Tokenizer, POSTagger, and TokenNameFinder interfaces, which stay the primary API for single-task use and are unchanged. All spans refer to the text as supplied. No new dependencies.
…ntainer, javadoc precision pass
…rom missing layers, validate providers at build time
… adaptive data on failure The lemmatizer adapter now slices tokens and tags per sentence like its POS and name-finder siblings, so lemmatization decisions never cross a sentence boundary, and it declares the sentence layer as required. The POS adapter rejects a tagger that returns a wrong tag count. The name-finder adapter rejects mentions whose token indices lie outside their sentence instead of silently reading the next sentence's tokens, clears adaptive data even when annotation fails, and derives UNTYPED from NameSample.DEFAULT_TYPE instead of re-declaring the literal.
… definition A blank check under the toolkit's whitespace definition, which unlike String.isBlank covers the no-break spaces, so annotators validating labels and identifiers share one predicate instead of each carrying a private copy. Reads whole code points; tests pin the no-break and figure spaces, the empty string, and a supplementary-plane letter.
…nt rule Adds the Document Annotation Container chapter to the manual, with every code example and every stated span and value mirroring the passing pipeline example test. The review pass aligns the branch with the project's conventions: layer key ids validate through StringUtil.isBlank, the annotator interface leaves thread safety implementation specific, the sentence and tokenizer adapters document annotate like their siblings, repeated rejection-message literals become per-class constants, and the name finder test's nine anonymous fixtures fold into one helper. Layers now states the key placement rule: core layer keys live there, capability layer keys on their providing annotator.
Every key the toolkit defines now carries the opennlp: id prefix (opennlp:sentences, opennlp:tokens, opennlp:pos, opennlp:entities, opennlp:lemmas, opennlp:stems). An extension defines its keys under its own prefix, and a bare id stays legal for an application-local layer, so ids from independent producers cannot collide. The rule is stated on Layers, LayerKey, and in the manual chapter.
A layer key now declares whether its layer is positional or document-scoped. A positional key, the default, guarantees a span on every annotation, so consumers never null-check one. A document-scoped key, created through LayerKey.document, carries whole-document values without spans, the home for a language id, a category distribution, or provenance. The scope is declared per key, never per annotation: the container rejects a span-less annotation under a positional key and a spanned annotation under a document-scoped key, naming the layer either way. Scope participates in key equality.
…on text The three invariants the contract tests already enforce are now stated on the Document interface and in the manual chapter: layers preserve insertion order and are never reordered, layers are immutable once added and detached from the caller's input list, and adding a layer is once-only with the rejection naming the key. Together they keep index-based references between layers valid for the lifetime of the document.
A corpus may carry a hand-annotated version of a layer beside a produced one. The convention is a gold: id prefix on the same key scheme, for example gold:opennlp:tokens beside opennlp:tokens. Because adding a layer is once-only, competing versions of a layer always live under distinct keys and never replace each other. Stated on Layers and in the manual chapter, with a contract test pinning the coexistence.
…ctories on Layers
…le test
Add {@inheritdoc} to the Document, LayerKey, and adapter overrides, and note in
the manual that DocumentPipelineExampleTest asserts the pipeline round-trip.
…ainer contract - Reject zero-length finder mentions in NameFinderAnnotator and pin the second-sentence case, which was previously mapped silently wrong, with a test - Add DocumentAnnotators with requireLayers and the per-sentence token walk, replacing three copies of the walk loop and four spellings of the missing-layer rejection; direct tests pin the helpers as public API - Capture the document text as a String at construction so ImmutableDocument's immutability and thread-safety claims hold for mutable CharSequence inputs - Move the copy-on-add and threading narrative from the Document interface Javadoc to ImmutableDocument; the interface now states that thread safety is implementation specific - Carry the entity type as the annotation value only; entity spans are untyped, and the Javadoc names the value as the single source of the type - Make all six adapter annotators final before the types freeze - Align the TokenLengthAnnotator example with the documented required-layer contract in both the manual and the example test via requireLayers - Housekeeping per review: docbook CDATA placement, imports over qualified names, a ParameterizedTest for the blank-input matrix, shared deterministic test components, static assertion imports, inheritDoc on the runtime adapters, Layers constructor comment, and the documented NPE of StringUtil.isBlank
…ll rejection - Fold the three verbatim copies of the "Ana runs. Bob sits." document into a single twoSentenceDocument() helper in NameFinderAnnotatorTest, so the sentence and token layers of the shared fixture are declared once instead of drifting between the over-long mention, zero-length mention, and per-sentence offset tests - Hoist the no-op TokenNameFinder out of the blank-input test into a NO_NAMES constant in DocumentAnalyzerTest, since a finder that returns no spans is pipeline plumbing rather than part of any one test case, and document what it is for - Add testAnnotatorAdaptersRejectNullDocuments to pin that all four adapters reject a null document with the same "document must not be null" message, whether they validate directly or through DocumentAnnotators.requireLayers; the shared message was previously unpinned and free to drift per adapter - Trim the stale "person-free" qualifier from the New York comment in testTokenIndexSpansBecomeCharacterSpans; the finder emits a location mention and the extra negation described a distinction the test no longer draws
…, pin blank and span edge cases - ImmutableDocument: wrap the layer map unmodifiable at construction and expose its cached key set; split the combined null check so the message names the offending argument - StringUtil.isBlank javadoc: state how it differs from isUnicodeBlank - Tests: parameterize the isBlank accept and reject sides, pin the null NPE, and pin char-indexed spans over a supplementary-plane character
Addresses rzo1's review comments on the manual: - Open with a plain-language definition and an inline typed-layer example instead of a bolded three-item list. - Show a stacked-layers figure for the running example up front and reference it from the pipeline section, so readers see the shape of a document before the API detail. - Explain span offsets, key identity, and the opennlp: prefix convention in prose a first-time reader can follow. - Reword the single-task API aside; drop the redundant custom annotator opener; cut the repeated statically-typed phrasing.
Two contract tests fail red against the default method stub: java.lang.UnsupportedOperationException: merge is not implemented yet merge joins two documents grown independently over the same text, the parallel fan-out join rzo1 asked for on the pull request: disjoint layers stack into one document, the sources stay untouched, and a null argument, a different text, or a duplicate layer key is rejected with the offending key named.
The default body validates the argument and the shared text, then adds each of the other document's layers through with(), so every layer is re-validated against this document's contract and a duplicate key is rejected by the same once-only rule a direct add follows. The pinned contract tests now pass; opennlp-api is 386 tests, 0 failures.
Two contract tests fail red against the stubbed two-arg merge: java.lang.UnsupportedOperationException: merge with policy is not implemented yet The strict single-arg merge stays the default; the policy variant opts into keeping one copy of a layer both documents rebuilt identically, and still rejects differing copies with the key named.
merge(other) stays strict and now delegates to merge(other, REJECT). The KEEP_EQUAL policy keeps one copy of a layer both documents rebuilt identically, the shared sentence/tokenizer prefix of two parallel branches, while differing copies are still rejected with the key named. The pinned contract tests now pass; opennlp-api is 388 tests, 0 failures. The manual's fan-out paragraph documents the option.
- literallayout class=monospaced makes the docbkx toolchain emit a pre block, so the figure's character ruler and layer rows column-align; plain literallayout renders in the proportional body font. - Correct the pipeline section: the figure shows three of the four layers; the custom token-lengths layer is the fourth. - Trim restated clauses in the merge javadoc, the contract test javadoc, and the introduction; align the layersEqual and addLayer helper javadoc with what the helpers do.
The repinned test fails red: a KEEP_EQUAL merge that rejects a layer whose copies differ still reports 'layer is already present', which reads as if the policy was ignored. The caller opted into duplicates; the reason worth naming is that the contents differ.
…ayer When the policy is KEEP_EQUAL and a layer key is present on both documents, a failed equality check now throws directly instead of falling through to with(), so the message states the actual reason: the copies differ, not merely that the key is a duplicate. The pinned contract test passes; opennlp-api is 388 tests, 0 failures.
One javadoc sentence on the constant: equality is Annotation equality, so spans compare by offsets and type, never by probability, and values by their own equals. Two branches running different models over the same text can therefore agree; the kept copy is this document's.
Two guards ahead of an ImmutableDocument merge override: the interface default serves implementations that do not override merge with the same join, KEEP_EQUAL, and rejection messages, and merge re-validates the layers it takes from a foreign document instead of trusting them, rejecting an out-of-bounds span by name.
The interface default adds the other document's layers through with(), building one intermediate document and one map copy per layer. The override validates each incoming layer with the same checks with() runs, then copies the layer map once and allocates one document; when nothing was added it returns this, matching the default. The layer validation moves from with() into a shared helper unchanged. Pinned by the cross-implementation contract tests; opennlp-api is 390 tests, 0 failures, runtime annotator suites green.
The chapter said offsets count characters; the pinned contract test shows a supplementary-plane character counts as two. Say Java chars (UTF-16 units) so the claim matches the tested behavior.
… models The scorer re-derived every feature's hidden-layer contribution from its embedding on every configuration, although for a frozen model the contribution of a (template position, embedding row) pair is a fixed vector. Parsers now turn on a bounded lazy cache that computes each pair's vector once on first sight and adds it thereafter, the adaptive form of the precomputation described for this architecture by Chen and Manning (2014): tag and label rows are fully cached within a document or two and word rows follow their frequency. Measured on realistic dimensions (20k words, embedding 50, hidden 400, 77 transitions): 5,462 to 71,954 scored states per second, 13.2x. Training and refinement work on uncached copies, copies never carry a cache, concurrent readers are safe by idempotent fill, and a test pins cached-versus-direct agreement to float rounding with identical winning transitions.
Replaces the two private blank helpers with the shared predicate; behavior is identical since both already followed the toolkit whitespace definition.
…ed example Add docbkx/dependency.xml, wire it into the manual, and cite ConlluDependencyParserUsageTest.
…e constants - DependencyParserME decodes the model outcome inventory once in the constructor and keeps it as a Transition[]. Decoding a sentence now indexes that array instead of parsing an outcome string per configuration and per outcome, and a model trained for another task is rejected with IllegalArgumentException when the parser is built rather than surfacing as an IllegalStateException in the middle of a sentence. Both constructors document the new failure, and a pinning test builds a parser over a MaxentModel whose outcomes are POS tags to prove the rejection happens up front. - Removed the private isBlank copy from ConlluDependencySampleStream and routed sentence separation through StringUtil.isBlank, so the toolkit whitespace definition lives in one place. The rationale the copy carried moved into the nextSentence javadoc, which also gained its missing @return and @throws. DependencyArc validates its relation through the same predicate, so an arc label made only of a no-break space is rejected exactly as a CoNLL-U separator line is. - StringUtil.isBlank rejects a null argument with IllegalArgumentException instead of letting a NullPointerException escape the loop, and documents it. Its test became a parameterized case list plus an explicit null case. - Extracted the magic values of DependencyContextGenerator into named constants: the word/tag and position separators, the feature count the list is sized to, the valency and distance bounds, and the long-distance feature value. The distance feature is computed once rather than twice. - Named the *ROOT* vocabulary key ROOT_SYMBOL and the "*" special-symbol prefix SPECIAL_SYMBOL_PREFIX on FeedforwardDependencyModel and used them from FeedforwardContext and FeedforwardDependencyTrainer, which spelled all three as literals. FeedforwardContext names the first dependent position instead of indexing the template at a bare 6. - Folded the three repeated special-symbol loops in the trainer vocabulary builder into addSpecialSymbols, and hoisted the repeated model.transitions() call out of the transition-decoding loop. - FeedforwardDependencyModel.score and featureIds validate their array argument, and lookup fails loudly when a vocabulary carries no *UNK* row to fall back on instead of returning null and unboxing to a NullPointerException later. - Documented the package-private accessors of FeedforwardDependencyModel, saying which of them hand out the live arrays the trainer writes into. - Trimmed commentary to what the code does: enableScoringCache drops the literature reference and the restatement of how caching pays off, and DependencyEvaluator.processSample uses {@inheritdoc} plus only what the override adds over the Evaluator contract. - Replaced fully qualified java.util.Arrays, java.util.function.Function, java.io.ByteArrayInputStream, java.io.ByteArrayOutputStream and MaxentModel uses with imports in the trainer, the arc-standard state and the tests, and dropped stray blank lines in DependencyGraph and Transition. - Moved the sample() and corpus() helpers, copied verbatim in three test classes, into a shared DependencyTestSamples fixture with the repetition count as a named constant. - Added pinning tests: a relation of U+00A0 alone is rejected while a label such as nmod:poss is kept, an empty DependencySample is rejected, and the arc and graph relation accessors return what was passed in.
Add reference links for CoNLL-U, the arc-standard system (Nivre 2004), the feedforward architecture and training recipe (Chen and Manning 2014), early update (Collins and Roark 2004), and the Unicode SpecialCasing file. State that thread safety is implementation specific on the DependencyParser interface. Add javadoc to the remaining private helpers in main and test code. Fold the duplicated corpus reading in the feedforward trainer into a readAll helper, and read the corrupt-model test fixture as UTF-8. The DependencyModel serialVersionUID was verified to equal the serialver default.
Wires the dependency parser into the document pipeline: reads the token and tag layers, parses, and provides a dependencies layer with one DependencyArc per token anchored on the dependent's span. Arc head and dependent are indices into the token layer, exercising the container rule that annotations reference each other by layer and index; the test resolves an arc's head through the token layer back to its span in the original text. (cherry picked from commit 902fbb3)
…ocument-coordinate javadoc
… boundaries The annotator now rejects a parser that returns a graph over a different token count than its sentence, instead of silently misaligning the dependency layer with the token layer, and the javadoc states the text-order requirement the walk has always relied on. New tests pin the empty-sentence index shift, a token straddling two sentence spans, the stuck-scan path behind a gap token, and the graph-size rejection. The staged copy of the document container was refreshed to the current foundation, whose adapters parse per sentence and whose empty-versus-missing layer distinction moves the empty-text failure into this annotator's own validation.
…ample Add a DependencyAnnotator section to the dependency chapter citing DependencyAnnotatorPipelineTest.
Open the CDATA on its own line so the rendered code block has no leading blank line, matching the two listings above it and parser.xml.
…t cleanup - Check each required layer for presence on its own, so a document missing the sentence, token, or tag layer is rejected with a message that names the key that is absent instead of being folded into the alignment complaint. - Accept present-but-empty required layers: a document with no sentences and no tokens now yields a present-but-empty dependencies layer rather than an IllegalArgumentException, which is the empty-versus-absent distinction the rest of the container annotators already follow. - Extract the shared rejection prefix into a MISSING_LAYER constant so all three absence checks emit one message shape. - Restate the annotate() javadoc and its @throws list against the checks that are actually performed, and say explicitly that the required layers may be empty. - Trim commentary that only repeated the javadoc: the class-level narration about being the first graph-shaped layer, the unwrap-the-slice comment, and the document-coordinate tail of the index-shift comment. - Replace the three hand-rolled absent-layer assertions with a parameterized test over one document per required layer, asserting the message names that layer. - Add pinning tests for the null-document message and for the empty document producing a present-but-empty arc layer, and flip the pipeline test on empty text to assert the same pass-through instead of a failure. - Assert the exact message in testMissingLayersThrow rather than only the exception type. - Hoist the one-token parser stub to a ONE_TOKEN_ROOT constant shared by the two tests that had declared it inline, and extract the repeated STRAY_TOKEN and MISALIGNED expected messages into constants. - Drop the two docbook sentences that pointed readers at test class names, state the present-but-empty layer contract in the annotator section, and correct "indexes" to "indices" in the example comment.
krickert
added a commit
that referenced
this pull request
Aug 26, 2026
…he gRPC helper base # Conflicts: # opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/depparse/DependencyAnnotatorPipelineTest.java
krickert
added a commit
that referenced
this pull request
Aug 30, 2026
Adapts a Chunker to the document pipeline: reads sentences, tokens, and POS tags and provides opennlp:chunks, one annotation per phrase chunk carrying its type on the span of its tokens.
Adapts a constituency Parser to the document pipeline: reads sentences and tokens and provides opennlp:phrases, one annotation per phrase node above the part-of-speech level in pre-order, carrying the label and the span of the head token the parser's head rules select.
Also makes the ParserAnnotator helpers instance methods, matching the other adapters.
krickert
added a commit
that referenced
this pull request
Sep 1, 2026
krickert
force-pushed
the
OPENNLP-547-dependency-parser
branch
from
September 1, 2026 07:54
71da201 to
83bc298
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stack
This draft depends on OPENNLP-547 and OPENNLP-1888. It must remain draft until both dependencies land.