Kristian Rickert created OPENNLP-1888:
-----------------------------------------
Summary: FEEDBACK / DESIGN : creating a Document annotation
container: typed, offset-anchored layer s so every future analysis result is an
additive 3.x feature
Key: OPENNLP-1888
URL: https://issues.apache.org/jira/browse/OPENNLP-1888
Project: OpenNLP
Issue Type: Improvement
Affects Versions: 3.0.0-M4
Reporter: Kristian Rickert
Assignee: Kristian Rickert
Designing the document generic type in Java:
To be explicit, because it is the first question this proposal should answer:
*this ticket deprecates nothing and replaces nothing.* It is something I've
brought up in the chats - the importance of now being the time to land API
changes.
h2. Motivation
OpenNLP's Java API is tool-by-tool: a tokenizer returns spans, a tagger returns
strings, a name finder returns spans, and the caller reassembles the pieces by
hand, differently in every application. Each new capability adds another entry
point and result type, so the cost compounds. I ran into this head-on writing
the gRPC layer, which is mostly mapping tool outputs into a common document
structure that the Java API itself never defines. Since 3.0 has not landed yet,
this feels like the right time to discuss adding that layer.
We already have two data points. {{Term}} validated the layered-container idea,
but it is token-scoped, string-valued, and keyed by a closed enum, so it cannot
hold anything structural. The emoji annotation layer (OPENNLP-1870) is the
first structured result that did not fit, and it correctly became a separate
accessor beside {{{}Term{}}}. Every future structured result (dependency arcs,
coreference, relations, money, PII) would repeat that pattern, each growing its
own parallel surface, while the combined-document view still would not exist.
The fix is one small container in {{{}opennlp-api{}}}: typed annotation layers
over the original text, identified by open-ended keys rather than an enum. Any
future capability then becomes three additions that touch no existing type: a
payload record, a layer key, and an annotator. Since anything published in 3.x
is frozen until 4.0, shipping this in 3.0 is what keeps the parallel surfaces
from ever accumulating.
h2. Design
Four public types, all in a new package in {{{}opennlp-api{}}}:
{code:java}
public interface Document {
CharSequence text(); // the original text, never a
normalized form
<T> List<Annotation<T>> get(LayerKey<T> layer); // empty list when the layer
is absent
Set<LayerKey<?>> layers();
}
public record Annotation<T>(Span span, T value) { }
public final class LayerKey<T> {
public static <T> LayerKey<T> of(String id, Class<T> type) { ... }
}
public interface DocumentAnnotator {
Document annotate(Document document); // returns a new document
with layers added
Set<LayerKey<?>> requires();
Set<LayerKey<?>> provides();
}
{code}
Standard keys for today's results (sentences, tokens, tags, chunks, entities)
ship with thin annotators wrapping the existing task interfaces, which stay
exactly as they are. A {{DocumentAnalyzer}} builder assembles annotators into a
pipeline ordered by {{{}requires(){}}}/{{{}provides(){}}}, mirroring
{{{}TermAnalyzer{}}}.
Rules that protect the additive property:
* Spans always point into the original text; normalization maps back through
{{{}Alignment{}}}.
* Layer identity is an open key, never an enum; payload types live in the
producing feature's package, and the container never imports or enumerates them.
* Annotations reference other annotations by layer and index (a dependency arc
names token indices), keeping documents immutable and mappable to any encoding.
* No new dependencies, and no serialization framework in {{{}opennlp-api{}}};
encodings live where their dependencies already are.
* Evolution by default methods and new types only; nothing sealed, no abstract
classes at the seam.
h2. Coexistence with the task-level API
The task interfaces are not a legacy layer. They remain the primary API for
single-task use, with no deprecation and no planned removal; annotators
delegate to them, so there is one behavior reachable two ways (the same
relationship {{TermAnalyzer}} has to the normalizer rungs). Deprecation is
reserved for the rare surface a layer supersedes outright, and even then
removal waits for a major, so existing call sites compile and run for the life
of 3.x. Models are unaffected: nothing about training or decoding changes.
h2. Validation before freezeĀ
# The existing analyze pipeline (sentences, tokens, tags, entities) expressed
as annotators, proving the adapters and ordering model.
# Dependency parsing as the first graph-shaped layer: its values reference
other annotations, so it exercises the cross-layer rules that coreference and
relation extraction will inherit. A transition-based parser over the existing
maxent machinery, trained from CoNLL-U treebanks, suffices and is a wanted
feature in its own right.
h2. Out of scope
Downstream features themselves (each is its own ticket), changes to any
existing task interface or result type, and document serialization.
h2. Acceptance criteria
* Container types in {{{}opennlp-api{}}}, no new dependencies, full javadoc;
existing tools populate a document through adapters unchanged.
* Both validation consumers pass tests covering cross-layer references and
span fidelity, including a normalization-shifted case.
* A new layer can be added in a test without touching the container's package.
* Binary compatibility from the previous release is verified mechanically in
CI.
* No existing type is modified or deprecated; the task-level API remains fully
supported.
h4. Guardrail Note:
The container's package must never grow knowledge of specific layers. The
moment it imports a payload type or enumerates layer identities, the additive
property is lost and every future feature becomes an API change again.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)