diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/TypeAsSynonymFilter.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/TypeAsSynonymFilter.java
new file mode 100644
index 0000000000..8269d5d2b9
--- /dev/null
+++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/TypeAsSynonymFilter.java
@@ -0,0 +1,80 @@
+/*
+ * 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 org.apache.lucene.analysis.miscellaneous;
+
+import java.io.IOException;
+
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.apache.lucene.util.AttributeSource;
+
+/**
+ * Adds the {@link TypeAttribute#type()} as a synonym,
+ * i.e. another token at the same position, optionally with a specified prefix prepended.
+ */
+public final class TypeAsSynonymFilter extends TokenFilter {
+ private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+ private final TypeAttribute typeAtt = addAttribute(TypeAttribute.class);
+ private final PositionIncrementAttribute posIncrAtt = addAttribute(PositionIncrementAttribute.class);
+ private final String prefix;
+
+ AttributeSource.State savedToken = null;
+
+
+ public TypeAsSynonymFilter(TokenStream input) {
+ this(input, null);
+ }
+
+ /**
+ * @param input input tokenstream
+ * @param prefix Prepend this string to every token type emitted as token text.
+ * If null, nothing will be prepended.
+ */
+ public TypeAsSynonymFilter(TokenStream input, String prefix) {
+ super(input);
+ this.prefix = prefix;
+ }
+
+ @Override
+ public boolean incrementToken() throws IOException {
+ if (savedToken != null) { // Emit last token's type at the same position
+ restoreState(savedToken);
+ savedToken = null;
+ termAtt.setEmpty();
+ if (prefix != null) {
+ termAtt.append(prefix);
+ }
+ termAtt.append(typeAtt.type());
+ posIncrAtt.setPositionIncrement(0);
+ return true;
+ } else if (input.incrementToken()) { // Ho pending token type to emit
+ savedToken = captureState();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ savedToken = null;
+ }
+}
diff --git a/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/TypeAsSynonymFilterFactory.java b/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/TypeAsSynonymFilterFactory.java
new file mode 100644
index 0000000000..69708b7f93
--- /dev/null
+++ b/lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/TypeAsSynonymFilterFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.apache.lucene.analysis.miscellaneous;
+
+import java.util.Map;
+
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.util.TokenFilterFactory;
+
+/**
+ * Factory for {@link TypeAsSynonymFilter}.
+ *
+ * <fieldType name="text_type_as_synonym" class="solr.TextField" positionIncrementGap="100">
+ * <analyzer>
+ * <tokenizer class="solr.UAX29URLEmailTokenizerFactory"/>
+ * <filter class="solr.TypeAsSynonymFilterFactory" prefix="_type_" />
+ * </analyzer>
+ * </fieldType>
+ *
+ *
+ * If the optional {@code prefix} parameter is used, the specified value will be prepended
+ * to the type, e.g. with prefix="_type_", for a token "example.com" with type "<URL>",
+ * the emitted synonym will have text "_type_<URL>".
+ */
+public class TypeAsSynonymFilterFactory extends TokenFilterFactory {
+ private final String prefix;
+
+ public TypeAsSynonymFilterFactory(Map args) {
+ super(args);
+ prefix = get(args, "prefix"); // default value is null
+ if (!args.isEmpty()) {
+ throw new IllegalArgumentException("Unknown parameters: " + args);
+ }
+ }
+
+ @Override
+ public TokenStream create(TokenStream input) {
+ return new TypeAsSynonymFilter(input, prefix);
+ }
+}
diff --git a/lucene/analysis/common/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory b/lucene/analysis/common/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory
index d871ad649d..6dcc81ce04 100644
--- a/lucene/analysis/common/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory
+++ b/lucene/analysis/common/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory
@@ -80,6 +80,7 @@ org.apache.lucene.analysis.miscellaneous.RemoveDuplicatesTokenFilterFactory
org.apache.lucene.analysis.miscellaneous.StemmerOverrideFilterFactory
org.apache.lucene.analysis.miscellaneous.TrimFilterFactory
org.apache.lucene.analysis.miscellaneous.TruncateTokenFilterFactory
+org.apache.lucene.analysis.miscellaneous.TypeAsSynonymFilterFactory
org.apache.lucene.analysis.miscellaneous.WordDelimiterFilterFactory
org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilterFactory
org.apache.lucene.analysis.miscellaneous.ScandinavianFoldingFilterFactory
diff --git a/lucene/analysis/common/src/test/org/apache/lucene/analysis/minhash/MinHashFilterTest.java b/lucene/analysis/common/src/test/org/apache/lucene/analysis/minhash/MinHashFilterTest.java
index a4080feab7..1bc6ed73c5 100644
--- a/lucene/analysis/common/src/test/org/apache/lucene/analysis/minhash/MinHashFilterTest.java
+++ b/lucene/analysis/common/src/test/org/apache/lucene/analysis/minhash/MinHashFilterTest.java
@@ -183,14 +183,14 @@ public class MinHashFilterTest extends BaseTokenStreamTestCase {
TokenStream ts = createTokenStream(5, "woof woof woof woof woof", 1, 1, 100, false);
assertTokenStreamContents(ts, hashes, new int[]{0},
new int[]{24}, new String[]{MinHashFilter.MIN_HASH_TYPE}, new int[]{1}, new int[]{1}, 24, 0, null,
- true);
+ true, null);
ts = createTokenStream(5, "woof woof woof woof woof", 2, 1, 1, false);
assertTokenStreamContents(ts, new String[]{new String(new char[]{0, 0, 8449, 54077, 64133, 32857, 8605, 41409}),
new String(new char[]{0, 1, 16887, 58164, 39536, 14926, 6529, 17276})}, new int[]{0, 0},
new int[]{24, 24}, new String[]{MinHashFilter.MIN_HASH_TYPE, MinHashFilter.MIN_HASH_TYPE}, new int[]{1, 0},
new int[]{1, 1}, 24, 0, null,
- true);
+ true, null);
}
@Test
@@ -203,7 +203,7 @@ public class MinHashFilterTest extends BaseTokenStreamTestCase {
false);
assertTokenStreamContents(ts, hashes, new int[]{0, 0},
new int[]{49, 49}, new String[]{MinHashFilter.MIN_HASH_TYPE, MinHashFilter.MIN_HASH_TYPE}, new int[]{1, 0},
- new int[]{1, 1}, 49, 0, null, true);
+ new int[]{1, 1}, 49, 0, null, true, null);
}
private ArrayList getTokens(TokenStream ts) throws IOException {
diff --git a/lucene/analysis/common/src/test/org/apache/lucene/analysis/miscellaneous/TestTypeAsSynonymFilterFactory.java b/lucene/analysis/common/src/test/org/apache/lucene/analysis/miscellaneous/TestTypeAsSynonymFilterFactory.java
new file mode 100644
index 0000000000..6beb139a58
--- /dev/null
+++ b/lucene/analysis/common/src/test/org/apache/lucene/analysis/miscellaneous/TestTypeAsSynonymFilterFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.apache.lucene.analysis.miscellaneous;
+
+import org.apache.lucene.analysis.CannedTokenStream;
+import org.apache.lucene.analysis.Token;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.util.BaseTokenStreamFactoryTestCase;
+
+public class TestTypeAsSynonymFilterFactory extends BaseTokenStreamFactoryTestCase {
+
+ private static final Token[] TOKENS = { token("Visit", ""), token("example.com", "") };
+
+ public void testBasic() throws Exception {
+ TokenStream stream = new CannedTokenStream(TOKENS);
+ stream = tokenFilterFactory("TypeAsSynonym").create(stream);
+ assertTokenStreamContents(stream, new String[] { "Visit", "", "example.com", "" },
+ null, null, new String[] { "", "", "", "" }, new int[] { 1, 0, 1, 0 });
+ }
+
+ public void testPrefix() throws Exception {
+ TokenStream stream = new CannedTokenStream(TOKENS);
+ stream = tokenFilterFactory("TypeAsSynonym", "prefix", "_type_").create(stream);
+ assertTokenStreamContents(stream, new String[] { "Visit", "_type_", "example.com", "_type_" },
+ null, null, new String[] { "", "", "", "" }, new int[] { 1, 0, 1, 0 });
+ }
+
+ private static Token token(String term, String type) {
+ Token token = new Token();
+ token.setEmpty();
+ token.append(term);
+ token.setType(type);
+ return token;
+ }
+}
diff --git a/lucene/analysis/opennlp/build.xml b/lucene/analysis/opennlp/build.xml
new file mode 100644
index 0000000000..e2cd20a5af
--- /dev/null
+++ b/lucene/analysis/opennlp/build.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+ OpenNLP Library Integration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lucene/analysis/opennlp/ivy.xml b/lucene/analysis/opennlp/ivy.xml
new file mode 100644
index 0000000000..c7b885f411
--- /dev/null
+++ b/lucene/analysis/opennlp/ivy.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPChunkerFilter.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPChunkerFilter.java
new file mode 100644
index 0000000000..cfc47e6c40
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPChunkerFilter.java
@@ -0,0 +1,108 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.opennlp.tools.NLPChunkerOp;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.apache.lucene.util.AttributeSource;
+
+/**
+ * Run OpenNLP chunker. Prerequisite: the OpenNLPTokenizer and OpenNLPPOSFilter must precede this filter.
+ * Tags terms in the TypeAttribute, replacing the POS tags previously put there by OpenNLPPOSFilter.
+ */
+public final class OpenNLPChunkerFilter extends TokenFilter {
+
+ private List sentenceTokenAttrs = new ArrayList<>();
+ private int tokenNum = 0;
+ private boolean moreTokensAvailable = true;
+ private String[] sentenceTerms = null;
+ private String[] sentenceTermPOSTags = null;
+
+ private final NLPChunkerOp chunkerOp;
+ private final TypeAttribute typeAtt = addAttribute(TypeAttribute.class);
+ private final FlagsAttribute flagsAtt = addAttribute(FlagsAttribute.class);
+ private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+
+ public OpenNLPChunkerFilter(TokenStream input, NLPChunkerOp chunkerOp) {
+ super(input);
+ this.chunkerOp = chunkerOp;
+ }
+
+ @Override
+ public final boolean incrementToken() throws IOException {
+ if ( ! moreTokensAvailable) {
+ clear();
+ return false;
+ }
+ if (tokenNum == sentenceTokenAttrs.size()) {
+ nextSentence();
+ if (sentenceTerms == null) {
+ clear();
+ return false;
+ }
+ assignTokenTypes(chunkerOp.getChunks(sentenceTerms, sentenceTermPOSTags, null));
+ tokenNum = 0;
+ }
+ clearAttributes();
+ sentenceTokenAttrs.get(tokenNum++).copyTo(this);
+ return true;
+ }
+
+ private void nextSentence() throws IOException {
+ List termList = new ArrayList<>();
+ List posTagList = new ArrayList<>();
+ sentenceTokenAttrs.clear();
+ boolean endOfSentence = false;
+ while ( ! endOfSentence && (moreTokensAvailable = input.incrementToken())) {
+ termList.add(termAtt.toString());
+ posTagList.add(typeAtt.type());
+ endOfSentence = 0 != (flagsAtt.getFlags() & OpenNLPTokenizer.EOS_FLAG_BIT);
+ sentenceTokenAttrs.add(input.cloneAttributes());
+ }
+ sentenceTerms = termList.size() > 0 ? termList.toArray(new String[termList.size()]) : null;
+ sentenceTermPOSTags = posTagList.size() > 0 ? posTagList.toArray(new String[posTagList.size()]) : null;
+ }
+
+ private void assignTokenTypes(String[] tags) {
+ for (int i = 0 ; i < tags.length ; ++i) {
+ sentenceTokenAttrs.get(i).getAttribute(TypeAttribute.class).setType(tags[i]);
+ }
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ moreTokensAvailable = true;
+ clear();
+ }
+
+ private void clear() {
+ sentenceTokenAttrs.clear();
+ sentenceTerms = null;
+ sentenceTermPOSTags = null;
+ tokenNum = 0;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPChunkerFilterFactory.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPChunkerFilterFactory.java
new file mode 100644
index 0000000000..96eb672452
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPChunkerFilterFactory.java
@@ -0,0 +1,81 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.opennlp.tools.NLPChunkerOp;
+import org.apache.lucene.analysis.opennlp.tools.OpenNLPOpsFactory;
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.lucene.analysis.util.ResourceLoaderAware;
+import org.apache.lucene.analysis.util.TokenFilterFactory;
+
+/**
+ * Factory for {@link OpenNLPChunkerFilter}.
+ *
+ *
+ * <fieldType name="text_opennlp_chunked" class="solr.TextField" positionIncrementGap="100">
+ * <analyzer>
+ * <tokenizer class="solr.OpenNLPTokenizerFactory" sentenceModel="filename" tokenizerModel="filename"/>
+ * <filter class="solr.OpenNLPPOSFilterFactory" posTaggerModel="filename"/>
+ * <filter class="solr.OpenNLPChunkerFilterFactory" chunkerModel="filename"/>
+ * </analyzer>
+ * </fieldType>
+ * @since 7.3.0
+ */
+public class OpenNLPChunkerFilterFactory extends TokenFilterFactory implements ResourceLoaderAware {
+ public static final String CHUNKER_MODEL = "chunkerModel";
+
+ private final String chunkerModelFile;
+
+ public OpenNLPChunkerFilterFactory(Map args) {
+ super(args);
+ chunkerModelFile = get(args, CHUNKER_MODEL);
+ if (!args.isEmpty()) {
+ throw new IllegalArgumentException("Unknown parameters: " + args);
+ }
+ }
+
+ @Override
+ public OpenNLPChunkerFilter create(TokenStream in) {
+ try {
+ NLPChunkerOp chunkerOp = null;
+
+ if (chunkerModelFile != null) {
+ chunkerOp = OpenNLPOpsFactory.getChunker(chunkerModelFile);
+ }
+ return new OpenNLPChunkerFilter(in, chunkerOp);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public void inform(ResourceLoader loader) {
+ try {
+ // load and register read-only models in cache with file/resource names
+ if (chunkerModelFile != null) {
+ OpenNLPOpsFactory.getChunkerModel(chunkerModelFile, loader);
+ }
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPLemmatizerFilter.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPLemmatizerFilter.java
new file mode 100644
index 0000000000..4c484b9435
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPLemmatizerFilter.java
@@ -0,0 +1,123 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.opennlp.tools.NLPLemmatizerOp;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
+import org.apache.lucene.analysis.tokenattributes.KeywordAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.apache.lucene.util.AttributeSource;
+
+/**
+ * Runs OpenNLP dictionary-based and/or MaxEnt lemmatizers.
+ *
+ * Both a dictionary-based lemmatizer and a MaxEnt lemmatizer are supported,
+ * via the "dictionary" and "lemmatizerModel" params, respectively.
+ * If both are configured, the dictionary-based lemmatizer is tried first,
+ * and then the MaxEnt lemmatizer is consulted for out-of-vocabulary tokens.
+ *
+ *
+ * The dictionary file must be encoded as UTF-8, with one entry per line,
+ * in the form word[tab]lemma[tab]part-of-speech
+ *
+ */
+public class OpenNLPLemmatizerFilter extends TokenFilter {
+ private final NLPLemmatizerOp lemmatizerOp;
+ private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+ private final TypeAttribute typeAtt = addAttribute(TypeAttribute.class);
+ private final KeywordAttribute keywordAtt = addAttribute(KeywordAttribute.class);
+ private final FlagsAttribute flagsAtt = addAttribute(FlagsAttribute.class);
+ private List sentenceTokenAttrs = new ArrayList<>();
+ private Iterator sentenceTokenAttrsIter = null;
+ private boolean moreTokensAvailable = true;
+ private String[] sentenceTokens = null; // non-keyword tokens
+ private String[] sentenceTokenTypes = null; // types for non-keyword tokens
+ private String[] lemmas = null; // lemmas for non-keyword tokens
+ private int lemmaNum = 0; // lemma counter
+
+ public OpenNLPLemmatizerFilter(TokenStream input, NLPLemmatizerOp lemmatizerOp) {
+ super(input);
+ this.lemmatizerOp = lemmatizerOp;
+ }
+
+ @Override
+ public final boolean incrementToken() throws IOException {
+ if ( ! moreTokensAvailable) {
+ clear();
+ return false;
+ }
+ if (sentenceTokenAttrsIter == null || ! sentenceTokenAttrsIter.hasNext()) {
+ nextSentence();
+ if (sentenceTokens == null) { // zero non-keyword tokens
+ clear();
+ return false;
+ }
+ lemmas = lemmatizerOp.lemmatize(sentenceTokens, sentenceTokenTypes);
+ lemmaNum = 0;
+ sentenceTokenAttrsIter = sentenceTokenAttrs.iterator();
+ }
+ clearAttributes();
+ sentenceTokenAttrsIter.next().copyTo(this);
+ if ( ! keywordAtt.isKeyword()) {
+ termAtt.setEmpty().append(lemmas[lemmaNum++]);
+ }
+ return true;
+
+ }
+
+ private void nextSentence() throws IOException {
+ List tokenList = new ArrayList<>();
+ List typeList = new ArrayList<>();
+ sentenceTokenAttrs.clear();
+ boolean endOfSentence = false;
+ while ( ! endOfSentence && (moreTokensAvailable = input.incrementToken())) {
+ if ( ! keywordAtt.isKeyword()) {
+ tokenList.add(termAtt.toString());
+ typeList.add(typeAtt.type());
+ }
+ endOfSentence = 0 != (flagsAtt.getFlags() & OpenNLPTokenizer.EOS_FLAG_BIT);
+ sentenceTokenAttrs.add(input.cloneAttributes());
+ }
+ sentenceTokens = tokenList.size() > 0 ? tokenList.toArray(new String[tokenList.size()]) : null;
+ sentenceTokenTypes = typeList.size() > 0 ? typeList.toArray(new String[typeList.size()]) : null;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ moreTokensAvailable = true;
+ clear();
+ }
+
+ private void clear() {
+ sentenceTokenAttrs.clear();
+ sentenceTokenAttrsIter = null;
+ sentenceTokens = null;
+ sentenceTokenTypes = null;
+ lemmas = null;
+ lemmaNum = 0;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPLemmatizerFilterFactory.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPLemmatizerFilterFactory.java
new file mode 100644
index 0000000000..90a0e43cad
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPLemmatizerFilterFactory.java
@@ -0,0 +1,89 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.opennlp.tools.NLPLemmatizerOp;
+import org.apache.lucene.analysis.opennlp.tools.OpenNLPOpsFactory;
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.lucene.analysis.util.ResourceLoaderAware;
+import org.apache.lucene.analysis.util.TokenFilterFactory;
+
+/**
+ * Factory for {@link OpenNLPLemmatizerFilter}.
+ *
+ *
+ * <fieldType name="text_opennlp_lemma" class="solr.TextField" positionIncrementGap="100"
+ * <analyzer>
+ * <tokenizer class="solr.OpenNLPTokenizerFactory"
+ * sentenceModel="filename"
+ * tokenizerModel="filename"/>
+ * />
+ * <filter class="solr.OpenNLPLemmatizerFilterFactory"
+ * dictionary="filename"
+ * lemmatizerModel="filename"/>
+ * </analyzer>
+ * </fieldType>
+ * @since 7.3.0
+ */
+public class OpenNLPLemmatizerFilterFactory extends TokenFilterFactory implements ResourceLoaderAware {
+ public static final String DICTIONARY = "dictionary";
+ public static final String LEMMATIZER_MODEL = "lemmatizerModel";
+
+ private final String dictionaryFile;
+ private final String lemmatizerModelFile;
+
+ public OpenNLPLemmatizerFilterFactory(Map args) {
+ super(args);
+ dictionaryFile = get(args, DICTIONARY);
+ lemmatizerModelFile = get(args, LEMMATIZER_MODEL);
+
+ if (dictionaryFile == null && lemmatizerModelFile == null) {
+ throw new IllegalArgumentException("Configuration Error: missing parameter: at least one of '"
+ + DICTIONARY + "' and '" + LEMMATIZER_MODEL + "' must be provided.");
+ }
+
+ if (!args.isEmpty()) {
+ throw new IllegalArgumentException("Unknown parameters: " + args);
+ }
+ }
+
+ @Override
+ public OpenNLPLemmatizerFilter create(TokenStream in) {
+ try {
+ NLPLemmatizerOp lemmatizerOp = OpenNLPOpsFactory.getLemmatizer(dictionaryFile, lemmatizerModelFile);
+ return new OpenNLPLemmatizerFilter(in, lemmatizerOp);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void inform(ResourceLoader loader) throws IOException {
+ // register models in cache with file/resource names
+ if (dictionaryFile != null) {
+ OpenNLPOpsFactory.getLemmatizerDictionary(dictionaryFile, loader);
+ }
+ if (lemmatizerModelFile != null) {
+ OpenNLPOpsFactory.getLemmatizerModel(lemmatizerModelFile, loader);
+ }
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPPOSFilter.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPPOSFilter.java
new file mode 100644
index 0000000000..a5bea287c4
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPPOSFilter.java
@@ -0,0 +1,96 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.opennlp.tools.NLPPOSTaggerOp;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.apache.lucene.util.AttributeSource;
+
+/**
+ * Run OpenNLP POS tagger. Tags all terms in the TypeAttribute.
+ */
+public final class OpenNLPPOSFilter extends TokenFilter {
+
+ private List sentenceTokenAttrs = new ArrayList<>();
+ String[] tags = null;
+ private int tokenNum = 0;
+ private boolean moreTokensAvailable = true;
+
+ private final NLPPOSTaggerOp posTaggerOp;
+ private final TypeAttribute typeAtt = addAttribute(TypeAttribute.class);
+ private final FlagsAttribute flagsAtt = addAttribute(FlagsAttribute.class);
+ private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+
+ public OpenNLPPOSFilter(TokenStream input, NLPPOSTaggerOp posTaggerOp) {
+ super(input);
+ this.posTaggerOp = posTaggerOp;
+ }
+
+ @Override
+ public final boolean incrementToken() throws IOException {
+ if ( ! moreTokensAvailable) {
+ clear();
+ return false;
+ }
+ if (tokenNum == sentenceTokenAttrs.size()) { // beginning of stream, or previous sentence exhausted
+ String[] sentenceTokens = nextSentence();
+ if (sentenceTokens == null) {
+ clear();
+ return false;
+ }
+ tags = posTaggerOp.getPOSTags(sentenceTokens);
+ tokenNum = 0;
+ }
+ clearAttributes();
+ sentenceTokenAttrs.get(tokenNum).copyTo(this);
+ typeAtt.setType(tags[tokenNum++]);
+ return true;
+ }
+
+ private String[] nextSentence() throws IOException {
+ List termList = new ArrayList<>();
+ sentenceTokenAttrs.clear();
+ boolean endOfSentence = false;
+ while ( ! endOfSentence && (moreTokensAvailable = input.incrementToken())) {
+ termList.add(termAtt.toString());
+ endOfSentence = 0 != (flagsAtt.getFlags() & OpenNLPTokenizer.EOS_FLAG_BIT);
+ sentenceTokenAttrs.add(input.cloneAttributes());
+ }
+ return termList.size() > 0 ? termList.toArray(new String[termList.size()]) : null;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ moreTokensAvailable = true;
+ }
+
+ private void clear() {
+ sentenceTokenAttrs.clear();
+ tags = null;
+ tokenNum = 0;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPPOSFilterFactory.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPPOSFilterFactory.java
new file mode 100644
index 0000000000..952218f533
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPPOSFilterFactory.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.opennlp.tools.OpenNLPOpsFactory;
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.lucene.analysis.util.ResourceLoaderAware;
+import org.apache.lucene.analysis.util.TokenFilterFactory;
+
+/**
+ * Factory for {@link OpenNLPPOSFilter}.
+ *
+ *
+ * <fieldType name="text_opennlp_pos" class="solr.TextField" positionIncrementGap="100">
+ * <analyzer>
+ * <tokenizer class="solr.OpenNLPTokenizerFactory" sentenceModel="filename" tokenizerModel="filename"/>
+ * <filter class="solr.OpenNLPPOSFilterFactory" posTaggerModel="filename"/>
+ * </analyzer>
+ * </fieldType>
+ * @since 7.3.0
+ */
+public class OpenNLPPOSFilterFactory extends TokenFilterFactory implements ResourceLoaderAware {
+ public static final String POS_TAGGER_MODEL = "posTaggerModel";
+
+ private final String posTaggerModelFile;
+
+ public OpenNLPPOSFilterFactory(Map args) {
+ super(args);
+ posTaggerModelFile = require(args, POS_TAGGER_MODEL);
+ if (!args.isEmpty()) {
+ throw new IllegalArgumentException("Unknown parameters: " + args);
+ }
+ }
+
+ @Override
+ public OpenNLPPOSFilter create(TokenStream in) {
+ try {
+ return new OpenNLPPOSFilter(in, OpenNLPOpsFactory.getPOSTagger(posTaggerModelFile));
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public void inform(ResourceLoader loader) {
+ try { // load and register the read-only model in cache with file/resource name
+ OpenNLPOpsFactory.getPOSTaggerModel(posTaggerModelFile, loader);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java
new file mode 100644
index 0000000000..f69fbc6b92
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java
@@ -0,0 +1,224 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.text.BreakIterator;
+import java.text.CharacterIterator;
+
+import opennlp.tools.util.Span;
+import org.apache.lucene.analysis.opennlp.tools.NLPSentenceDetectorOp;
+import org.apache.lucene.analysis.util.CharArrayIterator;
+
+/**
+ * A {@link BreakIterator} that splits sentences using an OpenNLP sentence chunking model.
+ */
+public final class OpenNLPSentenceBreakIterator extends BreakIterator {
+
+ private CharacterIterator text;
+ private int currentSentence;
+ private int[] sentenceStarts;
+ private NLPSentenceDetectorOp sentenceOp;
+
+ public OpenNLPSentenceBreakIterator(NLPSentenceDetectorOp sentenceOp) {
+ this.sentenceOp = sentenceOp;
+ }
+
+ @Override
+ public int current() {
+ return text.getIndex();
+ }
+
+ @Override
+ public int first() {
+ currentSentence = 0;
+ text.setIndex(text.getBeginIndex());
+ return current();
+ }
+
+ @Override
+ public int last() {
+ if (sentenceStarts.length > 0) {
+ currentSentence = sentenceStarts.length - 1;
+ text.setIndex(text.getEndIndex());
+ } else { // there are no sentences; both the first and last positions are the begin index
+ currentSentence = 0;
+ text.setIndex(text.getBeginIndex());
+ }
+ return current();
+ }
+
+ @Override
+ public int next() {
+ if (text.getIndex() == text.getEndIndex() || 0 == sentenceStarts.length) {
+ return DONE;
+ } else if (currentSentence < sentenceStarts.length - 1) {
+ text.setIndex(sentenceStarts[++currentSentence]);
+ return current();
+ } else {
+ return last();
+ }
+ }
+
+ @Override
+ public int following(int pos) {
+ if (pos < text.getBeginIndex() || pos > text.getEndIndex()) {
+ throw new IllegalArgumentException("offset out of bounds");
+ } else if (0 == sentenceStarts.length) {
+ text.setIndex(text.getBeginIndex());
+ return DONE;
+ } else if (pos >= sentenceStarts[sentenceStarts.length - 1]) {
+ // this conflicts with the javadocs, but matches actual behavior (Oracle has a bug in something)
+ // https://bugs.openjdk.java.net/browse/JDK-8015110
+ text.setIndex(text.getEndIndex());
+ currentSentence = sentenceStarts.length - 1;
+ return DONE;
+ } else { // there are at least two sentences
+ currentSentence = (sentenceStarts.length - 1) / 2; // start search from the middle
+ moveToSentenceAt(pos, 0, sentenceStarts.length - 2);
+ text.setIndex(sentenceStarts[++currentSentence]);
+ return current();
+ }
+ }
+
+ /** Binary search over sentences */
+ private void moveToSentenceAt(int pos, int minSentence, int maxSentence) {
+ if (minSentence != maxSentence) {
+ if (pos < sentenceStarts[currentSentence]) {
+ int newMaxSentence = currentSentence - 1;
+ currentSentence = minSentence + (currentSentence - minSentence) / 2;
+ moveToSentenceAt(pos, minSentence, newMaxSentence);
+ } else if (pos >= sentenceStarts[currentSentence + 1]) {
+ int newMinSentence = currentSentence + 1;
+ currentSentence = maxSentence - (maxSentence - currentSentence) / 2;
+ moveToSentenceAt(pos, newMinSentence, maxSentence);
+ }
+ } else {
+ assert currentSentence == minSentence;
+ assert pos >= sentenceStarts[currentSentence];
+ assert (currentSentence == sentenceStarts.length - 1 && pos <= text.getEndIndex())
+ || pos < sentenceStarts[currentSentence + 1];
+ }
+ // we have arrived - nothing to do
+ }
+
+ @Override
+ public int previous() {
+ if (text.getIndex() == text.getBeginIndex()) {
+ return DONE;
+ } else {
+ if (0 == sentenceStarts.length) {
+ text.setIndex(text.getBeginIndex());
+ return DONE;
+ }
+ if (text.getIndex() == text.getEndIndex()) {
+ text.setIndex(sentenceStarts[currentSentence]);
+ } else {
+ text.setIndex(sentenceStarts[--currentSentence]);
+ }
+ return current();
+ }
+ }
+
+ @Override
+ public int preceding(int pos) {
+ if (pos < text.getBeginIndex() || pos > text.getEndIndex()) {
+ throw new IllegalArgumentException("offset out of bounds");
+ } else if (0 == sentenceStarts.length) {
+ text.setIndex(text.getBeginIndex());
+ currentSentence = 0;
+ return DONE;
+ } else if (pos < sentenceStarts[0]) {
+ // this conflicts with the javadocs, but matches actual behavior (Oracle has a bug in something)
+ // https://bugs.openjdk.java.net/browse/JDK-8015110
+ text.setIndex(text.getBeginIndex());
+ currentSentence = 0;
+ return DONE;
+ } else {
+ currentSentence = sentenceStarts.length / 2; // start search from the middle
+ moveToSentenceAt(pos, 0, sentenceStarts.length - 1);
+ if (0 == currentSentence) {
+ text.setIndex(text.getBeginIndex());
+ return DONE;
+ } else {
+ text.setIndex(sentenceStarts[--currentSentence]);
+ return current();
+ }
+ }
+ }
+
+ @Override
+ public int next(int n) {
+ currentSentence += n;
+ if (n < 0) {
+ if (text.getIndex() == text.getEndIndex()) {
+ ++currentSentence;
+ }
+ if (currentSentence < 0) {
+ currentSentence = 0;
+ text.setIndex(text.getBeginIndex());
+ return DONE;
+ } else {
+ text.setIndex(sentenceStarts[currentSentence]);
+ }
+ } else if (n > 0) {
+ if (currentSentence >= sentenceStarts.length) {
+ currentSentence = sentenceStarts.length - 1;
+ text.setIndex(text.getEndIndex());
+ return DONE;
+ } else {
+ text.setIndex(sentenceStarts[currentSentence]);
+ }
+ }
+ return current();
+ }
+
+ @Override
+ public CharacterIterator getText() {
+ return text;
+ }
+
+ @Override
+ public void setText(CharacterIterator newText) {
+ text = newText;
+ text.setIndex(text.getBeginIndex());
+ currentSentence = 0;
+ Span[] spans = sentenceOp.splitSentences(characterIteratorToString());
+ sentenceStarts = new int[spans.length];
+ for (int i = 0; i < spans.length; ++i) {
+ // Adjust start positions to match those of the passed-in CharacterIterator
+ sentenceStarts[i] = spans[i].getStart() + text.getBeginIndex();
+ }
+ }
+
+ private String characterIteratorToString() {
+ String fullText;
+ if (text instanceof CharArrayIterator) {
+ CharArrayIterator charArrayIterator = (CharArrayIterator)text;
+ fullText = new String(charArrayIterator.getText(), charArrayIterator.getStart(), charArrayIterator.getLength());
+ } else {
+ // TODO: is there a better way to extract full text from arbitrary CharacterIterators?
+ StringBuilder builder = new StringBuilder();
+ for (char ch = text.first(); ch != CharacterIterator.DONE; ch = text.next()) {
+ builder.append(ch);
+ }
+ fullText = builder.toString();
+ text.setIndex(text.getBeginIndex());
+ }
+ return fullText;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizer.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizer.java
new file mode 100644
index 0000000000..75a3b81074
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizer.java
@@ -0,0 +1,98 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+
+import opennlp.tools.util.Span;
+
+import org.apache.lucene.analysis.opennlp.tools.NLPSentenceDetectorOp;
+import org.apache.lucene.analysis.opennlp.tools.NLPTokenizerOp;
+import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
+import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.util.SegmentingTokenizerBase;
+import org.apache.lucene.util.AttributeFactory;
+
+/**
+ * Run OpenNLP SentenceDetector and Tokenizer.
+ * The last token in each sentence is marked by setting the {@link #EOS_FLAG_BIT} in the FlagsAttribute;
+ * following filters can use this information to apply operations to tokens one sentence at a time.
+ */
+public final class OpenNLPTokenizer extends SegmentingTokenizerBase {
+ public static int EOS_FLAG_BIT = 1;
+
+ private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
+ private final FlagsAttribute flagsAtt = addAttribute(FlagsAttribute.class);
+ private final OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class);
+
+ private Span[] termSpans = null;
+ private int termNum = 0;
+ private int sentenceStart = 0;
+
+ private NLPSentenceDetectorOp sentenceOp = null;
+ private NLPTokenizerOp tokenizerOp = null;
+
+ public OpenNLPTokenizer(AttributeFactory factory, NLPSentenceDetectorOp sentenceOp, NLPTokenizerOp tokenizerOp) throws IOException {
+ super(factory, new OpenNLPSentenceBreakIterator(sentenceOp));
+ if (sentenceOp == null || tokenizerOp == null) {
+ throw new IllegalArgumentException("OpenNLPTokenizer: both a Sentence Detector and a Tokenizer are required");
+ }
+ this.sentenceOp = sentenceOp;
+ this.tokenizerOp = tokenizerOp;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ termSpans = null;
+ termNum = sentenceStart = 0;
+ };
+
+ @Override
+ protected void setNextSentence(int sentenceStart, int sentenceEnd) {
+ this.sentenceStart = sentenceStart;
+ String sentenceText = new String(buffer, sentenceStart, sentenceEnd - sentenceStart);
+ termSpans = tokenizerOp.getTerms(sentenceText);
+ termNum = 0;
+ }
+
+ @Override
+ protected boolean incrementWord() {
+ if (termSpans == null || termNum == termSpans.length) {
+ return false;
+ }
+ clearAttributes();
+ Span term = termSpans[termNum];
+ termAtt.copyBuffer(buffer, sentenceStart + term.getStart(), term.length());
+ offsetAtt.setOffset(correctOffset(offset + sentenceStart + term.getStart()),
+ correctOffset(offset + sentenceStart + term.getEnd()));
+ if (termNum == termSpans.length - 1) {
+ flagsAtt.setFlags(flagsAtt.getFlags() | EOS_FLAG_BIT); // mark the last token in the sentence with EOS_FLAG_BIT
+ }
+ ++termNum;
+ return true;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ termSpans = null;
+ termNum = sentenceStart = 0;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizerFactory.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizerFactory.java
new file mode 100644
index 0000000000..a60f23f6bd
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizerFactory.java
@@ -0,0 +1,79 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.lucene.analysis.opennlp.tools.NLPSentenceDetectorOp;
+import org.apache.lucene.analysis.opennlp.tools.NLPTokenizerOp;
+import org.apache.lucene.analysis.opennlp.tools.OpenNLPOpsFactory;
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.lucene.analysis.util.ResourceLoaderAware;
+import org.apache.lucene.analysis.util.TokenizerFactory;
+import org.apache.lucene.util.AttributeFactory;
+
+/**
+ * Factory for {@link OpenNLPTokenizer}.
+ *
+ *
+ * <fieldType name="text_opennlp" class="solr.TextField" positionIncrementGap="100"
+ * <analyzer>
+ * <tokenizer class="solr.OpenNLPTokenizerFactory" sentenceModel="filename" tokenizerModel="filename"/>
+ * </analyzer>
+ * </fieldType>
+ * @since 7.3.0
+ */
+public class OpenNLPTokenizerFactory extends TokenizerFactory implements ResourceLoaderAware {
+ public static final String SENTENCE_MODEL = "sentenceModel";
+ public static final String TOKENIZER_MODEL = "tokenizerModel";
+
+ private final String sentenceModelFile;
+ private final String tokenizerModelFile;
+
+ public OpenNLPTokenizerFactory(Map args) {
+ super(args);
+ sentenceModelFile = require(args, SENTENCE_MODEL);
+ tokenizerModelFile = require(args, TOKENIZER_MODEL);
+ if ( ! args.isEmpty()) {
+ throw new IllegalArgumentException("Unknown parameters: " + args);
+ }
+ }
+
+ @Override
+ public OpenNLPTokenizer create(AttributeFactory factory) {
+ try {
+ NLPSentenceDetectorOp sentenceOp = OpenNLPOpsFactory.getSentenceDetector(sentenceModelFile);
+ NLPTokenizerOp tokenizerOp = OpenNLPOpsFactory.getTokenizer(tokenizerModelFile);
+ return new OpenNLPTokenizer(factory, sentenceOp, tokenizerOp);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void inform(ResourceLoader loader) throws IOException {
+ // register models in cache with file/resource names
+ if (sentenceModelFile != null) {
+ OpenNLPOpsFactory.getSentenceModel(sentenceModelFile, loader);
+ }
+ if (tokenizerModelFile != null) {
+ OpenNLPOpsFactory.getTokenizerModel(tokenizerModelFile, loader);
+ }
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/package-info.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/package-info.java
new file mode 100644
index 0000000000..527e24fe00
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Analysis components based on OpenNLP
+ */
+package org.apache.lucene.analysis.opennlp;
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPChunkerOp.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPChunkerOp.java
new file mode 100644
index 0000000000..f6a5ea8792
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPChunkerOp.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import java.io.IOException;
+import opennlp.tools.chunker.ChunkerME;
+import opennlp.tools.chunker.ChunkerModel;
+
+/**
+ * Supply OpenNLP Chunking tool
+ * Requires binary models from OpenNLP project on SourceForge.
+ */
+public class NLPChunkerOp {
+ private ChunkerME chunker = null;
+
+ public NLPChunkerOp(ChunkerModel chunkerModel) throws IOException {
+ chunker = new ChunkerME(chunkerModel);
+ }
+
+ public synchronized String[] getChunks(String[] words, String[] tags, double[] probs) {
+ String[] chunks = chunker.chunk(words, tags);
+ if (probs != null)
+ chunker.probs(probs);
+ return chunks;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPLemmatizerOp.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPLemmatizerOp.java
new file mode 100644
index 0000000000..b09c63ec3a
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPLemmatizerOp.java
@@ -0,0 +1,80 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import opennlp.tools.lemmatizer.DictionaryLemmatizer;
+import opennlp.tools.lemmatizer.LemmatizerME;
+import opennlp.tools.lemmatizer.LemmatizerModel;
+
+/**
+ * Supply OpenNLP Lemmatizer tools.
+ *
+ * Both a dictionary-based lemmatizer and a MaxEnt lemmatizer are supported.
+ * If both are configured, the dictionary-based lemmatizer is tried first,
+ * and then the MaxEnt lemmatizer is consulted for out-of-vocabulary tokens.
+ *
+ *
+ * The MaxEnt implementation requires binary models from OpenNLP project on SourceForge.
+ *
+ */
+public class NLPLemmatizerOp {
+ private final DictionaryLemmatizer dictionaryLemmatizer;
+ private final LemmatizerME lemmatizerME;
+
+ public NLPLemmatizerOp(InputStream dictionary, LemmatizerModel lemmatizerModel) throws IOException {
+ assert dictionary != null || lemmatizerModel != null : "At least one parameter must be non-null";
+ dictionaryLemmatizer = dictionary == null ? null : new DictionaryLemmatizer(dictionary);
+ lemmatizerME = lemmatizerModel == null ? null : new LemmatizerME(lemmatizerModel);
+ }
+
+ public String[] lemmatize(String[] words, String[] postags) {
+ String[] lemmas = null;
+ String[] maxEntLemmas = null;
+ if (dictionaryLemmatizer != null) {
+ lemmas = dictionaryLemmatizer.lemmatize(words, postags);
+ for (int i = 0; i < lemmas.length; ++i) {
+ if (lemmas[i].equals("O")) { // this word is not in the dictionary
+ if (lemmatizerME != null) { // fall back to the MaxEnt lemmatizer if it's enabled
+ if (maxEntLemmas == null) {
+ maxEntLemmas = lemmatizerME.lemmatize(words, postags);
+ }
+ if ("_".equals(maxEntLemmas[i])) {
+ lemmas[i] = words[i]; // put back the original word if no lemma is found
+ } else {
+ lemmas[i] = maxEntLemmas[i];
+ }
+ } else { // there is no MaxEnt lemmatizer
+ lemmas[i] = words[i]; // put back the original word if no lemma is found
+ }
+ }
+ }
+ } else { // there is only a MaxEnt lemmatizer
+ maxEntLemmas = lemmatizerME.lemmatize(words, postags);
+ for (int i = 0 ; i < maxEntLemmas.length ; ++i) {
+ if ("_".equals(maxEntLemmas[i])) {
+ maxEntLemmas[i] = words[i]; // put back the original word if no lemma is found
+ }
+ }
+ lemmas = maxEntLemmas;
+ }
+ return lemmas;
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPNERTaggerOp.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPNERTaggerOp.java
new file mode 100644
index 0000000000..22e617d171
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPNERTaggerOp.java
@@ -0,0 +1,56 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import opennlp.tools.namefind.NameFinderME;
+import opennlp.tools.namefind.TokenNameFinder;
+import opennlp.tools.namefind.TokenNameFinderModel;
+import opennlp.tools.util.Span;
+
+/**
+ * Supply OpenNLP Named Entity Resolution tool
+ * Requires binary models from OpenNLP project on SourceForge.
+ *
+ * Usage: from the OpenNLP documentation:
+ *
+ * "The NameFinderME class is not thread safe, it must only be called from one thread.
+ * To use multiple threads multiple NameFinderME instances sharing the same model instance
+ * can be created. The input text should be segmented into documents, sentences and tokens.
+ * To perform entity detection an application calls the find method for every sentence in
+ * the document. After every document clearAdaptiveData must be called to clear the adaptive
+ * data in the feature generators. Not calling clearAdaptiveData can lead to a sharp drop
+ * in the detection rate after a few documents."
+ *
+ */
+public class NLPNERTaggerOp {
+ private final TokenNameFinder nameFinder;
+
+ public NLPNERTaggerOp(TokenNameFinderModel model) {
+ this.nameFinder = new NameFinderME(model);
+ }
+
+ public Span[] getNames(String[] words) {
+ Span[] names = nameFinder.find(words);
+ return names;
+ }
+
+ public synchronized void reset() {
+ nameFinder.clearAdaptiveData();
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPPOSTaggerOp.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPPOSTaggerOp.java
new file mode 100644
index 0000000000..447e1c0a5c
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPPOSTaggerOp.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import java.io.IOException;
+
+import opennlp.tools.postag.POSModel;
+import opennlp.tools.postag.POSTagger;
+import opennlp.tools.postag.POSTaggerME;
+
+/**
+ * Supply OpenNLP Parts-Of-Speech Tagging tool
+ * Requires binary models from OpenNLP project on SourceForge.
+ */
+
+public class NLPPOSTaggerOp {
+ private POSTagger tagger = null;
+
+ public NLPPOSTaggerOp(POSModel model) throws IOException {
+ tagger = new POSTaggerME(model);
+ }
+
+ public synchronized String[] getPOSTags(String[] words) {
+ return tagger.tag(words);
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPSentenceDetectorOp.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPSentenceDetectorOp.java
new file mode 100644
index 0000000000..21983ce4b5
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPSentenceDetectorOp.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import java.io.IOException;
+
+import opennlp.tools.sentdetect.SentenceDetectorME;
+import opennlp.tools.sentdetect.SentenceModel;
+import opennlp.tools.util.Span;
+
+/**
+ * Supply OpenNLP Sentence Detector tool
+ * Requires binary models from OpenNLP project on SourceForge.
+ */
+public class NLPSentenceDetectorOp {
+ private final SentenceDetectorME sentenceSplitter;
+
+ public NLPSentenceDetectorOp(SentenceModel model) throws IOException {
+ sentenceSplitter = new SentenceDetectorME(model);
+ }
+
+ public NLPSentenceDetectorOp() {
+ sentenceSplitter = null;
+ }
+
+ public synchronized Span[] splitSentences(String line) {
+ if (sentenceSplitter != null) {
+ return sentenceSplitter.sentPosDetect(line);
+ } else {
+ Span[] shorty = new Span[1];
+ shorty[0] = new Span(0, line.length());
+ return shorty;
+ }
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPTokenizerOp.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPTokenizerOp.java
new file mode 100644
index 0000000000..0aeb713314
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPTokenizerOp.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import opennlp.tools.tokenize.Tokenizer;
+import opennlp.tools.tokenize.TokenizerME;
+import opennlp.tools.tokenize.TokenizerModel;
+import opennlp.tools.util.Span;
+
+/**
+ * Supply OpenNLP Sentence Tokenizer tool
+ * Requires binary models from OpenNLP project on SourceForge.
+ */
+public class NLPTokenizerOp {
+ private final Tokenizer tokenizer;
+
+ public NLPTokenizerOp(TokenizerModel model) {
+ tokenizer = new TokenizerME(model);
+ }
+
+ public NLPTokenizerOp() {
+ tokenizer = null;
+ }
+
+ public synchronized Span[] getTerms(String sentence) {
+ if (tokenizer == null) {
+ Span[] span1 = new Span[1];
+ span1[0] = new Span(0, sentence.length());
+ return span1;
+ }
+ return tokenizer.tokenizePos(sentence);
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/OpenNLPOpsFactory.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/OpenNLPOpsFactory.java
new file mode 100644
index 0000000000..5348857ab4
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/OpenNLPOpsFactory.java
@@ -0,0 +1,176 @@
+/*
+ * 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 org.apache.lucene.analysis.opennlp.tools;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import opennlp.tools.chunker.ChunkerModel;
+import opennlp.tools.lemmatizer.LemmatizerModel;
+import opennlp.tools.namefind.TokenNameFinderModel;
+import opennlp.tools.postag.POSModel;
+import opennlp.tools.sentdetect.SentenceModel;
+import opennlp.tools.tokenize.TokenizerModel;
+import org.apache.lucene.analysis.util.ResourceLoader;
+
+/**
+ * Supply OpenNLP Named Entity Recognizer
+ * Cache model file objects. Assumes model files are thread-safe.
+ */
+public class OpenNLPOpsFactory {
+ private static Map sentenceModels = new ConcurrentHashMap<>();
+ private static ConcurrentHashMap tokenizerModels = new ConcurrentHashMap<>();
+ private static ConcurrentHashMap posTaggerModels = new ConcurrentHashMap<>();
+ private static ConcurrentHashMap chunkerModels = new ConcurrentHashMap<>();
+ private static Map nerModels = new ConcurrentHashMap<>();
+ private static Map lemmatizerModels = new ConcurrentHashMap<>();
+ private static Map lemmaDictionaries = new ConcurrentHashMap<>();
+
+ public static NLPSentenceDetectorOp getSentenceDetector(String modelName) throws IOException {
+ if (modelName != null) {
+ SentenceModel model = sentenceModels.get(modelName);
+ return new NLPSentenceDetectorOp(model);
+ } else {
+ return new NLPSentenceDetectorOp();
+ }
+ }
+
+ public static SentenceModel getSentenceModel(String modelName, ResourceLoader loader) throws IOException {
+ SentenceModel model = sentenceModels.get(modelName);
+ if (model == null) {
+ model = new SentenceModel(loader.openResource(modelName));
+ sentenceModels.put(modelName, model);
+ }
+ return model;
+ }
+
+ public static NLPTokenizerOp getTokenizer(String modelName) throws IOException {
+ if (modelName == null) {
+ return new NLPTokenizerOp();
+ } else {
+ TokenizerModel model = tokenizerModels.get(modelName);
+ return new NLPTokenizerOp(model);
+ }
+ }
+
+ public static TokenizerModel getTokenizerModel(String modelName, ResourceLoader loader) throws IOException {
+ TokenizerModel model = tokenizerModels.get(modelName);
+ if (model == null) {
+ model = new TokenizerModel(loader.openResource(modelName));
+ tokenizerModels.put(modelName, model);
+ }
+ return model;
+ }
+
+ public static NLPPOSTaggerOp getPOSTagger(String modelName) throws IOException {
+ POSModel model = posTaggerModels.get(modelName);
+ return new NLPPOSTaggerOp(model);
+ }
+
+ public static POSModel getPOSTaggerModel(String modelName, ResourceLoader loader) throws IOException {
+ POSModel model = posTaggerModels.get(modelName);
+ if (model == null) {
+ model = new POSModel(loader.openResource(modelName));
+ posTaggerModels.put(modelName, model);
+ }
+ return model;
+ }
+
+ public static NLPChunkerOp getChunker(String modelName) throws IOException {
+ ChunkerModel model = chunkerModels.get(modelName);
+ return new NLPChunkerOp(model);
+ }
+
+ public static ChunkerModel getChunkerModel(String modelName, ResourceLoader loader) throws IOException {
+ ChunkerModel model = chunkerModels.get(modelName);
+ if (model == null) {
+ model = new ChunkerModel(loader.openResource(modelName));
+ chunkerModels.put(modelName, model);
+ }
+ return model;
+ }
+
+ public static NLPNERTaggerOp getNERTagger(String modelName) throws IOException {
+ TokenNameFinderModel model = nerModels.get(modelName);
+ return new NLPNERTaggerOp(model);
+ }
+
+ public static TokenNameFinderModel getNERTaggerModel(String modelName, ResourceLoader loader) throws IOException {
+ TokenNameFinderModel model = nerModels.get(modelName);
+ if (model == null) {
+ model = new TokenNameFinderModel(loader.openResource(modelName));
+ nerModels.put(modelName, model);
+ }
+ return model;
+ }
+
+ public static NLPLemmatizerOp getLemmatizer(String dictionaryFile, String lemmatizerModelFile) throws IOException {
+ assert dictionaryFile != null || lemmatizerModelFile != null : "At least one parameter must be non-null";
+ InputStream dictionaryInputStream = null;
+ if (dictionaryFile != null) {
+ String dictionary = lemmaDictionaries.get(dictionaryFile);
+ dictionaryInputStream = new ByteArrayInputStream(dictionary.getBytes(StandardCharsets.UTF_8));
+ }
+ LemmatizerModel lemmatizerModel = lemmatizerModelFile == null ? null : lemmatizerModels.get(lemmatizerModelFile);
+ return new NLPLemmatizerOp(dictionaryInputStream, lemmatizerModel);
+ }
+
+ public static String getLemmatizerDictionary(String dictionaryFile, ResourceLoader loader) throws IOException {
+ String dictionary = lemmaDictionaries.get(dictionaryFile);
+ if (dictionary == null) {
+ Reader reader = new InputStreamReader(loader.openResource(dictionaryFile), StandardCharsets.UTF_8);
+ StringBuilder builder = new StringBuilder();
+ char[] chars = new char[8092];
+ int numRead = 0;
+ do {
+ numRead = reader.read(chars, 0, chars.length);
+ if (numRead > 0) {
+ builder.append(chars, 0, numRead);
+ }
+ } while (numRead > 0);
+ dictionary = builder.toString();
+ lemmaDictionaries.put(dictionaryFile, dictionary);
+ }
+ return dictionary;
+ }
+
+ public static LemmatizerModel getLemmatizerModel(String modelName, ResourceLoader loader) throws IOException {
+ LemmatizerModel model = lemmatizerModels.get(modelName);
+ if (model == null) {
+ model = new LemmatizerModel(loader.openResource(modelName));
+ lemmatizerModels.put(modelName, model);
+ }
+ return model;
+ }
+
+ // keeps unit test from blowing out memory
+ public static void clearModels() {
+ sentenceModels.clear();
+ tokenizerModels.clear();
+ posTaggerModels.clear();
+ chunkerModels.clear();
+ nerModels.clear();
+ lemmaDictionaries.clear();
+ }
+}
diff --git a/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/package-info.java b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/package-info.java
new file mode 100644
index 0000000000..523a0842c9
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tools to supply access to OpenNLP components.
+ */
+package org.apache.lucene.analysis.opennlp.tools;
diff --git a/lucene/analysis/opennlp/src/java/overview.html b/lucene/analysis/opennlp/src/java/overview.html
new file mode 100644
index 0000000000..bf70e95507
--- /dev/null
+++ b/lucene/analysis/opennlp/src/java/overview.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ Apache Lucene OpenNLP integration module
+
+
+
+
+ This module exposes functionality from
+ Apache OpenNLP to Apache Lucene.
+ The Apache OpenNLP library is a machine learning based toolkit for the processing of natural language text.
+
+ For an introduction to Lucene's analysis API, see the {@link org.apache.lucene.analysis} package documentation.
+
+ The OpenNLP Tokenizer behavior is similar to the WhiteSpaceTokenizer but is smart about
+ inter-word punctuation. The term stream looks very much like the way you parse words and
+ punctuation while reading. The major difference between this tokenizer and most other
+ tokenizers shipped with Lucene is that punctuation is tokenized. This is required for
+ the following taggers to operate properly.
+
+ The OpenNLP taggers annotate terms using the TypeAttribute.
+
+ OpenNLPTokenizer segments text into sentences or words. This Tokenizer
+ uses the OpenNLP Sentence Detector and/or Tokenizer classes. When used together, the
+ Tokenizer receives sentences and can do a better job.
+ OpenNLPFilter tags words using one or more technologies: Part-of-Speech,
+ Chunking, and Named Entity Recognition. These tags are assigned as token types. Note that
+ only of these operations will tag
+
+
+
+ Since the TypeAttribute is not stored in the index, it is recommended that one
+ of these filters is used following OpenNLPFilter to enable search against the
+ assigned tags:
+
+ TypeAsPayloadFilter copies the TypeAttribute value to the
+ PayloadAttribute
+ TypeAsSynonymFilter creates a cloned token at the same position as each
+ tagged token, and copies the {{TypeAttribute}} value to the {{CharTermAttribute}}, optionally
+ with a customized prefix (so that tags effectively occupy a different namespace from token
+ text).
+
+
+
diff --git a/lucene/analysis/opennlp/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory b/lucene/analysis/opennlp/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory
new file mode 100644
index 0000000000..61a685d934
--- /dev/null
+++ b/lucene/analysis/opennlp/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenFilterFactory
@@ -0,0 +1,18 @@
+# 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.
+
+org.apache.lucene.analysis.opennlp.OpenNLPChunkerFilterFactory
+org.apache.lucene.analysis.opennlp.OpenNLPLemmatizerFilterFactory
+org.apache.lucene.analysis.opennlp.OpenNLPPOSFilterFactory
diff --git a/lucene/analysis/opennlp/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenizerFactory b/lucene/analysis/opennlp/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenizerFactory
new file mode 100644
index 0000000000..076b308848
--- /dev/null
+++ b/lucene/analysis/opennlp/src/resources/META-INF/services/org.apache.lucene.analysis.util.TokenizerFactory
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.lucene.analysis.opennlp.OpenNLPTokenizerFactory
diff --git a/lucene/analysis/opennlp/src/test-files/org/apache/lucene/analysis/opennlp/en-test-chunker.bin b/lucene/analysis/opennlp/src/test-files/org/apache/lucene/analysis/opennlp/en-test-chunker.bin
new file mode 100644
index 0000000000000000000000000000000000000000..8151914c291cfd8638993065784bc3020cb238f1
GIT binary patch
literal 89915
zcmV(*K;FMlO9KQH00;;O0Ku4uO8@`>000000000001W^D0Apx%Zfj+7E^TjQWo(qa
zcVJXS*EqZi={>Z74G`2vmX#zxK*fP18Wv@XXHGw7&bS)ocmpn%>k@L#CI5?c
zHO}nl^V9$5v;X_8|9e@Bm$qDz>k7~8*el!m)A~1%*{dIa?qfZB`M+V-NB#0#VP5u+
z>k^MEtaI12)}378UAnaH)H>A_na=)go#u+}oX-Dho$iWF&5(b$?(B+BOVdxSySNfl
zQ{i*#uCAnRxS@51t10Y)x3%u(YL;$qY~9_JlIGOhI<=FlxzjJHsjil(ws!hH{i&(_
zC#$1m>qp$&&DErf*<{V7x%oMpNvbQUtI^)w>eFblUQ0~Hoo2X_I^za9aH`ovdrLDK
ztyw2vz;-i-?8cX7wxzlfaA)0Ji5T+MD{-*J+C$h22Sprg%5WvXNA^4Hp}Q*{{%#Er
z+g*dAi}fpbvc`j0#8D=x3vRHMn~;v0y1_Z{J%~?(uVg@oaahN}hHkES__{U882#MU
z6{De+0Ux@#V)VG(T~QJhI=f=jpVkr;JUCUTxk{Q)n4k&5c*mMN@R4>3fA40Iw|*f5_SDT4C4T|Hpr5EEXXsbDxgxvrchX(aVs|OO
zh{=$DQkbOa2MUvP_-J(@&<;|dn7*d7IM|?I!qGMkJmB9l;BO2H`jHM3o!&`2chOVs
z0=wkUj7e9I=m88y2`s9+cpr|V5~MRumUmd5rko&QDmuZ#Rs&I)k)H2BDR
zZJKH}G0ufs1rBPih<;bQlZH20@}@XFYFGR!MGx?527ExB(EaW#o&@!z!Tr{3s0n%$
zLIu6kP0wE9Y)rap$k5Z}D+9lEn3Ltfj=J
zrpmwdS|rCw=n5Yx`-5-Oxn-!w49+Uk)Q%*k!AH8OVkLZsqU76ZC7s!r@!$b{#8%ct
zJY>i{;ItVfU$VHIY`4DUL_qmn6mRXKN9zJdQ*vItmm$~27ZWAlvj&iVTKgKo|L!U$
zM!Sqm=g`fNon`2c*ccJK*&?X43<}_MLC}9(=v(PbzNYpA2}N*^{+0-Wy|9yg&6-2IGJH_ilV93?0JL
z>1bW}`*d{iM>kg(oqtzX7{$p9zN@s8F#5jrPpb7{sw>V>rR&n6rkvC8A|=3WU7@P3u;`sY*|4B)+
zOPUa#7K%#IDL(5WXP+(!E>-GV5$QrmMx+Y47@ppli9*((7RG@a&KA~%&$^48KJ7j%
zl}S8nXEUrF4YxESx}Vn1d^e-?UPfB|^F6RulBOq`rY9_jPgVlMu1gd035priERWCUY{$OH~MK~R<_mV^SNTJzy1CE)?@2B<{T3k{z
zdU(*)oVMi#3)8HR{Xy$*H|xy$vXG9!zjr0k7d)!TdR5K{4jynNff3xnG&;#R-sJOF
zcIr_Y1fS{7T+MW2nl;lL`deNupBuN``2_hlu0$}THAq&LD~7LvPu8h7JrTdZGPOrh
zg{!5*L;@U!DX;@KoG6<;bu!RKA8pLeb68TJKGq!B8rU1W*CqfqYT^ye!kTZw$ig%V
zH~EIEg<3$T9>HP7)-pyEje4*+NZ(|8m}+ein^6kvl}*B$E9ro;kE(DbO2qK`1Q8@E=nW|!F|w#^teyx5L{m5v$2kiZn*hj281Ol>
zSJPPEP1PFMTVjS975enyV1r99FQe2Y)}sgsy-|XuXc1?O-U2`R%{C{6vT{xwkCOqM
z2A2*WUTV#vnZhDBgjWjco{cGb)^nuHfrFYq(hUukW9$$B;LvbGL`MMkN+P#^EExs=
zTe27mf_T`2HAXz{kueNCcM4y7?nhgrzsnX__UZf
z=ajjKJuBky2IVN!ed$2b0{5-&tO+GSWV4#A^>cq~!L6B+0b*Aqh0e&LQ9-c33}MBdCE75pg_9^^uOiHJY6;NpB((}A=sYv86C<`7
zlkoRcjwHaHrxP?9a`?&sm)+h~4K&m+i0TY4B!9~5fV_De_iQb6w2@8}g
zp-@uzfLEmPWBbhO1Iv(|hjAIIBsOmc@C`sP#7MDAnGarq=2cpCX$YVeWMJ=7%*6w7
z;$Z7YN``B<=A`7dRsfRhCSS6N_gR2fQ#*=L4iZ9)T}R744AqCCa-wO%>9@18G2RUx
zEATE_^K+_22xU^WTBA6v+CpQbftHUc9>q7Kt=J5xUm&0+nP}j)2vnjjT@(^nZsXa*
zB@_jaP4Cbg&nqC83(Xaqav<4LGYmGjWY7psaS#fg20GLFwNw(sG-Cj9+P7Y|d6DJ<
zS86?7hPMIp9-I0lwTcqJh4j
zsEI{rn8{L+2Wy0^PI!+1`FgY-MK=Md4Ax!(l|g*qrsx6Gi3Gi&fOX_TRWQle7wfBV
zsJFr8Q`-?qN@`og{vW!KR0-qI@_{nz28=$zR0pAs;`IrRZQm@dPuMp`TD-bjuhKsy
zl4u`QD~x7HD~y3AW(?$CcEwCPA0w6y&Vg1EjeR8kLC#AqQ+N{)WLI
z4QB;&tDy-ZG0_9GU4$YaMoWT-XUKGBg_JY3H6C<8RZ3F_qEfTLX-ddSkAU$aVsH)r
zhLtRY{c^G;qsm9vuwUwmK>MJ|FXx4lZyHver-61i|E`=l*3xn{!kOZrfUgw#fuRxH
z%7$B7{V{Mv;;8hCCIRSrkb0n;eAuKZfnK~^pj;~pPg1Jr$0yYw;{}iGSUPG0YDT6<
zk{Pg-rNLohY^}iXXLCa=Sw&h?cKCeUj3(7)hfR3w*T1M=YA10*(}j0IOrcw2CBp
zI{uT$mDh(Qj
zSAc7h*!V#kE6pHHGn{{;rC5A{#|iunaRQBp$mq4}YiQ4bn;;G}HsIJ7Gy%udY}>0m
zwx)9~5sb}MKC~>0Z2`Wma08f+a$}nVs0y<#X!AzX4Mu#D%Xguc&Q%swAUyIoNKGt}oi_%#67)9O
z-03`u*u>#gBV3@=x5WQf*l=qCsFKMBRx#HIT2B@ZVKTIa-hR$6B|i~@jgtrPqAFI(
zC@{%{cc20KfetwJ4dqR-3LROb)TY{D&ADHuSZ)fUE$9Tp{_=oOl^s1++5ej`eLe$%
zQbbGX9*?KiFo(c#F(u>z+(2ja<@7s{%*r~mGfF#t`Fw_}TC$T^Kn!m5)E09L0U7v4
zkuez`w$`+#L>qJz@NsB%w)s2q1!+&&om)~|V|TjLYakut2shTAkwoLH1u;D8kvA26
zZLmum!of@oiw4|nK^O}#oEA--+yM5y^(v1jj3gVA_1WAO*~p5J8{|gpk;;B3LM6^Q
z7!?6W--rN3BHA40dDg!($Df^eQQ(z+qWMfJL{+AqB6ot=2$}F;@KlO|u?Pf^Jp=to
z%)Oa?Md~F*wXujs?OHfRs3b#jn_dv%JftSOwc@)h?vY^NG^9voxKi$xVDy39a_G%i
zG`4A=u-9l10BLq5!QxTCp)M#P*20a7U{SDmL~&UVxpurhojZaZGuFRtnT0EDGmhjo
z_c``z%UMCJq#@uW1sb(H!*_(z0@7&EKb(8}gn3?J{hQk3wqjdfz%49{-W-aqaz+#t
zqokUF3?qH?ta53_;>w9RJYjY;1ASD4dt!1`Fq^mIt(OU!B{GY%6C4
z%RflNl)-T|igrG@sO-TC%G#uZ!EcL5NDPb7o)Et)LM{uru-(iBoR`aaHVQ(!K`-2^
zYwZl9i_s%FBIuFehN)n%*m8v=mYPV(sjRqCf|eN}an?^GN0kp9JtRm|VqyTGG}xs>
z2U?xfc4Vs>JUS>p
<)ee32aA%*M-LxrO^T{#!-w?}^JFh2z!oYmX*{8v1{ByXE6^p#PUJY+70nn*g#11Vy6!9iP&(WdX8l`M
z&i0Tw45Ng}XOTy=)s{E9xMFD0Sl-1ji}%KGcZp=;GHPe>j4(!7d{_7u0O@M&p4j$E
z`bV18*H9+Z^E0X9po4!Fw+FOF`2=+U1?ddwM`Ma5Ei!bsW+^9ZO(#n43tb+i{ut;=
zqK(vZ6vnJ!fm+;462>7#36=sXnod!KG3mg}`Un@fRxy(0GuoqX-um!JYa=Sy0Q3#E
zLW<$xEcjAEP4u)LfngTbRg{-$oTf5G{>_}^sccVTp!yibZDG1k)~9l{f>Cj(Sr2SP
zI#L)F;^D{P)ImU2L3uYnLS0!zIg+N|fr-=VRbv
zTTYwU+^ii66V7C@F%g3I&<6IA=qVt6@rhsP!bMVDn3%9g5(*b~X}GXjBSgBYD~!>m
z^*Un~5t1Q@xo2AwVm~zIs33MfN@Y8-ap=6|VuXsx4b(_Ub*{!#>hSf@*D9ImA0g#e
z7#9)Iz)cXwCdxYGLKL_KXn#!bidh!$EiDoA^UID4IM*z#5X+?}kC{RrCU#n;iiL5e
z7{*g0!v&RvaV{6e1GW(il0`wy1o@J~c_&<5pbpixY#maIU`P~{L*?)psSU|t7)AA%
z7|7_tg>NHV7$;#|HxSFs;Zt(+p&7;sF4ve1AI3$pf~K`S5nOr<<)2XMr2krgWz(^?
z0;r&L;Ks~gZ_Lc_#+<5|fZ?J=@erouj>3iOPjeNWnC25YJC({@31EVZr3W@aGEwlXNU6E;6?=VBfy>yLt<`1Oufo14Chu8245*oUeb7Bm6@*HW?v;$-%b8Cs_|k)(+?*a*R1Sik&Dqu!
z%^IPvL&6SYdK>?zf-`WhP-LW4risU*k4mlb#X=c`n29#u6w1XxDk(xl9ngX)D;w{G
z>OG8qhP3-^^e8DpJTiSCjY=nqaLP`BFp(NvxZty~TqP4DA}%Zig8C9qQ+=dVTHJy3
zhN}}tOa%n=Z9p(hzVC(aC*TePTnTh@t_4E;0gv5se*)ycM->vyk26IAGl2<%21IRi(bY|Eyv(Y0l-^)
zKJA9^arKJ{-teH5DU|p|3HwUYBsZF<;TIt%&@Mg7R;2u_wfJYy-X9y`R?0k@AsGTe>Hq*ObpnEY=d6aeOmm5CTS5
z0>CXR=ai;m8`9wZ%|0mny04~YPP0%=$V)X37Z@{$s9d`fa6
z=2(aYO}QE!6zh=!hw^dIxtOfzM4=g_e?xnX+B6GWPwR=DTNjXB%F*+Gp=)^fRe^u%h(YisB9fv^-=*I?ac7PFJiUhF6
zY3_WJ8aASgR1j7wWafCRS`hQ~0^XAgdS#hlnu@cbG^CFJ3;{5GO5A#3MM0KgO#SrH
zseWU|OPfC9=b
zJhC!dB$|gT1`jVDA;Oi=2^_D7W^ybBX4TQT6j?%{=>B49ipW?L1rhfs%lKqoE_l#*
zBT~=PR7cuVF8wFa@{w*$sO5U3-HE2aGZwg%V|lCEfe)5I<=QBVr!r{sZ2pK!abkoa
z;E3YlU}@R?+Z{$r}2xS#e;*az=8ycU!>fY0L>*;
zs+S&N)`=*Cr2~k+N-JdFVpQryc5_~lq*kvKDyWvYL3~;QR_mB3JgdT_36d>oeV04_
z;}Ccm6sZ-)C#M3w5B<>FT5kS*5#n*k+TLxGj#vLMZ`B;
ztWCIK{h~7+E8qbRX+sK6ep_zxjpd%l5?6dBwsjdtkUJA5;_uog>Svx$YB+N0;6Bf6
zg)$bD8Z#ErILHsKu}sP^j)g+|EDFLHi4W~NjgW$kjn@0dMie0P1WkP%l^5~Bs5^90
zTD`|A2cEWnO!GUaU0Z^s4(3?VFH=kzvctCMO?pmD`ewX7N6%W8Vd;C2my{|VA~l=s
zx0oC(Ww@nsg`RG>7rKOj*BiTOp=@)QGv6BcXw8G+VjIzLQF;MK#ZDZz=nQ<&A#4Wbk
zUJph@4!G(DqQ;8!3UvaC?#a*SD?gL*8jFi?;g-aC45a@=)g<*d&7XOqy#5_LczL9D
zFz1pi{IBb+@WE*K8pJz_MuCHv@ZhS%AS&~vaK#xZK3kGk0I3&Vq{Zc+R2eBDqGXTG
zxpGFDhxRr5h3`^pm-5(HF670Wdlvw$Ptk%lc$mitYM$j)q(R)AzG9lnDZnykWz
zG`)m2870n-sPo4B?_{<6I-sDBIkAptg%A3I%)W?GpIB2t$40XmwK1kd(^twzS{gE=
zmn31OgLEhQCgV8xh?JjOwc>aH)1<^|KmilQQ>$}X?Jc+Ey7sD*|q`wdJztzQGE{!a*^PJoUJ-RDxy-kRMMCN9v18*
zgW5s~@MEQj=LJz6IZ~aykLBe^%7&DVED|YQmMR{>GPG#k7Cc%)TStsWF;~`Wl&%U8
ztp{gHB||ee6NdtFsb&@y)LYz)tVWI=JiK%W8%}+saUVG{xd|s6M%=+$R&39|a*Vt(
z
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
B^f-n-aP)VRENj1Y?Xv*SYiu4{9TAg
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