Skip to content

OPENNLP-1888: Document annotation container with typed offset-anchored layers - #1182

Open
krickert wants to merge 32 commits into
mainfrom
OPENNLP-1888-DocumentShape
Open

OPENNLP-1888: Document annotation container with typed offset-anchored layers#1182
krickert wants to merge 32 commits into
mainfrom
OPENNLP-1888-DocumentShape

Conversation

@krickert

Copy link
Copy Markdown
Contributor

Adds the document annotation container discussed on OPENNLP-1888: an immutable Document over the original text, typed LayerKey identities, span-anchored Annotation values, DocumentAnnotator with declared requires/provides, and a DocumentAnalyzer whose pipeline ordering is validated at build time. Four adapters over the existing single-task interfaces (sentence detector, tokenizer, POS tagger, name finder) plus lemmatizer and stemmer layer adapters come with it; the container itself never learns about specific layers.

Contract behavior, each pinned by a test asserting the exact message where one is thrown: spans are structurally mandatory and validated against the text length; key equality is the (id, type) pair; layers preserve insertion order and are never sorted; layers are immutable once added and detached from caller input; providing an already-present layer is rejected; reading an absent layer returns an empty immutable list. 33 tests, including a full pipeline example and a contract suite.

Follow-ups planned as commits on this PR, from the review discussion on the ticket: namespaced identifiers for the standard keys; per-key positional versus document scope for whole-document facts; the invariants above transcribed into the specification text; and the documented convention for gold versus predicted layers. Opening as a draft until those land.

The acceptance criterion suggested in that discussion, that a new layer can be added without touching the container package, is already observable: the feature branches on the ai-pipestream fork (glossary, PII, coreference, dependencies, relations, money/quantity/temporal, geo, embeddings) each add their layers with no container edits.

@krickert
krickert force-pushed the OPENNLP-1888-DocumentShape branch 3 times, most recently from 4cd9beb to 7e65aad Compare July 21, 2026 08:59
@krickert
krickert marked this pull request as ready for review July 21, 2026 09:07
@krickert

Copy link
Copy Markdown
Contributor Author

This was proposed about a week ago. Discussions pointed to this shape - I feel like it's a great direction as a lot of research went into landing this shape:

  • A single shared container for full pipelines: sentence, token, POS, lemma, and name annotations live in one Document with typed, offset-anchored layers instead of parallel arrays passed hand to hand.
  • Exact character-offset provenance: every annotation points back into the original text, the prerequisite for offset-aware normalization and span-level traceability.
  • A uniform DocumentAnnotator seam: new analysis steps (gazetteer joins, geocoding, quality scoring, PII detection, relation extraction) plug in as annotators over the same container rather than each inventing its own I/O shape.
  • Cross-component evidence sharing: one component's layer (for example emoji or entity annotations) is directly readable as features by another, with no glue code.
  • Cleaner downstream integration: document pipelines that use OpenNLP can consume one typed annotation record per document instead of adapting several tool-specific outputs.
  • Unblocks the staged follow-up branches that currently carry private copies of this container; once merged they rebase to plain, small diffs.

@rzo1

rzo1 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

I think that this needs to wait a bit more until a few more people ump into the discussion: https://lists.apache.org/thread/jwxxjkc2b0dqn4rwjvt1t7cdf056gqhp

@krickert
krickert marked this pull request as draft July 21, 2026 10:14
@krickert
krickert force-pushed the OPENNLP-1888-DocumentShape branch from 7e65aad to b920e43 Compare July 24, 2026 03:21
krickert added a commit to ai-pipestream/opennlp that referenced this pull request Jul 24, 2026
…ENNLP-1895 recorded

Restate the map against apache main a864230, cut as 3.0.0-M5 on 2026-07-24.
apache#1177 (OPENNLP-1870) merged upstream and moves into the merged box, apache#1190 and
apache#1191 are marked ready for review, and OPENNLP-1895 (quantized embedding
tables) joins the diagram in its own colour: filed in JIRA with the pull
request deliberately held until apache#1165 and apache#1152 move.

Statuses now carry the measured GitHub draft flag and how far each head sits
behind main, which surfaces three things the old text did not: apache#1182 is a draft
again, apache#1167 is based on main rather than on apache#1155 and carries the seam and
isBlank commits as copies, and apache#1152 reports conflicts only because its
apache-hosted sentencepiece base has diverged from the refreshed head.
@krickert
krickert force-pushed the OPENNLP-1888-DocumentShape branch from b920e43 to 3ab6920 Compare July 24, 2026 19:27
krickert added a commit to ai-pipestream/opennlp that referenced this pull request Jul 24, 2026
…est head

