Skip to content

Commit 64a8439

Browse files
OPENNLP-587: Document the Entity Linker framework (#1169)
1 parent 47367dd commit 64a8439

4 files changed

Lines changed: 399 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
18+
package opennlp.tools.entitylinker;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import opennlp.tools.namefind.TokenNameFinder;
27+
import opennlp.tools.sentdetect.SentenceDetector;
28+
import opennlp.tools.tokenize.Tokenizer;
29+
import opennlp.tools.tokenize.WhitespaceTokenizer;
30+
import opennlp.tools.util.Span;
31+
32+
import static org.junit.jupiter.api.Assertions.assertAll;
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
35+
public class EntityLinkerManualExamplesTest {
36+
37+
private static final String DOCUMENT_TEXT = "Visit Paris\nThen New York";
38+
39+
@Test
40+
void testNameTokenIndexesBecomeDocumentOffsets() {
41+
EntityLinker<LinkedSpan<GazetteerLink>> linker = new GazetteerLinker(Map.of(
42+
"Paris", new GazetteerLink("Q90", "Paris"),
43+
"New York", new GazetteerLink("Q60", "New York")));
44+
45+
List<? extends Span> results = linkEntities(DOCUMENT_TEXT, new FixedSentenceDetector(),
46+
WhitespaceTokenizer.INSTANCE, new FixedNameFinder(), linker);
47+
48+
assertEquals(2, results.size());
49+
assertLinkedSpan((LinkedSpan<?>) results.get(0), 6, 11, 0, "Paris", "Q90");
50+
assertLinkedSpan((LinkedSpan<?>) results.get(1), 17, 25, 1, "New York", "Q60");
51+
}
52+
53+
private static List<? extends Span> linkEntities(String documentText,
54+
SentenceDetector sentenceDetector, Tokenizer tokenizer,
55+
TokenNameFinder nameFinder, EntityLinker<? extends Span> linker) {
56+
57+
Span[] sentences = sentenceDetector.sentPosDetect(documentText);
58+
Span[][] tokensBySentence = new Span[sentences.length][];
59+
Span[][] namesBySentence = new Span[sentences.length][];
60+
61+
for (int i = 0; i < sentences.length; i++) {
62+
Span sentence = sentences[i];
63+
String sentenceText = sentence.getCoveredText(documentText).toString();
64+
Span[] relativeTokenSpans = tokenizer.tokenizePos(sentenceText);
65+
String[] tokenTexts = Span.spansToStrings(relativeTokenSpans, sentenceText);
66+
67+
namesBySentence[i] = nameFinder.find(tokenTexts);
68+
tokensBySentence[i] = new Span[relativeTokenSpans.length];
69+
for (int j = 0; j < relativeTokenSpans.length; j++) {
70+
// Tokenizer offsets are sentence-relative; EntityLinker expects document offsets.
71+
tokensBySentence[i][j] = new Span(relativeTokenSpans[j], sentence.getStart());
72+
}
73+
}
74+
75+
return linker.find(documentText, sentences, tokensBySentence, namesBySentence);
76+
}
77+
78+
private static void assertLinkedSpan(LinkedSpan<?> span, int start, int end,
79+
int sentenceId, String searchTerm, String itemId) {
80+
assertAll(
81+
() -> assertEquals(start, span.getStart()),
82+
() -> assertEquals(end, span.getEnd()),
83+
() -> assertEquals("location", span.getType()),
84+
() -> assertEquals(sentenceId, span.getSentenceid()),
85+
() -> assertEquals(searchTerm, span.getSearchTerm()),
86+
() -> assertEquals(itemId, span.getLinkedEntries().get(0).getItemID()));
87+
}
88+
89+
private static final class FixedSentenceDetector implements SentenceDetector {
90+
91+
@Override
92+
public String[] sentDetect(CharSequence text) {
93+
return Span.spansToStrings(sentPosDetect(text), text);
94+
}
95+
96+
@Override
97+
public Span[] sentPosDetect(CharSequence text) {
98+
return new Span[] {new Span(0, 11), new Span(12, 25)};
99+
}
100+
}
101+
102+
private static final class FixedNameFinder implements TokenNameFinder {
103+
104+
@Override
105+
public Span[] find(String[] tokens) {
106+
if (tokens.length == 2) {
107+
return new Span[] {new Span(1, 2, "location")};
108+
}
109+
return new Span[] {new Span(1, 3, "location")};
110+
}
111+
112+
@Override
113+
public void clearAdaptiveData() {
114+
// The fixed test implementation does not retain adaptive data.
115+
}
116+
}
117+
118+
private static final class GazetteerLink extends BaseLink {
119+
120+
private GazetteerLink(String id, String name) {
121+
super(null, id, name, "location");
122+
}
123+
}
124+
125+
private static final class GazetteerLinker
126+
implements EntityLinker<LinkedSpan<GazetteerLink>> {
127+
128+
private final Map<String, GazetteerLink> entries;
129+
130+
private GazetteerLinker(Map<String, GazetteerLink> entries) {
131+
this.entries = entries;
132+
}
133+
134+
@Override
135+
public void init(EntityLinkerProperties properties) {
136+
// The test linker receives its entries in memory and needs no external initialization.
137+
}
138+
139+
@Override
140+
public List<LinkedSpan<GazetteerLink>> find(String documentText,
141+
Span[] sentences, Span[][] tokensBySentence, Span[][] namesBySentence) {
142+
List<LinkedSpan<GazetteerLink>> results = new ArrayList<>();
143+
for (int i = 0; i < sentences.length; i++) {
144+
results.addAll(find(documentText, sentences, tokensBySentence,
145+
namesBySentence, i));
146+
}
147+
return results;
148+
}
149+
150+
@Override
151+
public List<LinkedSpan<GazetteerLink>> find(String documentText,
152+
Span[] sentences, Span[][] tokensBySentence, Span[][] namesBySentence,
153+
int sentenceIndex) {
154+
List<LinkedSpan<GazetteerLink>> results = new ArrayList<>();
155+
Span[] tokens = tokensBySentence[sentenceIndex];
156+
157+
for (Span name : namesBySentence[sentenceIndex]) {
158+
// Name spans contain token indexes, so map their boundaries through the token spans.
159+
int start = tokens[name.getStart()].getStart();
160+
int end = tokens[name.getEnd() - 1].getEnd();
161+
String searchTerm = documentText.substring(start, end);
162+
GazetteerLink match = entries.get(searchTerm);
163+
164+
if (match != null) {
165+
ArrayList<GazetteerLink> matches = new ArrayList<>();
166+
matches.add(match);
167+
LinkedSpan<GazetteerLink> linkedSpan =
168+
new LinkedSpan<>(matches, start, end, name.getType());
169+
linkedSpan.setSentenceid(sentenceIndex);
170+
linkedSpan.setSearchTerm(searchTerm);
171+
results.add(linkedSpan);
172+
}
173+
}
174+
return results;
175+
}
176+
}
177+
}

opennlp-docs/src/docbkx/cli.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4381,6 +4381,14 @@ Arguments description:
43814381

43824382
<para>Links an entity to an external data set</para>
43834383

4384+
<para>
4385+
The <emphasis>model</emphasis> argument is an Entity Linker properties file. The command loads
4386+
the implementation named by <literal>linker.location</literal> and reads tokenized,
4387+
named-entity-annotated sentences from standard input. See the
4388+
<link linkend="tools.entitylinker.commandline">Entity Linker chapter</link> for the input format
4389+
and configuration details.
4390+
</para>
4391+
43844392
<screen>
43854393
<![CDATA[Usage: opennlp EntityLinker model < sentences]]>
43864394
</screen>

0 commit comments

Comments
 (0)