Skip to content

Commit 8878e03

Browse files
committed
OPENNLP-1880: Lexical knowledge base seam with WN-LMF and WNDB readers and a Morphy lemmatizer
Adds the LexicalKnowledgeBase contract in opennlp.tools.wordnet and the opennlp-wordnet module implementing it twice: WnLmfReader for WN-LMF XML and WndbReader for the legacy WNDB database files, with reader-equivalence coverage over miniature fixtures of both formats. The WN-LMF reader skips DOCTYPE declarations unresolved with DTD support off, so Open English WordNet releases parse unmodified while entity expansion stays closed; the WNDB fixtures are pinned to LF so their embedded byte offsets survive Windows checkout. The Morphy lemmatizer resolves inflected forms through suffix rules and the format's exception lists. Null arguments fail loudly with IllegalArgumentException, malformed data raises the checked InvalidFormatException, and the public seam carries no brand name: WordNet stays in the names of the classes that actually read WordNet formats.
1 parent 6038bf5 commit 8878e03

40 files changed

Lines changed: 4271 additions & 0 deletions
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package opennlp.tools.wordnet;
18+
19+
import java.util.List;
20+
import java.util.Optional;
21+
22+
/**
23+
* Lemma and synset lookup over a loaded lexical-semantic resource in the WordNet family. Synset
24+
* identity is opaque and source-qualified (see {@link Synset#id()}). Lookups return their matches
25+
* in the source's sense order and never return {@code null}.
26+
*
27+
* <p>Lemma matching semantics are the implementation's concern. The reference implementations
28+
* match case-insensitively (case folding with the root locale) and treat the underscore some
29+
* formats store in multiword lemmas as a space; an implementation with different semantics must
30+
* document them. Returned {@link Synset#lemmas() lemmas} preserve the source's written forms,
31+
* with spaces in multiword lemmas.</p>
32+
*
33+
* <p>Implementations must be immutable and thread-safe after loading: one instance is meant to
34+
* be shared across an application's threads for concurrent lookups.</p>
35+
*/
36+
public interface LexicalKnowledgeBase {
37+
38+
/**
39+
* Finds the synsets containing a lemma with a part of speech, in the source's sense order
40+
* (the most salient sense first when the source ranks senses).
41+
*
42+
* @param lemma The lemma to look up. Must not be {@code null}.
43+
* @param pos The part of speech to look it up as. Must not be {@code null}.
44+
* @return The matching synsets, never {@code null}; empty when the lexicon does not contain
45+
* the lemma with that part of speech.
46+
* @throws IllegalArgumentException Thrown if {@code lemma} or {@code pos} is {@code null}.
47+
*/
48+
List<Synset> lookup(String lemma, WordNetPOS pos);
49+
50+
/**
51+
* Finds a synset by its opaque identifier.
52+
*
53+
* @param synsetId The synset identifier, as minted by this lexicon. Must not be {@code null}.
54+
* @return The synset, or empty when this lexicon has no synset with that identifier.
55+
* @throws IllegalArgumentException Thrown if {@code synsetId} is {@code null}.
56+
*/
57+
Optional<Synset> synset(String synsetId);
58+
59+
/**
60+
* Navigates one typed relation from a synset.
61+
*
62+
* @param synsetId The source synset identifier. Must not be {@code null}.
63+
* @param relation The relation type to follow. Must not be {@code null}.
64+
* @return The target synset ids in source order, never {@code null}; empty when the synset is
65+
* unknown or has no relation of that type.
66+
* @throws IllegalArgumentException Thrown if {@code synsetId} or {@code relation} is
67+
* {@code null}.
68+
*/
69+
default List<String> related(String synsetId, WordNetRelation relation) {
70+
if (relation == null) {
71+
throw new IllegalArgumentException("Relation must not be null");
72+
}
73+
return synset(synsetId).map(s -> s.related(relation)).orElse(List.of());
74+
}
75+
76+
/**
77+
* Tests whether the lexicon contains a lemma with a part of speech. This is the membership
78+
* check morphological rules validate their candidates against; implementations may override
79+
* it with a cheaper check than {@link #lookup(String, WordNetPOS)}.
80+
*
81+
* @param lemma The lemma to test. Must not be {@code null}.
82+
* @param pos The part of speech to test it as. Must not be {@code null}.
83+
* @return {@code true} if the lexicon contains the lemma with that part of speech.
84+
* @throws IllegalArgumentException Thrown if {@code lemma} or {@code pos} is {@code null}.
85+
*/
86+
default boolean contains(String lemma, WordNetPOS pos) {
87+
return !lookup(lemma, pos).isEmpty();
88+
}
89+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package opennlp.tools.wordnet;
18+
19+
import java.util.Collections;
20+
import java.util.EnumMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import opennlp.tools.commons.ThreadSafe;
25+
26+
/**
27+
* One synonym set: a single lexicalized concept with its member lemmas, gloss, and typed
28+
* relations to other synsets.
29+
*
30+
* <p>The {@link #id() id} is an opaque, source-qualified string minted by the reader that
31+
* produced the synset; consumers must not parse it, only pass it back to
32+
* {@link LexicalKnowledgeBase#synset(String)} and compare it for equality. Relations map each
33+
* {@link WordNetRelation} present on this synset to the target synset ids in source order.</p>
34+
*
35+
* <p>Instances are immutable and thread-safe: the list and map components are defensively
36+
* copied to immutable views at construction.</p>
37+
*
38+
* @param id The opaque, source-qualified synset identifier. Must not be {@code null} or
39+
* empty.
40+
* @param pos The part of speech. Must not be {@code null}.
41+
* @param lemmas The member lemmas in source order, human-readable (multiword lemmas use
42+
* spaces, not the underscores some formats store). Must not be {@code null} or
43+
* empty, and must not contain {@code null} or empty elements.
44+
* @param gloss The definition text, possibly empty when the source has none. Must not be
45+
* {@code null}.
46+
* @param relations The typed relations, each mapping to the target synset ids in source order.
47+
* Must not be {@code null}; keys must not be {@code null}; each value must be
48+
* a non-empty list of non-{@code null}, non-empty target ids.
49+
*/
50+
@ThreadSafe
51+
public record Synset(
52+
String id,
53+
WordNetPOS pos,
54+
List<String> lemmas,
55+
String gloss,
56+
Map<WordNetRelation, List<String>> relations) {
57+
58+
/**
59+
* Creates a synset.
60+
*
61+
* @throws IllegalArgumentException Thrown if any component violates its documented constraint.
62+
*/
63+
public Synset {
64+
if (id == null || id.isEmpty()) {
65+
throw new IllegalArgumentException("Id must not be null or empty");
66+
}
67+
if (pos == null) {
68+
throw new IllegalArgumentException("Pos must not be null");
69+
}
70+
if (lemmas == null || lemmas.isEmpty()) {
71+
throw new IllegalArgumentException("Lemmas must not be null or empty for synset " + id);
72+
}
73+
for (final String lemma : lemmas) {
74+
if (lemma == null || lemma.isEmpty()) {
75+
throw new IllegalArgumentException(
76+
"Lemmas must not contain a null or empty element for synset " + id);
77+
}
78+
}
79+
if (gloss == null) {
80+
throw new IllegalArgumentException("Gloss must not be null for synset " + id);
81+
}
82+
if (relations == null) {
83+
throw new IllegalArgumentException("Relations must not be null for synset " + id);
84+
}
85+
final Map<WordNetRelation, List<String>> copiedRelations =
86+
new EnumMap<>(WordNetRelation.class);
87+
for (final Map.Entry<WordNetRelation, List<String>> relation : relations.entrySet()) {
88+
if (relation.getKey() == null) {
89+
throw new IllegalArgumentException("Relations must not contain a null key for synset " + id);
90+
}
91+
final List<String> targets = relation.getValue();
92+
if (targets == null || targets.isEmpty()) {
93+
throw new IllegalArgumentException("Relation " + relation.getKey()
94+
+ " must map to a non-empty target list for synset " + id);
95+
}
96+
for (final String target : targets) {
97+
if (target == null || target.isEmpty()) {
98+
throw new IllegalArgumentException("Relation " + relation.getKey()
99+
+ " must not contain a null or empty target id for synset " + id);
100+
}
101+
}
102+
copiedRelations.put(relation.getKey(), List.copyOf(targets));
103+
}
104+
lemmas = List.copyOf(lemmas);
105+
relations = Collections.unmodifiableMap(copiedRelations);
106+
}
107+
108+
/**
109+
* Finds the target synset ids of one relation type.
110+
*
111+
* @param relation The relation type. Must not be {@code null}.
112+
* @return The target synset ids in source order, never {@code null}; empty when this synset
113+
* has no relation of that type.
114+
* @throws IllegalArgumentException Thrown if {@code relation} is {@code null}.
115+
*/
116+
public List<String> related(WordNetRelation relation) {
117+
if (relation == null) {
118+
throw new IllegalArgumentException("Relation must not be null");
119+
}
120+
final List<String> targets = relations.get(relation);
121+
return targets == null ? List.of() : targets;
122+
}
123+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package opennlp.tools.wordnet;
18+
19+
/**
20+
* The four parts of speech a wordnet-style lexicon distinguishes.
21+
*
22+
* <p>The enum carries none of the single-letter codes the on-disk formats use; readers own the
23+
* mapping from their format's codes to these values. Adjective satellites normalize to
24+
* {@link #ADJECTIVE}, with the cluster structure preserved through
25+
* {@link WordNetRelation#SIMILAR_TO}.</p>
26+
*/
27+
public enum WordNetPOS {
28+
29+
/** Nouns. */
30+
NOUN,
31+
32+
/** Verbs. */
33+
VERB,
34+
35+
/** Adjectives, including adjective satellites. */
36+
ADJECTIVE,
37+
38+
/** Adverbs. */
39+
ADVERB
40+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package opennlp.tools.wordnet;
18+
19+
/**
20+
* The typed relations a wordnet-style lexicon draws between {@link Synset synsets}. Readers map
21+
* their source format's relation names onto these values.
22+
*
23+
* <p>Relations that a source format draws between individual word senses (antonymy and
24+
* derivation, for example) surface here at the synset level: the synset containing the source
25+
* sense carries the relation to the synset containing the target sense.</p>
26+
*/
27+
public enum WordNetRelation {
28+
29+
/** Opposition in meaning, for example between the adjectives for tall and short. */
30+
ANTONYM,
31+
32+
/** The more general concept: a dog is a kind of canid. */
33+
HYPERNYM,
34+
35+
/** The class a named instance belongs to: a specific river is an instance of river. */
36+
INSTANCE_HYPERNYM,
37+
38+
/** The more specific concept: canid has the hyponym dog. */
39+
HYPONYM,
40+
41+
/** A named instance of this class. */
42+
INSTANCE_HYPONYM,
43+
44+
/** The group this synset is a member of. */
45+
MEMBER_HOLONYM,
46+
47+
/** The whole this synset is a substance of. */
48+
SUBSTANCE_HOLONYM,
49+
50+
/** The whole this synset is a part of. */
51+
PART_HOLONYM,
52+
53+
/** A member of this group. */
54+
MEMBER_MERONYM,
55+
56+
/** A substance this synset is made of. */
57+
SUBSTANCE_MERONYM,
58+
59+
/** A part of this synset. */
60+
PART_MERONYM,
61+
62+
/** The attribute a value expresses, or a value of this attribute. */
63+
ATTRIBUTE,
64+
65+
/** A derivationally related form, typically across parts of speech. */
66+
DERIVATIONALLY_RELATED,
67+
68+
/** An action entailed by this verb: snoring entails sleeping. */
69+
ENTAILMENT,
70+
71+
/** The verb that entails this one; the inverse of {@link #ENTAILMENT}. */
72+
ENTAILED_BY,
73+
74+
/** An effect this verb causes. */
75+
CAUSE,
76+
77+
/** The cause of this verb; the inverse of {@link #CAUSE}. */
78+
CAUSED_BY,
79+
80+
/** A related synset worth consulting. */
81+
ALSO_SEE,
82+
83+
/** A verb sense grouped with this one. */
84+
VERB_GROUP,
85+
86+
/** A satellite or head adjective in the same similarity cluster. */
87+
SIMILAR_TO,
88+
89+
/** The verb an adjective is the participle of. */
90+
PARTICIPLE,
91+
92+
/**
93+
* The noun an adjective pertains to, or the adjective an adverb derives from. The source
94+
* formats use one pointer for both directions of derivation, so this value does too.
95+
*/
96+
PERTAINYM,
97+
98+
/** The topical domain this synset belongs to. */
99+
DOMAIN_TOPIC,
100+
101+
/** A synset belonging to this topical domain. */
102+
MEMBER_OF_DOMAIN_TOPIC,
103+
104+
/** The regional domain this synset belongs to. */
105+
DOMAIN_REGION,
106+
107+
/** A synset belonging to this regional domain. */
108+
MEMBER_OF_DOMAIN_REGION,
109+
110+
/** The usage domain this synset belongs to, for example slang or archaism. */
111+
DOMAIN_USAGE,
112+
113+
/** A synset belonging to this usage domain. */
114+
MEMBER_OF_DOMAIN_USAGE
115+
}

0 commit comments

Comments
 (0)