All nine open heads now sit directly on a864230 and report mergeable. Two
were reporting conflicts and both cleared: apache#1167 through a plain rebase, and
apache#1152 by pointing its apache-hosted sentencepiece base branch at the refreshed
head it had drifted away from, which shrinks its diff back to the 30 commits it
owns. apache#1166 shed the 13 OPENNLP-1883 commits it carried, since apache#1163 is
upstream as a single squash, and is 3 commits now.

Also correct what the draft flag on apache#1182 means: the branch is review-ready and
waits on the upstream queue, not on unfinished work.
@rzo1

rzo1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review of PR #1182OPENNLP-1888: Document annotation container with typed offset-anchored layers

Blocking

opennlp-api/.../document/NameFinderAnnotator.java:136 — zero-length mention escapes the bounds check.
if (mention.getStart() < 0 || mention.getEnd() > count) does not reject getEnd() == 0. Span permits start == end (the contract test asserts zero-length spans are legal), so a finder returning new Span(0, 0) reaches line 141: tokens.get(first + mention.getEnd() - 1) indexes first - 1. For the first sentence that is IndexOutOfBoundsException; for a later sentence it silently takes the end offset of the previous sentence's last token and then new Span(start, end) throws IllegalArgumentException("start index must not be larger than end index"). Neither is the documented loud rejection. Add || mention.getStart() >= mention.getEnd() to the check and pin it with a test, next to testMentionOutsideSentenceTokensFailsLoud.

opennlp-api/.../document/Document.java:23-49 — interface Javadoc describes the implementation and threading.
"Documents are immutable: with(...) returns a new document that shares the unchanged layers. That immutability makes instances safe to share between threads." That is ImmutableDocument behaviour, not the interface contract, and threading belongs in the accepted phrasing "Thread safety is implementation specific." — which DocumentAnnotator:346 already uses correctly. Move the copy-on-add and sharing narrative to ImmutableDocument, keep the invariants (insertion order, once-only add, unmodifiable lists) on the interface where they are genuinely contractual.

ImmutableDocument.java:50 — the text is a CharSequence held by reference, so the immutability claim is not enforced.
empty(CharSequence) stores the caller's instance. Pass a StringBuilder and the document's text, the span bounds validated in with(...), and the "safe to share between threads" claim all become false after the caller mutates it. Either normalize to String on construction, or drop the immutability and thread-safety guarantees from the Javadoc. This is a new public type frozen at release, so it needs deciding now.

Three copies of the sentence/token walk loop.
NameFinderAnnotator.java:117, POSTaggerAnnotator.java:94, LemmatizerAnnotator.java:115 each contain the same ~25 lines: the while advancing next over tokens enclosed by the sentence span, the count == 0 continue, the per-sentence String[] fill, and the trailing token at ... lies outside every sentence check. Extract one helper. LemmatizerAnnotator lives in opennlp.tools.lemmatizer, so a package-private helper is not enough — either a public helper type in opennlp.tools.document or an abstract per-sentence annotator base.

MISSING_LAYER duplicated three times and inlined once.
Declared identically in NameFinderAnnotator.java:36, POSTaggerAnnotator.java:41, LemmatizerAnnotator.java:52, then spelled as a bare literal in StemmerAnnotator.java:81. Partial alignment is worse than none. One shared constant, or better one shared requireLayer(document, key) helper that throws, used by all four.

LemmatizerAnnotator.java:152,157 and StemmerAnnotator.java:93,98 — no Javadoc on requires()/provides().
The opennlp-api adapters put {@inheritDoc} on both. Same convention, or drop it from all six adapters.

DocumentPipelineExampleTest.java:172 — the example contradicts the documented contract and the manual.
TokenLengthAnnotator.annotate throws "document lacks the required layer " when the token layer is empty. The class Javadoc, DocumentAnnotator:355, and document.xml:205 all state that a required layer must be present but may be empty. The same class printed in the manual (document.xml:168) has no such check. This test is the reference example a first-time user copies, so it must not teach the opposite of the contract. Remove the tokens.isEmpty() branch.

