-
Notifications
You must be signed in to change notification settings - Fork 507
OPENNLP-1888: Document annotation container with typed offset-anchored layers #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
49b9612
OPENNLP-1888: Document annotation container: typed offset-anchored la…
krickert 3e382b3
OPENNLP-1888: Lemma and stem layer adapters
krickert e8a2ca4
OPENNLP-1888: Pipeline example and contract tests for the document co…
krickert e9b53a7
OPENNLP-1888: Parse per sentence in the adapters, distinguish empty f…
krickert 6b384b6
OPENNLP-1888: Lemmatize per sentence, validate adapter outputs, clear…
krickert 99e0b83
OPENNLP-1888: Add StringUtil.isBlank following the toolkit whitespace…
krickert eb277fb
OPENNLP-1888: Manual chapter, review-convention pass, and key placeme…
krickert 1ba2fcd
OPENNLP-1888: Namespace the standard layer key ids
krickert f8b2ee9
OPENNLP-1888: Declare per-key positional or document scope
krickert b39dd25
OPENNLP-1888: State the index-reference invariants in the specificati…
krickert e4d6ed7
OPENNLP-1888: Document the gold-layer convention
krickert 266e780
OPENNLP-1888: Create toolkit layer keys through namespace-applying fa…
krickert d43c1ec
OPENNLP-1888: Javadoc container overrides and cite the pipeline examp…
krickert 0453217
OPENNLP-1888: Anchor the Document thread-safety note to immutability
krickert 15102d7
OPENNLP-1888: Address review: shared annotator helpers, hardened cont…
krickert e906225
OPENNLP-1888: Address review: fold duplicate test fixtures and pin nu…
krickert 6896361
OPENNLP-1888: Name the null argument, avoid per-call key set wrapping…
krickert 6084309
OPENNLP-1888: Rework the document chapter after docs review
krickert 9442b91
OPENNLP-1888: Pin the Document.merge contract
krickert 919cf59
OPENNLP-1888: Implement Document.merge as a default method
krickert aa5aeba
OPENNLP-1888: Document the parallel fan-out join in the manual
krickert adb15b4
OPENNLP-1888: Pin the duplicate-layer policy contract for merge
krickert 1297f72
OPENNLP-1888: Add a duplicate-layer policy to Document.merge
krickert 3245eff
OPENNLP-1888: Render the layer figure monospaced and tighten the prose
krickert 4f5c0ca
OPENNLP-1888: Pin a differing-contents message for KEEP_EQUAL rejection
krickert 844128f
OPENNLP-1888: Name the differing contents when KEEP_EQUAL rejects a l…
krickert 75fcac0
OPENNLP-1888: Pin what layer equality compares for KEEP_EQUAL
krickert 562af87
OPENNLP-1888: Pin merge semantics across Document implementations
krickert 09cd47d
OPENNLP-1888: Merge with a single map copy in ImmutableDocument
krickert 0705c5d
OPENNLP-1888: State span offsets in UTF-16 units in the manual
krickert 550bc44
OPENNLP-1888: Pin that adapters name themselves by class name
krickert f4dd343
OPENNLP-1888: Name adapters by their simple class name
krickert e380352
OPENNLP-1888: Move concrete annotators to runtime
krickert 22780da
OPENNLP-1888: Place annotators with their components
krickert 65c215b
OPENNLP-1888: Add a ChunkerAnnotator providing a chunks layer
krickert 1b24dad
OPENNLP-1888: Add a ParserAnnotator providing a phrases layer
krickert e37f9c1
OPENNLP-1888: List the chunker and parser adapters in the manual
krickert ea33ac8
OPENNLP-1888: Document the purpose, layer contents, and lineage of th…
krickert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
68 changes: 68 additions & 0 deletions
68
opennlp-api/src/main/java/opennlp/tools/document/Annotation.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package opennlp.tools.document; | ||
|
|
||
| import opennlp.tools.util.Span; | ||
|
|
||
| /** | ||
| * One annotation of a {@link Document}: a typed value anchored to a {@link Span} of the | ||
| * document's original text, or a span-less value under a | ||
| * {@link LayerKey.Scope#DOCUMENT document-scoped} key. | ||
| * | ||
| * <p>The span always refers to the text the document was created with, never to a | ||
| * normalized or otherwise derived form, so any annotation can be highlighted in what the | ||
| * caller supplied. Whether a span is present is decided by the layer key's scope, not | ||
| * per annotation: the container rejects a span-less annotation under a positional key | ||
| * and a spanned annotation under a document-scoped key. Annotations that need to | ||
| * reference other annotations, for example a dependency arc naming its head token, do | ||
| * so by the index of the target annotation within its layer, never by object | ||
| * identity.</p> | ||
| * | ||
| * @param span The location of the annotation in the original text, or {@code null} for | ||
| * a value under a document-scoped key. | ||
| * @param value The annotation value. Must not be {@code null}. | ||
| * @param <T> The type of the annotation value. | ||
| * | ||
| * @since 3.0.0 | ||
| */ | ||
| public record Annotation<T>(Span span, T value) { | ||
|
|
||
| /** | ||
| * Validates the annotation. | ||
| * | ||
| * @throws IllegalArgumentException Thrown if {@code value} is {@code null}. | ||
| */ | ||
| public Annotation { | ||
| if (value == null) { | ||
| throw new IllegalArgumentException("value must not be null"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a span-less annotation for a {@link LayerKey.Scope#DOCUMENT | ||
| * document-scoped} layer. | ||
| * | ||
| * @param value The annotation value. Must not be {@code null}. | ||
| * @param <T> The type of the annotation value. | ||
| * @return An {@link Annotation} without a span. Never {@code null}. | ||
| * @throws IllegalArgumentException Thrown if {@code value} is {@code null}. | ||
| */ | ||
| public static <T> Annotation<T> of(T value) { | ||
| return new Annotation<>(null, value); | ||
| } | ||
| } |
196 changes: 196 additions & 0 deletions
196
opennlp-api/src/main/java/opennlp/tools/document/Document.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package opennlp.tools.document; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * An offset-anchored annotation container: the original text of one document plus any | ||
| * number of typed annotation layers over it. | ||
| * | ||
| * <p>A layer is a list of {@link Annotation annotations} identified by a | ||
| * {@link LayerKey}. The container itself knows nothing about specific layers; every | ||
| * analysis capability contributes its results as one more layer without any change to | ||
| * this interface, which is what keeps new capabilities additive. All spans refer to | ||
| * {@link #text()} as supplied, never to a derived form. A | ||
| * {@link LayerKey.Scope#DOCUMENT document-scoped} layer carries whole-document values | ||
| * without spans, for example a language id.</p> | ||
| * | ||
| * <p>A document is never modified in place: {@link #with(LayerKey, List)} leaves its | ||
| * receiver untouched and returns a new document. Thread safety is implementation | ||
| * specific.</p> | ||
| * | ||
| * <p>Three invariants make index-based references sound. A layer preserves its | ||
| * insertion order, and the container never sorts or reorders it. A layer is immutable | ||
| * once added: the returned lists reject modification and are detached from the | ||
| * caller's input list. Providing a layer that already exists is rejected loudly: the | ||
| * add is once-only, and the exception names the offending key. An annotation that | ||
| * references another annotation by its index within a layer, for example a dependency | ||
| * arc naming its head token, therefore stays valid for the lifetime of the | ||
| * document.</p> | ||
| * | ||
| * @since 3.0.0 | ||
| */ | ||
| public interface Document { | ||
|
|
||
| /** | ||
| * Creates an empty {@link Document} over a text. The returned document is immutable | ||
| * and safe to share between threads: it captures the text's content at construction, | ||
| * so later changes to a mutable {@code CharSequence} do not reach the document. | ||
| * | ||
| * @param text The original document text. Must not be {@code null}. | ||
| * @return A {@link Document} without any layers. Never {@code null}. | ||
| * @throws IllegalArgumentException Thrown if {@code text} is {@code null}. | ||
| */ | ||
| static Document of(CharSequence text) { | ||
| return ImmutableDocument.empty(text); | ||
| } | ||
|
|
||
| /** | ||
| * @return The original text of the document. Never {@code null}. | ||
| */ | ||
| CharSequence text(); | ||
|
|
||
| /** | ||
| * Retrieves the annotations of one layer. | ||
| * | ||
| * @param layer The layer to read. Must not be {@code null}. | ||
| * @param <T> The type of the layer's annotation values. | ||
| * @return The layer's annotations in their layer order, or an empty list when the | ||
| * layer is absent. Never {@code null}; the list is unmodifiable. | ||
| * @throws IllegalArgumentException Thrown if {@code layer} is {@code null}. | ||
| */ | ||
| <T> List<Annotation<T>> get(LayerKey<T> layer); | ||
|
|
||
| /** | ||
| * @return The keys of all layers present on the document. Never {@code null}; the set | ||
| * is unmodifiable. | ||
| */ | ||
| Set<LayerKey<?>> layers(); | ||
|
|
||
| /** | ||
| * Returns a new document with one layer added. | ||
| * | ||
| * @param layer The key of the layer to add. Must not be {@code null} and must not | ||
| * already be present. | ||
| * @param annotations The annotations of the layer. Must not be {@code null}, must not | ||
| * contain {@code null}, and every value must be assignable to the | ||
| * layer's type. Under a positional key every annotation must carry | ||
| * a span within the text bounds; under a document-scoped key no | ||
| * annotation may carry a span. | ||
| * @param <T> The type of the layer's annotation values. | ||
| * @return A new {@link Document} sharing this document's text and existing layers. | ||
| * Never {@code null}. | ||
| * @throws IllegalArgumentException Thrown if any of the above constraints is violated. | ||
| */ | ||
| <T> Document with(LayerKey<T> layer, List<Annotation<T>> annotations); | ||
|
|
||
| /** | ||
| * How {@link #merge(Document, DuplicateLayerPolicy)} treats a layer key that is | ||
| * present on both documents. | ||
| */ | ||
| enum DuplicateLayerPolicy { | ||
|
|
||
| /** Reject any layer key present on both documents. */ | ||
| REJECT, | ||
|
|
||
| /** | ||
| * Keep one copy of a layer key present on both documents when the two layers are | ||
| * structurally equal, for example when two parallel branches ran the same | ||
| * tokenizer. Layers whose contents differ are rejected as with {@link #REJECT}. | ||
| * Equality is {@link Annotation} equality: spans compare by offsets and type, | ||
| * never by probability, and values by their own {@code equals}. | ||
| */ | ||
| KEEP_EQUAL | ||
| } | ||
|
|
||
| /** | ||
| * Returns a new document combining this document's layers with another document's | ||
| * layers over the same text, joining documents grown independently, for example by | ||
| * pipelines that ran in parallel. | ||
| * | ||
| * @param other The document whose layers are added on top of this document's layers. | ||
| * Must not be {@code null}, must carry the same text content, and must | ||
| * not provide a layer this document already has. | ||
| * @return A new {@link Document} carrying the layers of both documents. Never | ||
| * {@code null}; both source documents are left untouched. | ||
| * @throws IllegalArgumentException Thrown if {@code other} is {@code null}, if its | ||
| * text content differs, or if a layer key is present on both documents; the | ||
| * exception names the offending key. | ||
| */ | ||
| default Document merge(Document other) { | ||
| return merge(other, DuplicateLayerPolicy.REJECT); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a new document combining this document's layers with another document's | ||
| * layers over the same text, resolving duplicate layer keys with | ||
| * {@code duplicateLayers}. | ||
| * | ||
| * @param other The document whose layers are added on top of this document's layers. | ||
| * Must not be {@code null} and must carry the same text content. | ||
| * @param duplicateLayers How to treat a layer key present on both documents. Must | ||
| * not be {@code null}. | ||
| * @return A new {@link Document} carrying the layers of both documents. Never | ||
| * {@code null}; both source documents are left untouched. | ||
| * @throws IllegalArgumentException Thrown if either argument is {@code null}, if the | ||
| * text content differs, or if a layer key is present on both documents and | ||
| * the policy does not keep it; the exception names the offending key. | ||
| */ | ||
| default Document merge(Document other, DuplicateLayerPolicy duplicateLayers) { | ||
| if (other == null) { | ||
| throw new IllegalArgumentException("other must not be null"); | ||
| } | ||
| if (duplicateLayers == null) { | ||
| throw new IllegalArgumentException("duplicateLayers must not be null"); | ||
| } | ||
| if (!text().toString().contentEquals(other.text())) { | ||
| throw new IllegalArgumentException( | ||
| "merge requires both documents to carry the same text"); | ||
| } | ||
| Document merged = this; | ||
| for (final LayerKey<?> layer : other.layers()) { | ||
| if (duplicateLayers == DuplicateLayerPolicy.KEEP_EQUAL | ||
| && merged.layers().contains(layer)) { | ||
| if (layersEqual(merged, layer, other)) { | ||
| continue; | ||
| } | ||
| throw new IllegalArgumentException( | ||
| "layer is present on both documents with differing contents: " + layer); | ||
| } | ||
| merged = addLayer(merged, layer, other); | ||
| } | ||
| return merged; | ||
| } | ||
|
|
||
| /** | ||
| * @return Whether the two documents carry structurally equal contents for the layer. | ||
| */ | ||
| private static <T> boolean layersEqual(Document first, LayerKey<T> layer, Document second) { | ||
| return first.get(layer).equals(second.get(layer)); | ||
| } | ||
|
|
||
| /** | ||
| * Adds one layer of {@code from} to {@code base} through {@link #with(LayerKey, List)}, | ||
| * capturing the key's value type. | ||
| */ | ||
| private static <T> Document addLayer(Document base, LayerKey<T> layer, Document from) { | ||
| return base.with(layer, from.get(layer)); | ||
| } | ||
| } | ||
125 changes: 125 additions & 0 deletions
125
opennlp-api/src/main/java/opennlp/tools/document/DocumentAnalyzer.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package opennlp.tools.document; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Runs a fixed sequence of {@link DocumentAnnotator annotators} over a text, producing | ||
| * one {@link Document} that carries every step's layers. | ||
| * | ||
| * <p>The pipeline is validated at build time: every annotator's required layers must be | ||
| * provided by an earlier annotator, and no two annotators may provide the same layer, so | ||
| * a misordered or conflicting pipeline fails when it is assembled rather than midway | ||
| * through a document. The analyzer holds no per-call state; it is as thread-safe as the | ||
| * annotators it is built from.</p> | ||
| * | ||
| * @since 3.0.0 | ||
| */ | ||
| public final class DocumentAnalyzer { | ||
|
|
||
| private final List<DocumentAnnotator> annotators; | ||
|
|
||
| private DocumentAnalyzer(List<DocumentAnnotator> annotators) { | ||
| this.annotators = annotators; | ||
| } | ||
|
|
||
| /** | ||
| * @return A new {@link Builder}. Never {@code null}. | ||
| */ | ||
| public static Builder builder() { | ||
| return new Builder(); | ||
| } | ||
|
|
||
| /** | ||
| * Analyzes a text by running every annotator in order. | ||
| * | ||
| * @param text The original document text. Must not be {@code null}. | ||
| * @return The annotated {@link Document}. Never {@code null}. | ||
| * @throws IllegalArgumentException Thrown if {@code text} is {@code null}. | ||
| */ | ||
| public Document analyze(CharSequence text) { | ||
| Document document = Document.of(text); | ||
| for (final DocumentAnnotator annotator : annotators) { | ||
| document = annotator.annotate(document); | ||
| } | ||
| return document; | ||
| } | ||
|
|
||
| /** | ||
| * Assembles a {@link DocumentAnalyzer} from annotators in execution order. | ||
| */ | ||
| public static final class Builder { | ||
|
|
||
| private final List<DocumentAnnotator> annotators = new ArrayList<>(); | ||
|
|
||
| private Builder() { | ||
| } | ||
|
|
||
| /** | ||
| * Appends an annotator to the pipeline. | ||
| * | ||
| * @param annotator The annotator to run after the ones already added. Must not be | ||
| * {@code null}. | ||
| * @return This {@link Builder}. Never {@code null}. | ||
| * @throws IllegalArgumentException Thrown if {@code annotator} is {@code null}. | ||
| */ | ||
| public Builder add(DocumentAnnotator annotator) { | ||
| if (annotator == null) { | ||
| throw new IllegalArgumentException("annotator must not be null"); | ||
| } | ||
| annotators.add(annotator); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Validates the pipeline and builds the analyzer. | ||
| * | ||
| * @return A {@link DocumentAnalyzer}. Never {@code null}. | ||
| * @throws IllegalArgumentException Thrown if the pipeline is empty, an annotator | ||
| * requires a layer no earlier annotator provides, or two annotators provide | ||
| * the same layer. | ||
| */ | ||
| public DocumentAnalyzer build() { | ||
| if (annotators.isEmpty()) { | ||
| throw new IllegalArgumentException("a pipeline needs at least one annotator"); | ||
| } | ||
| final Map<LayerKey<?>, Integer> providers = new HashMap<>(); | ||
| for (int position = 0; position < annotators.size(); position++) { | ||
| final DocumentAnnotator annotator = annotators.get(position); | ||
| for (final LayerKey<?> required : annotator.requires()) { | ||
| if (!providers.containsKey(required)) { | ||
| throw new IllegalArgumentException("annotator " + annotator | ||
| + " requires layer " + required + ", which no earlier annotator provides"); | ||
| } | ||
| } | ||
| for (final LayerKey<?> provided : annotator.provides()) { | ||
| final Integer earlier = providers.putIfAbsent(provided, position); | ||
| if (earlier != null) { | ||
| throw new IllegalArgumentException("annotators at positions " + earlier | ||
| + " and " + position + " both provide layer " + provided); | ||
| } | ||
| } | ||
| } | ||
| return new DocumentAnalyzer(List.copyOf(annotators)); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.