Minor

  • LemmatizerAnnotator.java:95,99,103document.layers() called three times, each allocating a fresh unmodifiableSet wrapper. NameFinderAnnotator:99 and POSTaggerAnnotator:83 hoist it into a local; do the same here.
  • document.xml:76 and the other four listings — <![CDATA[ sits on its own line indented four tabs. Every existing chapter (stemmer.xml:40, :60) puts it on the <programlisting language="java"> line. The deviation also renders a leading blank line in each snippet.
  • DocumentAnalyzerTest.java:55,75,107,112 — fully qualified java.util.List, java.util.ArrayList, opennlp.tools.util.Sequence although java.util.List is imported at the top and the sibling test files import Sequence.
  • DocumentAnalyzerTest.java:168for (final String text : new String[] {"", " "}) is a hard-coded input array; make it a @ParameterizedTest so a failure names the offending input.
  • Test fixture duplication: SPLITTER/WHITESPACE in DocumentAnalyzerTest.java:43,66 are the same period splitter and whitespace tokenizer as PERIOD_SPLITTER/SPACE_TOKENIZER in DocumentPipelineExampleTest.java:60,87. Extract to a shared test utility.
  • DocumentAnalyzerTest.java:96"barks.".contains(sentence[i]) has the arguments reversed, so any substring of "barks." gets VBZ. It happens to pass; use a set lookup.
  • SentenceDetectorAnnotator, TokenizerAnnotator, POSTaggerAnnotator, NameFinderAnnotator, LemmatizerAnnotator, StemmerAnnotator are non-final public class while DocumentAnalyzer is final. Decide extensibility per class before the types freeze.
  • Layers.java:130 — the private constructor comment says "This class holds constants only and is never instantiated", but the class also exposes key(...) and documentKey(...). Fix the comment.
  • LemmatizerAnnotatorTest and StemmerAnnotatorTest use Assertions.assertEquals(...) throughout while the opennlp-api tests use static imports. Align.
  • NameFinderAnnotator.java:143 — the entity type is written both into the Span type and into the annotation value. Two sources of truth for the same fact; pick one and say which in the Javadoc.
  • StringUtil.java:283 — Javadoc says the argument "Must not be null" but there is no validation and no @throws. It matches the neighbouring isEmpty, so consistency is fine, but document the NPE.

Process

You have eight other open PRs (#1191, #1190, #1167, #1166, #1165, #1155, #1154, #1152). The duplication, {@inheritDoc} on overrides, repeated string literals as constants, interface-Javadoc-without-threading, and @ParameterizedTest for hard-coded input arrays are recurring items — please apply them to all open PRs likewise. No model-affecting changes here, so no eval build needed. Keeping this one draft until the four follow-ups land is right.

@krickert

Copy link
Copy Markdown
Contributor Author

Thanks for taking the time I will address all of these within a few hours

@krickert
krickert force-pushed the OPENNLP-1888-DocumentShape branch from 819070c to 5ba76d6 Compare July 28, 2026 15:15
@krickert

Copy link
Copy Markdown
Contributor Author

All of it is addressed. The branch is rebased on current main.

Blocking

Zero-length mention. mention.getStart() >= mention.getEnd() added to the bounds check. Pinned by testZeroLengthMentionFailsLoud, which returns Span(0, 0) for the second sentence, the case that was mapped silently wrong rather than throwing, and asserts the adaptive data is still cleared.

Interface Javadoc. The copy-on-add and sharing narrative moved to ImmutableDocument. Document keeps only what is contractual (insertion order, once-only add, unmodifiable lists) and now says "Thread safety is implementation specific."

CharSequence held by reference. Normalized to String at construction, so the span bounds validated on insertion stay valid for the document's lifetime. testTextIsCapturedAtConstruction mutates a StringBuilder after construction and asserts the document is unchanged.

Three copies of the walk, and MISSING_LAYER. Both replaced by one public helper, opennlp.tools.document.DocumentAnnotators, since LemmatizerAnnotator sits in another package. It carries requireLayers(Document, LayerKey...) and forEachSentence(sentences, tokens, consumer). All four annotators use it, so the four spellings of the missing-layer rejection are now one, and DocumentAnnotatorsTest pins the helper as public API in its own right, including the walk's contiguous-run slicing and the token-outside-every-sentence rejection.

{@inheritDoc} on the runtime adapters. Added to requires() and provides() on both.

The example contradicting the contract. The tokens.isEmpty() branch is gone. TokenLengthAnnotator now calls DocumentAnnotators.requireLayers, which is the same rejection the contract describes, and the manual listing matches the test again.

Minor

Done: hoisted document.layers(); <![CDATA[ moved onto the programlisting line in all five listings; fully qualified names replaced with imports; @ParameterizedTest for the blank-input matrix; shared TestComponents for the period splitter and space tokenizer; Layers constructor comment corrected; static assertion imports in the two runtime tests; @throws NullPointerException documented on StringUtil.isBlank.

Reversed contains. Replaced with a Set.of("barks.", "eats.") lookup rather than left passing by accident.

Entity type in two places. The annotation value is now the single source. Spans are constructed untyped and the Javadoc says so, on both Layers.ENTITIES and the annotator. Tests assert span().getType() is null.

Extensibility. All six adapters are final, matching DocumentAnalyzer. Easier to open one later than to close one after the types freeze.

Since then a second pass folded the three copies of the "Ana runs. Bob sits." fixture in NameFinderAnnotatorTest into one helper, hoisted the no-op finder to a constant, and added a test pinning the exact null-document rejection across all four api-side adapters, which had no coverage.

The document package runs 65 tests, none skipped.

@krickert
krickert marked this pull request as ready for review July 28, 2026 22:43
@krickert
krickert requested review from jzonthemtn, mawiesne and rzo1 July 31, 2026 10:35
krickert added 11 commits August 5, 2026 22:10
…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.
…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.
…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
@krickert
krickert force-pushed the OPENNLP-1888-DocumentShape branch from 5ba76d6 to e906225 Compare August 6, 2026 02:11
…, 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
krickert added a commit that referenced this pull request Aug 15, 2026
@krickert krickert mentioned this pull request Aug 16, 2026
10 tasks

@rzo1 rzo1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a few docs regarding the documentation and one suggestion.

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
<para>
The design follows three rules:
</para>
<itemizedlist>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we make this less LLM generated? The bolt stuff at the beginning and the rest reads very "bla" ;-)

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
highlighted in the source text.
</para>
<para>
The design follows three rules:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these rules ?

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
<title>Introduction</title>
<para>
The package <code>opennlp.tools.document</code> provides an immutable container
that carries the original text of one document together with any number of typed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is a typed annotation layer? A user doesn't now that yet. maybe give an inline sxample.

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
that carries the original text of one document together with any number of typed
annotation layers over it. Every annotation is anchored to a
<code>Span</code> of the text exactly as the caller supplied it, never to a
normalized or otherwise derived form, so any result of any pipeline step can be

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A user doesn't now about pipeline steps yet. Can we rephrase it or link to a later explanation ?

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
<para>
<emphasis role="bold">Offset-anchored.</emphasis> A layer is a list of
<code>Annotation</code> values, each pairing a <code>Span</code> in original
text coordinates with a typed value. Annotations reference other annotations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is a original text coordinate?

Adapters for the toolkit's own components are provided:
<code>SentenceDetectorAnnotator</code>, <code>TokenizerAnnotator</code>,
<code>POSTaggerAnnotator</code>, <code>NameFinderAnnotator</code>,
<code>LemmatizerAnnotator</code>, and <code>StemmerAnnotator</code>. Each wraps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we reword that? "single task" API or "primary API" bla bla? This doesnt read nice. Users can decide to use this new API for their single-task use ;-)

.add(new TokenLengthAnnotator())
.build();

Document document = analyzer.analyze("The dog barks. It naps.");]]>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could use exactly that example here to show a graphical version of a document, so we can reference it here later and people direcltryn ow what do expect?

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
<section xml:id="tools.document.custom">
<title>Writing a custom annotator</title>
<para>
A new capability contributes its results as one more layer without any change to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is already known now, imho. First sentence can be dropped imho.

Comment thread opennlp-docs/src/docbkx/document.xml Outdated
}]]>
</programlisting>
<para>
Reading the layer back is statically typed by the key, so the values are used as

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The "statically typed" bla is mentioned a lot of times in this doc. Please reduce.

*
* @since 3.0.0
*/
public interface Document {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what do you think about defining a Document merge(Document d) on the interface? Say you have processed the same document in parallel, so different layers are stacked in it now and you want to combine them (like adding the layers on top of the current one). wdyt?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a great idea. We have the technology. We can build it. I'll work on it tonight after day job.

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.
Three tests fail red: the adapters inherit the identity toString, so a
pipeline validation message reads 'annotator
opennlp.tools.document.SentenceDetectorAnnotator@3b96c42e requires
layer ...' instead of naming the adapter. The stemmer test pins the
full analyzer message exactly, since that adapter requires a single
layer and the message is therefore deterministic.
All six adapters override toString with the simple class name, so a
pipeline validation message names the offending annotator readably.
The pinned tests pass; opennlp-api and opennlp-runtime suites green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants