This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new bdb16c9706c SOLR-18362: replace org.apache.solr.spelling.Token with
SpellCheckToken (#4812)
bdb16c9706c is described below
commit bdb16c9706cf1cc94fba701c7e81aa89c19fd7a0
Author: Serhiy Bzhezytskyy <[email protected]>
AuthorDate: Fri Sep 11 04:12:54 2026 +0300
SOLR-18362: replace org.apache.solr.spelling.Token with SpellCheckToken
(#4812)
The spellchecker API no longer passes terms around as
org.apache.solr.spelling.Token, which is removed. Custom plugins in the
org.apache.solr.spelling package need updating: QueryConverter.convert(String)
now returns a Lucene TokenStream rather than a Collection<Token>,
SpellingOptions.tokens is a List<SpellCheckToken> rather than a
Collection<Token>, and SpellingResult is keyed by the new immutable record
SpellCheckToken wherever it previously took a Token. Behaviour of the shipped s
[...]
Co-authored-by: Eric Pugh <[email protected]>
---
.../SOLR-18362-remove-spelling-token.yml | 15 ++
.../handler/component/SpellCheckComponent.java | 65 +++-----
.../solr/spelling/AbstractLuceneSpellChecker.java | 8 +-
.../solr/spelling/ConjunctionSolrSpellChecker.java | 12 +-
.../solr/spelling/DirectSolrSpellChecker.java | 8 +-
.../apache/solr/spelling/PossibilityIterator.java | 7 +-
.../org/apache/solr/spelling/QueryConverter.java | 10 +-
.../solr/spelling/QueryWordsTokenStream.java | 149 ++++++++++++++++++
.../java/org/apache/solr/spelling/ResultEntry.java | 4 +-
.../org/apache/solr/spelling/SolrSpellChecker.java | 3 +-
.../apache/solr/spelling/SpellCheckCollator.java | 6 +-
.../apache/solr/spelling/SpellCheckCorrection.java | 6 +-
.../org/apache/solr/spelling/SpellCheckToken.java | 119 ++++++++++++++
.../org/apache/solr/spelling/SpellingOptions.java | 19 ++-
.../solr/spelling/SpellingQueryConverter.java | 78 +++-------
.../org/apache/solr/spelling/SpellingResult.java | 43 ++----
.../solr/spelling/SuggestQueryConverter.java | 18 +--
.../src/java/org/apache/solr/spelling/Token.java | 172 ---------------------
.../solr/spelling/WordBreakSolrSpellChecker.java | 22 +--
.../apache/solr/spelling/suggest/Suggester.java | 12 +-
.../solr/spelling/DirectSolrSpellCheckerTest.java | 25 +--
.../solr/spelling/FileBasedSpellCheckerTest.java | 36 +++--
.../solr/spelling/IndexBasedSpellCheckerTest.java | 58 ++++---
.../apache/solr/spelling/SimpleQueryConverter.java | 43 +-----
.../spelling/SpellPossibilityIteratorTest.java | 55 +++----
.../solr/spelling/SpellingQueryConverterTest.java | 138 +++++++++++------
.../spelling/TestSuggestSpellingConverter.java | 21 ++-
.../spelling/WordBreakSolrSpellCheckerTest.java | 12 +-
.../component/DummyCustomParamSpellChecker.java | 4 +-
29 files changed, 622 insertions(+), 546 deletions(-)
diff --git a/changelog/unreleased/SOLR-18362-remove-spelling-token.yml
b/changelog/unreleased/SOLR-18362-remove-spelling-token.yml
new file mode 100644
index 00000000000..468ed975392
--- /dev/null
+++ b/changelog/unreleased/SOLR-18362-remove-spelling-token.yml
@@ -0,0 +1,15 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: >
+ The spellchecker API no longer passes terms around as
`org.apache.solr.spelling.Token`, which is
+ removed. Custom plugins in the `org.apache.solr.spelling` package need
updating:
+ `QueryConverter.convert(String)` now returns a Lucene `TokenStream` rather
than a
+ `Collection<Token>`, `SpellingOptions.tokens` is a `List<SpellCheckToken>`
rather than a
+ `Collection<Token>`, and `SpellingResult` is keyed by the new immutable
record `SpellCheckToken`
+ wherever it previously took a `Token`. Behaviour of the shipped
spellcheckers and of the `/spell`
+ request handler is unchanged.
+type: removed
+authors:
+ - name: Serhiy Bzhezytskyy
+links:
+ - name: SOLR-18362
+ url: https://issues.apache.org/jira/browse/SOLR-18362
diff --git
a/solr/core/src/java/org/apache/solr/handler/component/SpellCheckComponent.java
b/solr/core/src/java/org/apache/solr/handler/component/SpellCheckComponent.java
index 98af4ed46dc..0221f720f4d 100644
---
a/solr/core/src/java/org/apache/solr/handler/component/SpellCheckComponent.java
+++
b/solr/core/src/java/org/apache/solr/handler/component/SpellCheckComponent.java
@@ -20,7 +20,6 @@ import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -32,12 +31,6 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
-import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
-import org.apache.lucene.analysis.tokenattributes.PayloadAttribute;
-import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
-import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.ExitableDirectoryReader;
import org.apache.lucene.search.Query;
@@ -69,10 +62,10 @@ import org.apache.solr.spelling.QueryConverter;
import org.apache.solr.spelling.SolrSpellChecker;
import org.apache.solr.spelling.SpellCheckCollation;
import org.apache.solr.spelling.SpellCheckCollator;
+import org.apache.solr.spelling.SpellCheckToken;
import org.apache.solr.spelling.SpellingOptions;
import org.apache.solr.spelling.SpellingQueryConverter;
import org.apache.solr.spelling.SpellingResult;
-import org.apache.solr.spelling.Token;
import org.apache.solr.util.SolrResponseUtil;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.slf4j.Logger;
@@ -143,20 +136,23 @@ public class SpellCheckComponent extends SearchComponent
implements SolrCoreAwar
SolrSpellChecker spellChecker = getSpellChecker(params);
if (spellChecker != null) {
- Collection<Token> tokens;
String q = params.get(SPELLCHECK_Q);
+ // the query is analyzed once, here: every spellchecker reads every
token, and
+ // ConjunctionSolrSpellChecker hands the same options to each of its
children, so one list
+ // serves them all.
+ final List<SpellCheckToken> tokens;
if (q != null) {
// we have a spell check param, tokenize it with the query analyzer
applicable for this
// spellchecker
- tokens = getTokens(q, spellChecker.getQueryAnalyzer());
+ tokens = SpellCheckToken.drain(getTokens(q,
spellChecker.getQueryAnalyzer()));
} else {
q = rb.getQueryString();
if (q == null) {
q = params.get(CommonParams.Q);
}
- tokens = queryConverter.convert(q);
+ tokens = SpellCheckToken.drain(queryConverter.convert(q));
}
- if (tokens != null && tokens.isEmpty() == false) {
+ if (!tokens.isEmpty()) {
int count = params.getInt(SPELLCHECK_COUNT, 1);
boolean onlyMorePopular =
params.getBool(SPELLCHECK_ONLY_MORE_POPULAR,
DEFAULT_ONLY_MORE_POPULAR);
@@ -347,10 +343,10 @@ public class SpellCheckComponent extends SearchComponent
implements SolrCoreAwar
}
private void addOriginalTermsToResponse(
- NamedList<Object> response, Collection<Token> originalTerms) {
- List<String> originalTermStr = new ArrayList<String>();
- for (Token t : originalTerms) {
- originalTermStr.add(t.toString());
+ NamedList<Object> response, List<SpellCheckToken> originalTerms) {
+ List<String> originalTermStr = new ArrayList<>(originalTerms.size());
+ for (SpellCheckToken token : originalTerms) {
+ originalTermStr.add(token.text());
}
response.add("originalTerms", originalTermStr);
}
@@ -580,32 +576,9 @@ public class SpellCheckComponent extends SearchComponent
implements SolrCoreAwar
}
}
- private Collection<Token> getTokens(String q, Analyzer analyzer) throws
IOException {
- Collection<Token> result = new ArrayList<>();
+ private TokenStream getTokens(String q, Analyzer analyzer) {
assert analyzer != null;
- try (TokenStream ts = analyzer.tokenStream("", q)) {
- ts.reset();
- // TODO: support custom attributes
- CharTermAttribute termAtt = ts.addAttribute(CharTermAttribute.class);
- OffsetAttribute offsetAtt = ts.addAttribute(OffsetAttribute.class);
- TypeAttribute typeAtt = ts.addAttribute(TypeAttribute.class);
- FlagsAttribute flagsAtt = ts.addAttribute(FlagsAttribute.class);
- PayloadAttribute payloadAtt = ts.addAttribute(PayloadAttribute.class);
- PositionIncrementAttribute posIncAtt =
ts.addAttribute(PositionIncrementAttribute.class);
-
- while (ts.incrementToken()) {
- Token token = new Token();
- token.copyBuffer(termAtt.buffer(), 0, termAtt.length());
- token.setOffset(offsetAtt.startOffset(), offsetAtt.endOffset());
- token.setType(typeAtt.type());
- token.setFlags(flagsAtt.getFlags());
- token.setPayload(payloadAtt.getPayload());
- token.setPositionIncrement(posIncAtt.getPositionIncrement());
- result.add(token);
- }
- ts.end();
- return result;
- }
+ return analyzer.tokenStream("", q);
}
protected SolrSpellChecker getSpellChecker(SolrParams params) {
@@ -658,13 +631,15 @@ public class SpellCheckComponent extends SearchComponent
implements SolrCoreAwar
String origQuery,
boolean extendedResults) {
NamedList<Object> result = new NamedList<>();
- Map<Token, LinkedHashMap<String, Integer>> suggestions =
spellingResult.getSuggestions();
+ Map<SpellCheckToken, LinkedHashMap<String, Integer>> suggestions =
+ spellingResult.getSuggestions();
boolean hasFreqInfo = spellingResult.hasTokenFrequencyInfo();
boolean hasSuggestions = false;
boolean hasZeroFrequencyToken = false;
- for (Map.Entry<Token, LinkedHashMap<String, Integer>> entry :
suggestions.entrySet()) {
- Token inputToken = entry.getKey();
- String tokenString = new String(inputToken.buffer(), 0,
inputToken.length());
+ for (Map.Entry<SpellCheckToken, LinkedHashMap<String, Integer>> entry :
+ suggestions.entrySet()) {
+ SpellCheckToken inputToken = entry.getKey();
+ String tokenString = inputToken.text();
Map<String, Integer> theSuggestions = new
LinkedHashMap<>(entry.getValue());
theSuggestions.keySet().removeIf(sug -> sug.equals(tokenString));
if (theSuggestions.size() > 0) {
diff --git
a/solr/core/src/java/org/apache/solr/spelling/AbstractLuceneSpellChecker.java
b/solr/core/src/java/org/apache/solr/spelling/AbstractLuceneSpellChecker.java
index 25cb239b2e0..284df9a71e8 100644
---
a/solr/core/src/java/org/apache/solr/spelling/AbstractLuceneSpellChecker.java
+++
b/solr/core/src/java/org/apache/solr/spelling/AbstractLuceneSpellChecker.java
@@ -132,19 +132,19 @@ public abstract class AbstractLuceneSpellChecker extends
SolrSpellChecker {
@Override
public SpellingResult getSuggestions(SpellingOptions options) throws
IOException {
- SpellingResult result = new SpellingResult(options.tokens);
+ SpellingResult result = new SpellingResult();
IndexReader reader = determineReader(options.reader);
Term term = field != null ? new Term(field, "") : null;
float theAccuracy =
(options.accuracy == Float.MIN_VALUE) ? spellChecker.getAccuracy() :
options.accuracy;
int count = Math.max(options.count,
AbstractLuceneSpellChecker.DEFAULT_SUGGESTION_COUNT);
- for (Token token : options.tokens) {
- if (token.length() == 0) {
+ for (SpellCheckToken token : options.tokens) {
+ String tokenText = token.text();
+ if (tokenText.isEmpty()) {
result.add(token, List.of());
continue;
}
- String tokenText = new String(token.buffer(), 0, token.length());
term = new Term(field, tokenText);
int docFreq = 0;
if (reader != null) {
diff --git
a/solr/core/src/java/org/apache/solr/spelling/ConjunctionSolrSpellChecker.java
b/solr/core/src/java/org/apache/solr/spelling/ConjunctionSolrSpellChecker.java
index dc67b47cb17..077464bd0aa 100644
---
a/solr/core/src/java/org/apache/solr/spelling/ConjunctionSolrSpellChecker.java
+++
b/solr/core/src/java/org/apache/solr/spelling/ConjunctionSolrSpellChecker.java
@@ -131,13 +131,14 @@ public class ConjunctionSolrSpellChecker extends
SolrSpellChecker {
// TODO: This just interleaves the results. In the future, we might want to
let users give each
// checker its own weight and use that in combination to score & frequency
to sort the results ?
private SpellingResult mergeCheckers(SpellingResult[] results, int numSug) {
- Map<Token, Integer> combinedTokenFrequency = new HashMap<>();
- Map<Token, List<LinkedHashMap<String, Integer>>> allSuggestions = new
LinkedHashMap<>();
+ Map<SpellCheckToken, Integer> combinedTokenFrequency = new HashMap<>();
+ Map<SpellCheckToken, List<LinkedHashMap<String, Integer>>> allSuggestions =
+ new LinkedHashMap<>();
for (SpellingResult result : results) {
if (result.getTokenFrequency() != null) {
combinedTokenFrequency.putAll(result.getTokenFrequency());
}
- for (Map.Entry<Token, LinkedHashMap<String, Integer>> entry :
+ for (Map.Entry<SpellCheckToken, LinkedHashMap<String, Integer>> entry :
result.getSuggestions().entrySet()) {
List<LinkedHashMap<String, Integer>> allForThisToken =
allSuggestions.get(entry.getKey());
if (allForThisToken == null) {
@@ -148,8 +149,9 @@ public class ConjunctionSolrSpellChecker extends
SolrSpellChecker {
}
}
SpellingResult combinedResult = new SpellingResult();
- for (Map.Entry<Token, List<LinkedHashMap<String, Integer>>> entry :
allSuggestions.entrySet()) {
- Token original = entry.getKey();
+ for (Map.Entry<SpellCheckToken, List<LinkedHashMap<String, Integer>>>
entry :
+ allSuggestions.entrySet()) {
+ SpellCheckToken original = entry.getKey();
List<Iterator<Map.Entry<String, Integer>>> corrIters =
new ArrayList<>(entry.getValue().size());
for (LinkedHashMap<String, Integer> corrections : entry.getValue()) {
diff --git
a/solr/core/src/java/org/apache/solr/spelling/DirectSolrSpellChecker.java
b/solr/core/src/java/org/apache/solr/spelling/DirectSolrSpellChecker.java
index acbb8828316..09397ce39cf 100644
--- a/solr/core/src/java/org/apache/solr/spelling/DirectSolrSpellChecker.java
+++ b/solr/core/src/java/org/apache/solr/spelling/DirectSolrSpellChecker.java
@@ -183,18 +183,16 @@ public class DirectSolrSpellChecker extends
SolrSpellChecker {
@Override
public SpellingResult getSuggestions(SpellingOptions options) throws
IOException {
- log.debug("getSuggestions: {}", options.tokens);
-
SpellingResult result = new SpellingResult();
float accuracy =
(options.accuracy == Float.MIN_VALUE) ? checker.getAccuracy() :
options.accuracy;
- for (Token token : options.tokens) {
- if (token.length() == 0) {
+ for (SpellCheckToken token : options.tokens) {
+ String tokenText = token.text();
+ if (tokenText.isEmpty()) {
result.add(token, List.of());
continue;
}
- String tokenText = token.toString();
Term term = new Term(field, tokenText);
int freq = options.reader.docFreq(term);
int count =
diff --git
a/solr/core/src/java/org/apache/solr/spelling/PossibilityIterator.java
b/solr/core/src/java/org/apache/solr/spelling/PossibilityIterator.java
index 821df88c4c9..2af1f069d0e 100644
--- a/solr/core/src/java/org/apache/solr/spelling/PossibilityIterator.java
+++ b/solr/core/src/java/org/apache/solr/spelling/PossibilityIterator.java
@@ -55,13 +55,14 @@ public class PossibilityIterator implements
Iterator<PossibilityIterator.RankedS
* Possible Correction".
*/
public PossibilityIterator(
- Map<Token, LinkedHashMap<String, Integer>> suggestions,
+ Map<SpellCheckToken, LinkedHashMap<String, Integer>> suggestions,
int maximumRequiredSuggestions,
int maxEvaluations,
boolean overlap) {
this.suggestionsMayOverlap = overlap;
- for (Map.Entry<Token, LinkedHashMap<String, Integer>> entry :
suggestions.entrySet()) {
- Token token = entry.getKey();
+ for (Map.Entry<SpellCheckToken, LinkedHashMap<String, Integer>> entry :
+ suggestions.entrySet()) {
+ SpellCheckToken token = entry.getKey();
if (entry.getValue().size() == 0) {
continue;
}
diff --git a/solr/core/src/java/org/apache/solr/spelling/QueryConverter.java
b/solr/core/src/java/org/apache/solr/spelling/QueryConverter.java
index 307e27471e9..55106c5c660 100644
--- a/solr/core/src/java/org/apache/solr/spelling/QueryConverter.java
+++ b/solr/core/src/java/org/apache/solr/spelling/QueryConverter.java
@@ -16,8 +16,8 @@
*/
package org.apache.solr.spelling;
-import java.util.Collection;
import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
import org.apache.solr.util.plugin.NamedListInitializedPlugin;
/**
@@ -57,10 +57,12 @@ public abstract class QueryConverter implements
NamedListInitializedPlugin {
public static final int TERM_IN_BOOLEAN_QUERY_FLAG = 131072;
/**
- * Returns the Collection of {@link Token}s for the query. Offsets on the
Token should correspond
- * to the correct offset in the origQuery
+ * Returns a fresh {@link TokenStream} over the query's terms. Offsets
should correspond to the
+ * correct offset in the origQuery. The caller owns the returned stream's
lifecycle: {@link
+ * SpellCheckToken#drain(TokenStream)} resets it, reads it to the end and
closes it, and the
+ * spellcheckers read that list rather than this stream.
*/
- public abstract Collection<Token> convert(String original);
+ public abstract TokenStream convert(String original);
/** Set the analyzer to use. Must be set before any calls to convert. */
public void setAnalyzer(Analyzer analyzer) {
diff --git
a/solr/core/src/java/org/apache/solr/spelling/QueryWordsTokenStream.java
b/solr/core/src/java/org/apache/solr/spelling/QueryWordsTokenStream.java
new file mode 100644
index 00000000000..92524dba6ec
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/spelling/QueryWordsTokenStream.java
@@ -0,0 +1,149 @@
+/*
+ * 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.solr.spelling;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
+import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
+import org.apache.lucene.analysis.tokenattributes.PayloadAttribute;
+import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.apache.lucene.util.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Analyzes each parsed query word in turn and concatenates their analysis
into a single stream,
+ * shifting offsets to the original query string and setting {@link
FlagsAttribute} from the
+ * query-syntax parse ({@link QueryConverter#REQUIRED_TERM_FLAG} and friends).
Each word is only
+ * analyzed once this stream reaches it, not up front.
+ */
+final class QueryWordsTokenStream extends TokenStream {
+
+ private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ /** One query word as parsed from query syntax, prior to analysis. */
+ record ParsedWord(String text, int startIndex, int flags) {}
+
+ private final List<ParsedWord> words;
+ private final Analyzer analyzer;
+ private int nextWordIndex;
+ private int currentWordIndex;
+ private TokenStream current;
+ private CharTermAttribute currentTermAtt;
+ private OffsetAttribute currentOffsetAtt;
+ private TypeAttribute currentTypeAtt;
+ private PositionIncrementAttribute currentPosIncAtt;
+ private PayloadAttribute currentPayloadAtt;
+
+ private final CharTermAttribute termAtt =
addAttribute(CharTermAttribute.class);
+ private final OffsetAttribute offsetAtt =
addAttribute(OffsetAttribute.class);
+ private final TypeAttribute typeAtt = addAttribute(TypeAttribute.class);
+ private final PositionIncrementAttribute posIncAtt =
+ addAttribute(PositionIncrementAttribute.class);
+ private final PayloadAttribute payloadAtt =
addAttribute(PayloadAttribute.class);
+ private final FlagsAttribute flagsAtt = addAttribute(FlagsAttribute.class);
+
+ QueryWordsTokenStream(List<ParsedWord> words, Analyzer analyzer) {
+ this.words = words;
+ this.analyzer = analyzer;
+ }
+
+ @Override
+ public boolean incrementToken() throws IOException {
+ while (true) {
+ // A word whose analysis fails is skipped and the query's other words
are still checked; an
+ // IOException escaping here would fail the whole spellcheck request
instead.
+ if (current != null) {
+ try {
+ if (current.incrementToken()) {
+ ParsedWord word = words.get(currentWordIndex);
+ clearAttributes();
+ termAtt.append(currentTermAtt);
+ offsetAtt.setOffset(
+ word.startIndex() + currentOffsetAtt.startOffset(),
+ word.startIndex() + currentOffsetAtt.endOffset());
+ typeAtt.setType(currentTypeAtt.type());
+
posIncAtt.setPositionIncrement(currentPosIncAtt.getPositionIncrement());
+ payloadAtt.setPayload(currentPayloadAtt.getPayload());
+ flagsAtt.setFlags(word.flags());
+ return true;
+ }
+ current.end();
+ } catch (IOException e) {
+ log.warn("Skipping the rest of query word '{}': its analysis
failed", currentWord(), e);
+ }
+ IOUtils.closeWhileHandlingException(current);
+ current = null;
+ }
+ if (nextWordIndex >= words.size()) {
+ return false;
+ }
+ currentWordIndex = nextWordIndex++;
+ try {
+ current = analyzer.tokenStream("", currentWord());
+ currentTermAtt = current.addAttribute(CharTermAttribute.class);
+ currentOffsetAtt = current.addAttribute(OffsetAttribute.class);
+ currentTypeAtt = current.addAttribute(TypeAttribute.class);
+ currentPosIncAtt =
current.addAttribute(PositionIncrementAttribute.class);
+ currentPayloadAtt = current.addAttribute(PayloadAttribute.class);
+ current.reset();
+ } catch (IOException e) {
+ log.warn("Skipping query word '{}': its analysis failed",
currentWord(), e);
+ IOUtils.closeWhileHandlingException(current);
+ current = null;
+ }
+ }
+ }
+
+ private String currentWord() {
+ return words.get(currentWordIndex).text();
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ nextWordIndex = 0;
+ currentWordIndex = 0;
+ if (current != null) {
+ current.close();
+ current = null;
+ }
+ }
+
+ @Override
+ public void end() throws IOException {
+ super.end();
+ if (current != null) {
+ current.end();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (current != null) {
+ current.close();
+ current = null;
+ }
+ super.close();
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/spelling/ResultEntry.java
b/solr/core/src/java/org/apache/solr/spelling/ResultEntry.java
index cadb3c76d6e..5f8ea9334ec 100644
--- a/solr/core/src/java/org/apache/solr/spelling/ResultEntry.java
+++ b/solr/core/src/java/org/apache/solr/spelling/ResultEntry.java
@@ -19,11 +19,11 @@ package org.apache.solr.spelling;
import java.util.Objects;
public class ResultEntry {
- public Token token;
+ public SpellCheckToken token;
public String suggestion;
public int freq;
- ResultEntry(Token t, String s, int f) {
+ ResultEntry(SpellCheckToken t, String s, int f) {
token = t;
suggestion = s;
freq = f;
diff --git a/solr/core/src/java/org/apache/solr/spelling/SolrSpellChecker.java
b/solr/core/src/java/org/apache/solr/spelling/SolrSpellChecker.java
index f5a9989d9da..da0542243f6 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SolrSpellChecker.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SolrSpellChecker.java
@@ -129,7 +129,8 @@ public abstract class SolrSpellChecker {
// create token
SpellCheckResponse.Suggestion suggestion =
mergeData.origVsSuggestion.get(original);
- Token token = new Token(original, suggestion.getStartOffset(),
suggestion.getEndOffset());
+ SpellCheckToken token =
+ new SpellCheckToken(original, suggestion.getStartOffset(),
suggestion.getEndOffset());
// get top 'count' suggestions out of 'sugQueue.size()' candidates
SuggestWord[] suggestions = new SuggestWord[Math.min(count,
sugQueue.size())];
diff --git
a/solr/core/src/java/org/apache/solr/spelling/SpellCheckCollator.java
b/solr/core/src/java/org/apache/solr/spelling/SpellCheckCollator.java
index 344088c83ca..9faaa87a2d0 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SpellCheckCollator.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SpellCheckCollator.java
@@ -211,10 +211,10 @@ public class SpellCheckCollator {
String corr = "";
for (int i = 0; i < corrections.size(); i++) {
SpellCheckCorrection correction = corrections.get(i);
- Token tok = correction.getOriginal();
+ SpellCheckToken tok = correction.getOriginal();
// we are replacing the query in order, but injected terms might cause
// illegal offsets due to previous replacements.
- if (tok.getPositionIncrement() == 0) continue;
+ if (tok.positionIncrement() == 0) continue;
corr = correction.getCorrection();
boolean addParenthesis = false;
Character requiredOrProhibited = null;
@@ -233,7 +233,7 @@ public class SpellCheckCollator {
requiredOrProhibited = previousChar;
}
bump++;
- } else if ((tok.getFlags() & QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG)
+ } else if ((tok.flags() & QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG)
== QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG) {
addParenthesis = true;
corrSb.insert(indexOfSpace + bump, "AND ");
diff --git
a/solr/core/src/java/org/apache/solr/spelling/SpellCheckCorrection.java
b/solr/core/src/java/org/apache/solr/spelling/SpellCheckCorrection.java
index c040aca9b66..b8d465b5cbc 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SpellCheckCorrection.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SpellCheckCorrection.java
@@ -17,12 +17,12 @@
package org.apache.solr.spelling;
public class SpellCheckCorrection {
- private Token original;
+ private SpellCheckToken original;
private String originalAsString = null;
private String correction;
private int numberOfOccurences;
- public Token getOriginal() {
+ public SpellCheckToken getOriginal() {
return original;
}
@@ -33,7 +33,7 @@ public class SpellCheckCorrection {
return originalAsString;
}
- public void setOriginal(Token original) {
+ public void setOriginal(SpellCheckToken original) {
this.original = original;
this.originalAsString = null;
}
diff --git a/solr/core/src/java/org/apache/solr/spelling/SpellCheckToken.java
b/solr/core/src/java/org/apache/solr/spelling/SpellCheckToken.java
new file mode 100644
index 00000000000..34f89a92768
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/spelling/SpellCheckToken.java
@@ -0,0 +1,119 @@
+/*
+ * 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.solr.spelling;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
+import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
+import org.apache.lucene.analysis.tokenattributes.PayloadAttribute;
+import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * One term occurrence carried through the spellchecker API, and the key type
of {@link
+ * SpellingResult}. Unlike the old {@code Token} it replaces, this is a plain,
immutable record --
+ * not a Lucene {@code AttributeImpl} subclass.
+ *
+ * <p>A value type is needed here, rather than reading terms off a stream,
because {@link
+ * SolrSpellChecker#mergeSuggestions} keys suggestions by (text, offset) pairs
deserialized from a
+ * remote shard's response, where there is no {@link
org.apache.lucene.analysis.TokenStream} to read
+ * from at all.
+ */
+public record SpellCheckToken(
+ String text,
+ int startOffset,
+ int endOffset,
+ String type,
+ int positionIncrement,
+ int flags,
+ BytesRef payload) {
+
+ public SpellCheckToken {
+ // PayloadAttribute hands out a buffer it refills for the next token, so a
reference to it is
+ // not a value. This record is a map key in SpellingResult and in
ConjunctionSolrSpellChecker,
+ // which need one whose bytes cannot change after it is stored.
+ payload = payload == null ? null : BytesRef.deepCopyOf(payload);
+ }
+
+ public SpellCheckToken(String text, int startOffset, int endOffset) {
+ this(text, startOffset, endOffset, "word", 1, 0, null);
+ }
+
+ /** The length of {@link #text()}, which the {@code Token} this replaced
also offered. */
+ public int length() {
+ return text.length();
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+
+ /**
+ * Reads {@code stream} to its end into a list, and closes it. This is the
single point where a
+ * {@link TokenStream} becomes values: every consumer of the spellcheck API
reads every token, so
+ * the stream's lifecycle need not reach any of them.
+ */
+ public static List<SpellCheckToken> drain(TokenStream stream) throws
IOException {
+ List<SpellCheckToken> tokens = new ArrayList<>();
+ try (stream) {
+ AttributeReader attrs = new AttributeReader(stream);
+ stream.reset();
+ while (stream.incrementToken()) {
+ tokens.add(attrs.current());
+ }
+ stream.end();
+ }
+ return tokens;
+ }
+
+ /** Registers the six attributes {@link #drain} reads, once for the whole
stream. */
+ private static final class AttributeReader {
+ private final CharTermAttribute termAtt;
+ private final OffsetAttribute offsetAtt;
+ private final TypeAttribute typeAtt;
+ private final PositionIncrementAttribute posIncAtt;
+ private final FlagsAttribute flagsAtt;
+ private final PayloadAttribute payloadAtt;
+
+ AttributeReader(TokenStream stream) {
+ termAtt = stream.addAttribute(CharTermAttribute.class);
+ offsetAtt = stream.addAttribute(OffsetAttribute.class);
+ typeAtt = stream.addAttribute(TypeAttribute.class);
+ posIncAtt = stream.addAttribute(PositionIncrementAttribute.class);
+ flagsAtt = stream.addAttribute(FlagsAttribute.class);
+ payloadAtt = stream.addAttribute(PayloadAttribute.class);
+ }
+
+ /** Builds a {@link SpellCheckToken} from the stream's current position. */
+ SpellCheckToken current() {
+ return new SpellCheckToken(
+ termAtt.toString(),
+ offsetAtt.startOffset(),
+ offsetAtt.endOffset(),
+ typeAtt.type(),
+ posIncAtt.getPositionIncrement(),
+ flagsAtt.getFlags(),
+ payloadAtt.getPayload());
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/spelling/SpellingOptions.java
b/solr/core/src/java/org/apache/solr/spelling/SpellingOptions.java
index 4975c1538ee..9f89c97a7f7 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SpellingOptions.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SpellingOptions.java
@@ -16,7 +16,7 @@
*/
package org.apache.solr.spelling;
-import java.util.Collection;
+import java.util.List;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.spell.SuggestMode;
import org.apache.solr.common.params.SolrParams;
@@ -24,8 +24,11 @@ import org.apache.solr.common.params.SolrParams;
/** */
public class SpellingOptions {
- /** The tokens to spell check */
- public Collection<Token> tokens;
+ /**
+ * The terms to spell check, analyzed once. Several {@link
SolrSpellChecker}s may read them --
+ * e.g. via {@link ConjunctionSolrSpellChecker} -- and each reads this same
list.
+ */
+ public List<SpellCheckToken> tokens;
/** An optional {@link org.apache.lucene.index.IndexReader} */
public IndexReader reader;
@@ -52,24 +55,24 @@ public class SpellingOptions {
public SpellingOptions() {}
// A couple of convenience ones
- public SpellingOptions(Collection<Token> tokens, int count) {
+ public SpellingOptions(List<SpellCheckToken> tokens, int count) {
this.tokens = tokens;
this.count = count;
}
- public SpellingOptions(Collection<Token> tokens, IndexReader reader) {
+ public SpellingOptions(List<SpellCheckToken> tokens, IndexReader reader) {
this.tokens = tokens;
this.reader = reader;
}
- public SpellingOptions(Collection<Token> tokens, IndexReader reader, int
count) {
+ public SpellingOptions(List<SpellCheckToken> tokens, IndexReader reader, int
count) {
this.tokens = tokens;
this.reader = reader;
this.count = count;
}
public SpellingOptions(
- Collection<Token> tokens,
+ List<SpellCheckToken> tokens,
IndexReader reader,
int count,
SuggestMode suggestMode,
@@ -86,7 +89,7 @@ public class SpellingOptions {
}
public SpellingOptions(
- Collection<Token> tokens,
+ List<SpellCheckToken> tokens,
IndexReader reader,
int count,
int alternativeTermCount,
diff --git
a/solr/core/src/java/org/apache/solr/spelling/SpellingQueryConverter.java
b/solr/core/src/java/org/apache/solr/spelling/SpellingQueryConverter.java
index 6870459a377..9fda927588e 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SpellingQueryConverter.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SpellingQueryConverter.java
@@ -16,31 +16,24 @@
*/
package org.apache.solr.spelling;
-import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.lucene.analysis.TokenStream;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
-import org.apache.lucene.analysis.tokenattributes.PayloadAttribute;
-import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
-import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
/**
- * Converts the query string to a Collection of Lucene tokens using a regular
expression. Boolean
- * operators AND, OR, NOT are skipped.
+ * Converts the query string to a TokenStream using a regular expression.
Boolean operators AND, OR,
+ * NOT are skipped.
*
* <p>Each term is checked to determine if it is optional, required or
prohibited. Required terms
- * output a {@link Token} with the {@link QueryConverter#REQUIRED_TERM_FLAG}
set. Prohibited terms
- * output a {@link Token} with the {@link QueryConverter#PROHIBITED_TERM_FLAG}
set. If the query
- * uses the plus (+) and minus (-) to denote required and prohibited, this
determination will be
- * accurate. In the case boolean AND/OR/NOTs are used, this converter makes an
uninformed guess as
- * to whether the term would likely behave as if it is Required or Prohibited
and sets the flags
- * accordingly. These flags are used downstream to generate collations for
{@link
- * WordBreakSolrSpellChecker}, in cases where an original term is split up
into multiple Tokens.
+ * output a token with the {@link QueryConverter#REQUIRED_TERM_FLAG} set.
Prohibited terms output a
+ * token with the {@link QueryConverter#PROHIBITED_TERM_FLAG} set. If the
query uses the plus (+)
+ * and minus (-) to denote required and prohibited, this determination will be
accurate. In the case
+ * boolean AND/OR/NOTs are used, this converter makes an uninformed guess as
to whether the term
+ * would likely behave as if it is Required or Prohibited and sets the flags
accordingly. These
+ * flags are used downstream to generate collations for {@link
WordBreakSolrSpellChecker}, in cases
+ * where an original term is split up into multiple tokens.
*
* @since solr 1.3
*/
@@ -95,20 +88,22 @@ public class SpellingQueryConverter extends QueryConverter {
protected Pattern QUERY_REGEX = Pattern.compile(PATTERN);
/**
- * Converts the original query string to a collection of Lucene Tokens.
+ * Parses the original query string into a {@link TokenStream}; each matched
query word is
+ * analyzed lazily as the stream is consumed.
*
* @param original the original query string
- * @return a Collection of Lucene Tokens
+ * @return a TokenStream over the query's terms, with {@code FlagsAttribute}
set per word from the
+ * query-syntax parse below
*/
@Override
- public Collection<Token> convert(String original) {
+ public TokenStream convert(String original) {
if (original == null) { // this can happen with q.alt = and no query
- return List.of();
+ return new QueryWordsTokenStream(List.of(), analyzer);
}
boolean mightContainRangeQuery =
(original.indexOf('[') != -1 || original.indexOf('{') != -1)
&& (original.indexOf(']') != -1 || original.indexOf('}') != -1);
- Collection<Token> result = new ArrayList<>();
+ List<QueryWordsTokenStream.ParsedWord> words = new ArrayList<>();
Matcher matcher = QUERY_REGEX.matcher(original);
String nextWord = null;
int nextStartIndex = 0;
@@ -161,42 +156,17 @@ public class SpellingQueryConverter extends
QueryConverter {
&& ("NOT".equals(nextWord))) {
flagValue = TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG;
}
- try {
- analyze(result, word, startIndex, flagValue);
- } catch (IOException e) {
- // TODO: shouldn't we log something?
- }
+ words.add(new QueryWordsTokenStream.ParsedWord(word, startIndex,
flagValue));
}
if (lastBooleanOp != null) {
- for (Token t : result) {
- int f = t.getFlags();
- t.setFlags(f |= QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG);
+ for (int i = 0; i < words.size(); i++) {
+ QueryWordsTokenStream.ParsedWord w = words.get(i);
+ words.set(
+ i,
+ new QueryWordsTokenStream.ParsedWord(
+ w.text(), w.startIndex(), w.flags() |
QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG));
}
}
- return result;
- }
-
- protected void analyze(Collection<Token> result, String text, int offset,
int flagsAttValue)
- throws IOException {
- TokenStream stream = analyzer.tokenStream("", text);
- // TODO: support custom attributes
- CharTermAttribute termAtt = stream.addAttribute(CharTermAttribute.class);
- TypeAttribute typeAtt = stream.addAttribute(TypeAttribute.class);
- PayloadAttribute payloadAtt = stream.addAttribute(PayloadAttribute.class);
- PositionIncrementAttribute posIncAtt =
stream.addAttribute(PositionIncrementAttribute.class);
- OffsetAttribute offsetAtt = stream.addAttribute(OffsetAttribute.class);
- stream.reset();
- while (stream.incrementToken()) {
- Token token = new Token();
- token.copyBuffer(termAtt.buffer(), 0, termAtt.length());
- token.setOffset(offset + offsetAtt.startOffset(), offset +
offsetAtt.endOffset());
- token.setFlags(flagsAttValue); // overwriting any flags already set...
- token.setType(typeAtt.type());
- token.setPayload(payloadAtt.getPayload());
- token.setPositionIncrement(posIncAtt.getPositionIncrement());
- result.add(token);
- }
- stream.end();
- stream.close();
+ return new QueryWordsTokenStream(words, analyzer);
}
}
diff --git a/solr/core/src/java/org/apache/solr/spelling/SpellingResult.java
b/solr/core/src/java/org/apache/solr/spelling/SpellingResult.java
index 8cf29aa148f..fdec078404a 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SpellingResult.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SpellingResult.java
@@ -16,7 +16,6 @@
*/
package org.apache.solr.spelling;
-import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -28,30 +27,25 @@ import java.util.Map;
* @since solr 1.3
*/
public class SpellingResult {
- private Collection<Token> tokens;
/**
* Key == token Value = Map -> key is the suggestion, value is the frequency
of the token in the
* collection
*/
- private Map<Token, LinkedHashMap<String, Integer>> suggestions = new
LinkedHashMap<>();
+ private Map<SpellCheckToken, LinkedHashMap<String, Integer>> suggestions =
new LinkedHashMap<>();
- private Map<Token, Integer> tokenFrequency;
+ private Map<SpellCheckToken, Integer> tokenFrequency;
public static final int NO_FREQUENCY_INFO = -1;
public SpellingResult() {}
- public SpellingResult(Collection<Token> tokens) {
- this.tokens = tokens;
- }
-
/**
* Adds a whole bunch of suggestions, and does not worry about frequency.
*
* @param token The token to associate the suggestions with
* @param suggestions The suggestions
*/
- public void add(Token token, List<String> suggestions) {
+ public void add(SpellCheckToken token, List<String> suggestions) {
LinkedHashMap<String, Integer> map = this.suggestions.get(token);
if (map == null) {
map = new LinkedHashMap<>();
@@ -68,7 +62,7 @@ public class SpellingResult {
* @param token original token
* @param docFreq original token's document frequency
*/
- public void addFrequency(Token token, int docFreq) {
+ public void addFrequency(SpellCheckToken token, int docFreq) {
if (tokenFrequency == null) {
tokenFrequency = new LinkedHashMap<>();
}
@@ -78,11 +72,11 @@ public class SpellingResult {
/**
* Suggestions must be added with the best suggestion first. ORDER is
important.
*
- * @param token The {@link Token}
- * @param suggestion The suggestion for the Token
+ * @param token The {@link SpellCheckToken}
+ * @param suggestion The suggestion for the token
* @param docFreq The document frequency
*/
- public void add(Token token, String suggestion, int docFreq) {
+ public void add(SpellCheckToken token, String suggestion, int docFreq) {
LinkedHashMap<String, Integer> map = this.suggestions.get(token);
// Don't bother adding if we already have this token
if (map == null) {
@@ -95,13 +89,13 @@ public class SpellingResult {
/**
* Gets the suggestions for the given token.
*
- * @param token The {@link Token} to look up
+ * @param token The {@link SpellCheckToken} to look up
* @return A LinkedHashMap of the suggestions. Key is the suggestion, value
is the token frequency
* in the index, else {@link #NO_FREQUENCY_INFO}.
* <p>The suggestions are added in sorted order (i.e. best suggestion
first) then the iterator
* will return the suggestions in order
*/
- public LinkedHashMap<String, Integer> get(Token token) {
+ public LinkedHashMap<String, Integer> get(SpellCheckToken token) {
return suggestions.get(token);
}
@@ -111,7 +105,7 @@ public class SpellingResult {
* @param token The token
* @return The frequency or null
*/
- public Integer getTokenFrequency(Token token) {
+ public Integer getTokenFrequency(SpellCheckToken token) {
return tokenFrequency.get(token);
}
@@ -122,26 +116,15 @@ public class SpellingResult {
/**
* All the suggestions. The ordering of the inner LinkedHashMap is by best
suggestion first.
*
- * @return The Map of suggestions for each Token. Key is the token, value is
a LinkedHashMap whose
+ * @return The Map of suggestions for each token. Key is the token, value is
a LinkedHashMap whose
* key is the Suggestion and the value is the frequency or {@link
#NO_FREQUENCY_INFO} if
* frequency info is not available.
*/
- public Map<Token, LinkedHashMap<String, Integer>> getSuggestions() {
+ public Map<SpellCheckToken, LinkedHashMap<String, Integer>> getSuggestions()
{
return suggestions;
}
- public Map<Token, Integer> getTokenFrequency() {
+ public Map<SpellCheckToken, Integer> getTokenFrequency() {
return tokenFrequency;
}
-
- /**
- * @return The original tokens
- */
- public Collection<Token> getTokens() {
- return tokens;
- }
-
- public void setTokens(Collection<Token> tokens) {
- this.tokens = tokens;
- }
}
diff --git
a/solr/core/src/java/org/apache/solr/spelling/SuggestQueryConverter.java
b/solr/core/src/java/org/apache/solr/spelling/SuggestQueryConverter.java
index da0bcbf3a55..84f4d5d7541 100644
--- a/solr/core/src/java/org/apache/solr/spelling/SuggestQueryConverter.java
+++ b/solr/core/src/java/org/apache/solr/spelling/SuggestQueryConverter.java
@@ -16,26 +16,18 @@
*/
package org.apache.solr.spelling;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
+import org.apache.lucene.analysis.TokenStream;
/** Passes the entire query string to the configured analyzer as-is. */
public class SuggestQueryConverter extends SpellingQueryConverter {
@Override
- public Collection<Token> convert(String original) {
+ public TokenStream convert(String original) {
if (original == null) { // this can happen with q.alt = and no query
- return List.of();
+ return new QueryWordsTokenStream(List.of(), analyzer);
}
-
- Collection<Token> result = new ArrayList<>();
- try {
- analyze(result, original, 0, 0);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- return result;
+ return new QueryWordsTokenStream(
+ List.of(new QueryWordsTokenStream.ParsedWord(original, 0, 0)),
analyzer);
}
}
diff --git a/solr/core/src/java/org/apache/solr/spelling/Token.java
b/solr/core/src/java/org/apache/solr/spelling/Token.java
deleted file mode 100644
index c163784a8bd..00000000000
--- a/solr/core/src/java/org/apache/solr/spelling/Token.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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.solr.spelling;
-
-import java.util.Objects;
-import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
-import org.apache.lucene.analysis.tokenattributes.PackedTokenAttributeImpl;
-import org.apache.lucene.analysis.tokenattributes.PayloadAttribute;
-import org.apache.lucene.util.AttributeImpl;
-import org.apache.lucene.util.AttributeReflector;
-import org.apache.lucene.util.BytesRef;
-
-/**
- * A Token is an occurrence of a term from the text of a field. It consists of
a term's text, the
- * start and end offset of the term in the text of the field, and a type
string.
- *
- * <p>The start and end offsets permit applications to re-associate a token
with its source text,
- * e.g., to display highlighted query terms in a document browser, or to show
matching text
- * fragments in a <a
href="http://en.wikipedia.org/wiki/Key_Word_in_Context">KWIC</a> display, etc.
- *
- * <p>The type is a string, assigned by a lexical analyzer (a.k.a. tokenizer),
naming the lexical or
- * syntactic class that the token belongs to. For example an end of sentence
marker token might be
- * implemented with type "eos". The default token type is "word".
- *
- * <p>A Token can optionally have metadata (a.k.a. payload) in the form of a
variable length byte
- * array. Use {@link org.apache.lucene.index.PostingsEnum#getPayload()} to
retrieve the payloads
- * from the index.
- *
- * <p>A few things to note:
- *
- * <ul>
- * <li>clear() initializes all of the fields to default values. This was
changed in contrast to
- * Lucene 2.4, but should affect no one.
- * <li>Because <code>TokenStreams</code> can be chained, one cannot assume
that the <code>Token's
- * </code> current type is correct.
- * <li>The startOffset and endOffset represent the start and offset in the
source text, so be
- * careful in adjusting them.
- * <li>When caching a reusable token, clone it. When injecting a cached
token into a stream that
- * can be reset, clone it again.
- * </ul>
- */
-@Deprecated(since = "7.0")
-public class Token extends PackedTokenAttributeImpl implements FlagsAttribute,
PayloadAttribute {
-
- // TODO Refactor the spellchecker API to use TokenStreams properly, rather
than this hack
-
- private int flags;
- private BytesRef payload;
-
- /** Constructs a Token will null text. */
- public Token() {}
-
- /**
- * Constructs a Token with the given term text, start and end offsets. The
type defaults to
- * "word." <b>NOTE:</b> for better indexing speed you should instead use the
char[] termBuffer
- * methods to set the term text.
- *
- * @param text term text
- * @param start start offset in the source text
- * @param end end offset in the source text
- */
- public Token(CharSequence text, int start, int end) {
- append(text);
- setOffset(start, end);
- }
-
- /**
- * {@inheritDoc}
- *
- * @see FlagsAttribute
- */
- @Override
- public int getFlags() {
- return flags;
- }
-
- /**
- * {@inheritDoc}
- *
- * @see FlagsAttribute
- */
- @Override
- public void setFlags(int flags) {
- this.flags = flags;
- }
-
- /**
- * {@inheritDoc}
- *
- * @see PayloadAttribute
- */
- @Override
- public BytesRef getPayload() {
- return this.payload;
- }
-
- /**
- * {@inheritDoc}
- *
- * @see PayloadAttribute
- */
- @Override
- public void setPayload(BytesRef payload) {
- this.payload = payload;
- }
-
- /**
- * Resets the term text, payload, flags, positionIncrement, positionLength,
startOffset, endOffset
- * and token type to default.
- */
- @Override
- public void clear() {
- super.clear();
- flags = 0;
- payload = null;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) return true;
- if (!(obj instanceof Token other)) return false;
-
- return (flags == other.flags && (Objects.equals(payload, other.payload))
&& super.equals(obj));
- }
-
- @Override
- public int hashCode() {
- int code = super.hashCode();
- code = code * 31 + flags;
- if (payload != null) {
- code = code * 31 + payload.hashCode();
- }
- return code;
- }
-
- @Override
- public Token clone() {
- final Token t = (Token) super.clone();
- if (payload != null) {
- t.payload = BytesRef.deepCopyOf(payload);
- }
- return t;
- }
-
- @Override
- public void copyTo(AttributeImpl target) {
- super.copyTo(target);
- ((FlagsAttribute) target).setFlags(flags);
- ((PayloadAttribute) target).setPayload((payload == null) ? null :
BytesRef.deepCopyOf(payload));
- }
-
- @Override
- public void reflectWith(AttributeReflector reflector) {
- super.reflectWith(reflector);
- reflector.reflect(FlagsAttribute.class, "flags", flags);
- reflector.reflect(PayloadAttribute.class, "payload", payload);
- }
-}
diff --git
a/solr/core/src/java/org/apache/solr/spelling/WordBreakSolrSpellChecker.java
b/solr/core/src/java/org/apache/solr/spelling/WordBreakSolrSpellChecker.java
index 1095acc2bd4..8f573054e74 100644
--- a/solr/core/src/java/org/apache/solr/spelling/WordBreakSolrSpellChecker.java
+++ b/solr/core/src/java/org/apache/solr/spelling/WordBreakSolrSpellChecker.java
@@ -158,9 +158,11 @@ public class WordBreakSolrSpellChecker extends
SolrSpellChecker {
int numSuggestions = options.count;
StringBuilder sb = new StringBuilder();
- Token[] tokenArr = options.tokens.toArray(new Token[0]);
- List<Token> tokenArrWithSeparators = new ArrayList<>(options.tokens.size()
+ 2);
- List<Term> termArr = new ArrayList<>(options.tokens.size() + 2);
+ // an array because, unlike the other SolrSpellCheckers, this one reads
several positions at
+ // once to combine adjacent terms into one corrected phrase
+ SpellCheckToken[] tokenArr = options.tokens.toArray(new
SpellCheckToken[0]);
+ List<SpellCheckToken> tokenArrWithSeparators = new
ArrayList<>(tokenArr.length + 2);
+ List<Term> termArr = new ArrayList<>(tokenArr.length + 2);
List<ResultEntry> breakSuggestionList = new ArrayList<>();
List<ResultEntry> noBreakSuggestionList = new ArrayList<>();
boolean lastOneProhibited = false;
@@ -168,13 +170,13 @@ public class WordBreakSolrSpellChecker extends
SolrSpellChecker {
boolean lastOneprocedesNewBooleanOp = false;
for (int i = 0; i < tokenArr.length; i++) {
boolean prohibited =
- (tokenArr[i].getFlags() & QueryConverter.PROHIBITED_TERM_FLAG)
+ (tokenArr[i].flags() & QueryConverter.PROHIBITED_TERM_FLAG)
== QueryConverter.PROHIBITED_TERM_FLAG;
boolean required =
- (tokenArr[i].getFlags() & QueryConverter.REQUIRED_TERM_FLAG)
+ (tokenArr[i].flags() & QueryConverter.REQUIRED_TERM_FLAG)
== QueryConverter.REQUIRED_TERM_FLAG;
boolean procedesNewBooleanOp =
- (tokenArr[i].getFlags() &
QueryConverter.TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG)
+ (tokenArr[i].flags() &
QueryConverter.TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG)
== QueryConverter.TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG;
if (i > 0
&& (prohibited != lastOneProhibited
@@ -234,8 +236,8 @@ public class WordBreakSolrSpellChecker extends
SolrSpellChecker {
}
sb.append(tokenArrWithSeparators.get(i).toString());
}
- Token token =
- new Token(
+ SpellCheckToken token =
+ new SpellCheckToken(
sb.toString(),
tokenArrWithSeparators.get(firstTermIndex).startOffset(),
tokenArrWithSeparators.get(lastTermIndex).endOffset());
@@ -316,7 +318,7 @@ public class WordBreakSolrSpellChecker extends
SolrSpellChecker {
private void addToResult(
SpellingResult result,
- Token token,
+ SpellCheckToken token,
int tokenFrequency,
String suggestion,
int suggestionFrequency) {
@@ -329,7 +331,7 @@ public class WordBreakSolrSpellChecker extends
SolrSpellChecker {
}
}
- private int getCombineFrequency(IndexReader ir, Token token) throws
IOException {
+ private int getCombineFrequency(IndexReader ir, SpellCheckToken token)
throws IOException {
String[] words = spacePattern.split(token.toString());
int result = 0;
if (sortMethod ==
BreakSuggestionSortMethod.NUM_CHANGES_THEN_MAX_FREQUENCY) {
diff --git a/solr/core/src/java/org/apache/solr/spelling/suggest/Suggester.java
b/solr/core/src/java/org/apache/solr/spelling/suggest/Suggester.java
index c2a5d374ba7..21ce05aed6b 100644
--- a/solr/core/src/java/org/apache/solr/spelling/suggest/Suggester.java
+++ b/solr/core/src/java/org/apache/solr/spelling/suggest/Suggester.java
@@ -35,15 +35,14 @@ import org.apache.lucene.search.suggest.Lookup;
import org.apache.lucene.search.suggest.Lookup.LookupResult;
import org.apache.lucene.search.suggest.analyzing.AnalyzingSuggester;
import org.apache.lucene.search.suggest.fst.WFSTCompletionLookup;
-import org.apache.lucene.util.CharsRef;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CloseHook;
import org.apache.solr.core.SolrCore;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.spelling.SolrSpellChecker;
+import org.apache.solr.spelling.SpellCheckToken;
import org.apache.solr.spelling.SpellingOptions;
import org.apache.solr.spelling.SpellingResult;
-import org.apache.solr.spelling.Token;
import org.apache.solr.spelling.suggest.fst.FSTLookupFactory;
import org.apache.solr.spelling.suggest.tst.TSTLookupFactory;
import org.slf4j.Logger;
@@ -195,22 +194,17 @@ public class Suggester extends SolrSpellChecker {
@Override
public SpellingResult getSuggestions(SpellingOptions options) throws
IOException {
- log.debug("getSuggestions: {}", options.tokens);
if (lookup == null) {
log.info("Lookup is null - invoke spellchecker.build first");
return EMPTY_RESULT;
}
SpellingResult res = new SpellingResult();
- CharsRef scratch = new CharsRef();
- for (Token t : options.tokens) {
- scratch.chars = t.buffer();
- scratch.offset = 0;
- scratch.length = t.length();
+ for (SpellCheckToken t : options.tokens) {
boolean onlyMorePopular =
(options.suggestMode == SuggestMode.SUGGEST_MORE_POPULAR)
&& !(lookup instanceof WFSTCompletionLookup)
&& !(lookup instanceof AnalyzingSuggester);
- List<LookupResult> suggestions = lookup.lookup(scratch, onlyMorePopular,
options.count);
+ List<LookupResult> suggestions = lookup.lookup(t.text(),
onlyMorePopular, options.count);
if (suggestions == null) {
continue;
}
diff --git
a/solr/core/src/test/org/apache/solr/spelling/DirectSolrSpellCheckerTest.java
b/solr/core/src/test/org/apache/solr/spelling/DirectSolrSpellCheckerTest.java
index 3a642df514f..0b17667ed01 100644
---
a/solr/core/src/test/org/apache/solr/spelling/DirectSolrSpellCheckerTest.java
+++
b/solr/core/src/test/org/apache/solr/spelling/DirectSolrSpellCheckerTest.java
@@ -16,9 +16,10 @@
*/
package org.apache.solr.spelling;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.core.KeywordAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressTempFileChecks;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.SpellingParams;
@@ -51,6 +52,11 @@ public class DirectSolrSpellCheckerTest extends
SolrTestCaseJ4 {
queryConverter.init(new NamedList<>());
}
+ /** A stream that emits exactly one token whose term text is empty. */
+ private static TokenStream singleEmptyTermTokenStream() {
+ return new KeywordAnalyzer().tokenStream("", "");
+ }
+
@Test
public void test() throws Exception {
DirectSolrSpellChecker checker = new DirectSolrSpellChecker();
@@ -67,11 +73,11 @@ public class DirectSolrSpellCheckerTest extends
SolrTestCaseJ4 {
searcher -> {
// check that 'fob' is corrected to 'foo'
- Collection<Token> tokens = queryConverter.convert("fob");
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(queryConverter.convert("fob"));
SpellingOptions spellOpts = new SpellingOptions(tokens,
searcher.getIndexReader());
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertFalse("suggestions shouldn't be empty",
suggestions.isEmpty());
Map.Entry<String, Integer> entry =
suggestions.entrySet().iterator().next();
assertEquals("foo", entry.getKey());
@@ -81,18 +87,18 @@ public class DirectSolrSpellCheckerTest extends
SolrTestCaseJ4 {
(int) entry.getValue());
// check that 'super' is *not* corrected
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNotNull("suggestions shouldn't be null", suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
// Check empty token due to spellcheck.q = ""
- spellOpts.tokens = List.of(new Token("", 0, 0));
+ spellOpts.tokens =
SpellCheckToken.drain(singleEmptyTermTokenStream());
result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(new SpellCheckToken("", 0, 0));
assertNotNull("suggestions shouldn't be null", suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
return null;
@@ -143,11 +149,12 @@ public class DirectSolrSpellCheckerTest extends
SolrTestCaseJ4 {
h.getCore()
.withSearcher(
searcher -> {
- Collection<Token> tokens = queryConverter.convert("anothar");
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(queryConverter.convert("anothar"));
SpellingOptions spellOpts = new SpellingOptions(tokens,
searcher.getIndexReader());
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertNotNull("suggestions shouldn't be null", suggestions);
if (limitQueryLength) {
diff --git
a/solr/core/src/test/org/apache/solr/spelling/FileBasedSpellCheckerTest.java
b/solr/core/src/test/org/apache/solr/spelling/FileBasedSpellCheckerTest.java
index 5065e0c398a..5c90814ff9a 100644
--- a/solr/core/src/test/org/apache/solr/spelling/FileBasedSpellCheckerTest.java
+++ b/solr/core/src/test/org/apache/solr/spelling/FileBasedSpellCheckerTest.java
@@ -18,9 +18,10 @@ package org.apache.solr.spelling;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.core.KeywordAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressTempFileChecks;
import org.apache.solr.SolrTestCaseJ4;
@@ -59,6 +60,11 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
queryConverter = null;
}
+ /** A stream that emits exactly one token whose term text is empty. */
+ private static TokenStream singleEmptyTermTokenStream() {
+ return new KeywordAnalyzer().tokenStream("", "");
+ }
+
@Test
public void test() throws Exception {
FileBasedSpellChecker checker = new FileBasedSpellChecker();
@@ -79,11 +85,11 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
h.getCore()
.withSearcher(
searcher -> {
- Collection<Token> tokens = queryConverter.convert("fob");
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(queryConverter.convert("fob"));
SpellingOptions spellOpts = new SpellingOptions(tokens,
searcher.getIndexReader());
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
Map.Entry<String, Integer> entry =
suggestions.entrySet().iterator().next();
assertEquals(entry.getKey() + " is not equal to " + "foo",
"foo", entry.getKey());
assertEquals(
@@ -91,18 +97,18 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
SpellingResult.NO_FREQUENCY_INFO,
(int) entry.getValue());
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNotNull("suggestions shouldn't be null", suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
// Check empty token due to spellcheck.q = ""
- spellOpts.tokens = List.of(new Token("", 0, 0));
+ spellOpts.tokens =
SpellCheckToken.drain(singleEmptyTermTokenStream());
result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(new SpellCheckToken("", 0, 0));
assertNotNull("suggestions shouldn't be null", suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
return null;
@@ -127,7 +133,7 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
assertEquals(dictName + " is not equal to " + "external", "external",
dictName);
checker.build(core, null);
- Collection<Token> tokens = queryConverter.convert("Solar");
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(queryConverter.convert("Solar"));
h.getCore()
.withSearcher(
searcher -> {
@@ -135,7 +141,7 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
// should be lowercased, b/c we are using a lowercasing analyzer
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertEquals(
"suggestions Size: " + suggestions.size() + " is not: " + 1,
1,
@@ -148,10 +154,10 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
(int) entry.getValue());
// test something not in the spell checker
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNotNull("suggestions shouldn't be null", suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
return null;
@@ -180,12 +186,12 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
h.getCore()
.withSearcher(
searcher -> {
- Collection<Token> tokens = queryConverter.convert("solar");
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(queryConverter.convert("solar"));
SpellingOptions spellOpts = new SpellingOptions(tokens,
searcher.getIndexReader());
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
// should be lowercased, b/c we are using a lowercasing analyzer
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertEquals(
"suggestions Size: " + suggestions.size() + " is not: " + 1,
1,
@@ -197,10 +203,10 @@ public class FileBasedSpellCheckerTest extends
SolrTestCaseJ4 {
SpellingResult.NO_FREQUENCY_INFO,
(int) entry.getValue());
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result shouldn't be null", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNotNull("suggestions shouldn't be null", suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
return null;
diff --git
a/solr/core/src/test/org/apache/solr/spelling/IndexBasedSpellCheckerTest.java
b/solr/core/src/test/org/apache/solr/spelling/IndexBasedSpellCheckerTest.java
index 6ee537f814d..71bcf056be7 100644
---
a/solr/core/src/test/org/apache/solr/spelling/IndexBasedSpellCheckerTest.java
+++
b/solr/core/src/test/org/apache/solr/spelling/IndexBasedSpellCheckerTest.java
@@ -18,11 +18,12 @@ package org.apache.solr.spelling;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.core.KeywordAnalyzer;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
@@ -126,12 +127,13 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
checker.build(core, searcher);
IndexReader reader = searcher.getIndexReader();
- Collection<Token> tokens = queryConverter.convert("documemt");
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(queryConverter.convert("documemt"));
SpellingOptions spellOpts = new SpellingOptions(tokens, reader);
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
// should be lowercased, b/c we are using a lowercasing analyzer
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertNotNull("documemt is null and it shouldn't be",
suggestions);
assertEquals(
"documemt Size: " + suggestions.size() + " is not: " + 1, 1,
suggestions.size());
@@ -144,32 +146,32 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
(int) entry.getValue());
// test something not in the spell checker
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertEquals("suggestions size should be 0", 0,
suggestions.size());
// test something that is spelled correctly
- spellOpts.tokens = queryConverter.convert("document");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("document"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNull("suggestions is null and it shouldn't be",
suggestions);
// Has multiple possibilities, but the exact exists, so that
should be returned
- spellOpts.tokens = queryConverter.convert("red");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("red"));
spellOpts.count = 2;
result = checker.getSuggestions(spellOpts);
assertNotNull(result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNull("suggestions is not null and it should be",
suggestions);
// Try out something which should have multiple suggestions
- spellOpts.tokens = queryConverter.convert("bug");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("bug"));
result = checker.getSuggestions(spellOpts);
assertNotNull(result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNotNull(suggestions);
assertEquals(
"suggestions Size: " + suggestions.size() + " is not: " + 2,
@@ -197,16 +199,21 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
(int) entry.getValue());
// Check empty token due to spellcheck.q = ""
- spellOpts.tokens = List.of(new Token("", 0, 0));
+ spellOpts.tokens =
SpellCheckToken.drain(singleEmptyTermTokenStream());
result = checker.getSuggestions(spellOpts);
assertNotNull(result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(new SpellCheckToken("", 0, 0));
assertNotNull(suggestions);
assertTrue("suggestions should be empty", suggestions.isEmpty());
return null;
});
}
+ /** A stream that emits exactly one token whose term text is empty. */
+ private static TokenStream singleEmptyTermTokenStream() {
+ return new KeywordAnalyzer().tokenStream("", "");
+ }
+
@Test
public void testExtendedResults() throws Exception {
IndexBasedSpellChecker checker = new IndexBasedSpellChecker();
@@ -230,14 +237,15 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
checker.build(core, searcher);
IndexReader reader = searcher.getIndexReader();
- Collection<Token> tokens = queryConverter.convert("documemt");
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(queryConverter.convert("documemt"));
SpellingOptions spellOpts =
new SpellingOptions(
tokens, reader, 1,
SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX, true, 0.5f, null);
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
// should be lowercased, b/c we are using a lowercasing analyzer
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertNotNull("documemt is null and it shouldn't be",
suggestions);
assertEquals(
"documemt Size: " + suggestions.size() + " is not: " + 1, 1,
suggestions.size());
@@ -247,16 +255,16 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
assertEquals(entry.getValue() + " does not equal: " + 2, 2,
(int) entry.getValue());
// test something not in the spell checker
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertEquals("suggestions size should be 0", 0,
suggestions.size());
- spellOpts.tokens = queryConverter.convert("document");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("document"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNull("suggestions is not null and it should be",
suggestions);
return null;
});
@@ -350,14 +358,14 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
checker.build(core, searcher);
IndexReader reader = searcher.getIndexReader();
- Collection<Token> tokens = queryConverter.convert("flesh");
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(queryConverter.convert("flesh"));
SpellingOptions spellOpts =
new SpellingOptions(
tokens, reader, 1,
SuggestMode.SUGGEST_WHEN_NOT_IN_INDEX, true, 0.5f, null);
SpellingResult result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
// should be lowercased, b/c we are using a lowercasing analyzer
- Map<String, Integer> suggestions =
result.get(spellOpts.tokens.iterator().next());
+ Map<String, Integer> suggestions =
result.get(spellOpts.tokens.get(0));
assertNotNull("flesh is null and it shouldn't be", suggestions);
assertEquals(
"flesh Size: " + suggestions.size() + " is not: " + 1, 1,
suggestions.size());
@@ -366,16 +374,16 @@ public class IndexBasedSpellCheckerTest extends
SolrTestCaseJ4 {
assertEquals(entry.getValue() + " does not equal: " + 1, 1,
(int) entry.getValue());
// test something not in the spell checker
- spellOpts.tokens = queryConverter.convert("super");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("super"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertEquals("suggestions size should be 0", 0,
suggestions.size());
- spellOpts.tokens = queryConverter.convert("Caroline");
+ spellOpts.tokens =
SpellCheckToken.drain(queryConverter.convert("Caroline"));
result = checker.getSuggestions(spellOpts);
assertNotNull("result is null and it shouldn't be", result);
- suggestions = result.get(spellOpts.tokens.iterator().next());
+ suggestions = result.get(spellOpts.tokens.get(0));
assertNull("suggestions is not null and it should be",
suggestions);
return null;
});
diff --git
a/solr/core/src/test/org/apache/solr/spelling/SimpleQueryConverter.java
b/solr/core/src/test/org/apache/solr/spelling/SimpleQueryConverter.java
index 9cda54676b1..3342f6a5082 100644
--- a/solr/core/src/test/org/apache/solr/spelling/SimpleQueryConverter.java
+++ b/solr/core/src/test/org/apache/solr/spelling/SimpleQueryConverter.java
@@ -16,53 +16,18 @@
*/
package org.apache.solr.spelling;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashSet;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.analysis.tokenattributes.FlagsAttribute;
-import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
-import org.apache.lucene.analysis.tokenattributes.PayloadAttribute;
-import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
-import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
/**
* @since solr 1.3
*/
class SimpleQueryConverter extends SpellingQueryConverter {
- @Override
- public Collection<Token> convert(String origQuery) {
- Collection<Token> result = new HashSet<>();
-
- try (WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer();
- TokenStream ts = analyzer.tokenStream("", origQuery)) {
- // TODO: support custom attributes
- CharTermAttribute termAtt = ts.addAttribute(CharTermAttribute.class);
- OffsetAttribute offsetAtt = ts.addAttribute(OffsetAttribute.class);
- TypeAttribute typeAtt = ts.addAttribute(TypeAttribute.class);
- FlagsAttribute flagsAtt = ts.addAttribute(FlagsAttribute.class);
- PayloadAttribute payloadAtt = ts.addAttribute(PayloadAttribute.class);
- PositionIncrementAttribute posIncAtt =
ts.addAttribute(PositionIncrementAttribute.class);
+ private static final WhitespaceAnalyzer ANALYZER = new WhitespaceAnalyzer();
- ts.reset();
-
- while (ts.incrementToken()) {
- Token tok = new Token();
- tok.copyBuffer(termAtt.buffer(), 0, termAtt.length());
- tok.setOffset(offsetAtt.startOffset(), offsetAtt.endOffset());
- tok.setFlags(flagsAtt.getFlags());
- tok.setPayload(payloadAtt.getPayload());
- tok.setPositionIncrement(posIncAtt.getPositionIncrement());
- tok.setType(typeAtt.type());
- result.add(tok);
- }
- ts.end();
- return result;
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ @Override
+ public TokenStream convert(String origQuery) {
+ return ANALYZER.tokenStream("", origQuery);
}
}
diff --git
a/solr/core/src/test/org/apache/solr/spelling/SpellPossibilityIteratorTest.java
b/solr/core/src/test/org/apache/solr/spelling/SpellPossibilityIteratorTest.java
index d402044b4ce..365744601c1 100644
---
a/solr/core/src/test/org/apache/solr/spelling/SpellPossibilityIteratorTest.java
+++
b/solr/core/src/test/org/apache/solr/spelling/SpellPossibilityIteratorTest.java
@@ -25,10 +25,10 @@ import org.junit.Before;
import org.junit.Test;
public class SpellPossibilityIteratorTest extends SolrTestCaseJ4 {
- private static final Token TOKEN_AYE = new Token("AYE", 0, 3);
- private static final Token TOKEN_BEE = new Token("BEE", 4, 7);
- private static final Token TOKEN_AYE_BEE = new Token("AYE BEE", 0, 7);
- private static final Token TOKEN_CEE = new Token("CEE", 8, 11);
+ private static final SpellCheckToken TOKEN_AYE = new SpellCheckToken("AYE",
0, 3);
+ private static final SpellCheckToken TOKEN_BEE = new SpellCheckToken("BEE",
4, 7);
+ private static final SpellCheckToken TOKEN_AYE_BEE = new
SpellCheckToken("AYE BEE", 0, 7);
+ private static final SpellCheckToken TOKEN_CEE = new SpellCheckToken("CEE",
8, 11);
private LinkedHashMap<String, Integer> AYE;
private LinkedHashMap<String, Integer> BEE;
@@ -87,26 +87,26 @@ public class SpellPossibilityIteratorTest extends
SolrTestCaseJ4 {
@Test
public void testScalability() {
- Map<Token, LinkedHashMap<String, Integer>> lotsaSuggestions = new
LinkedHashMap<>();
+ Map<SpellCheckToken, LinkedHashMap<String, Integer>> lotsaSuggestions =
new LinkedHashMap<>();
lotsaSuggestions.put(TOKEN_AYE, AYE);
lotsaSuggestions.put(TOKEN_BEE, BEE);
lotsaSuggestions.put(TOKEN_CEE, CEE);
- lotsaSuggestions.put(new Token("AYE1", 0, 3), AYE);
- lotsaSuggestions.put(new Token("BEE1", 4, 7), BEE);
- lotsaSuggestions.put(new Token("CEE1", 8, 11), CEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE1", 0, 3), AYE);
+ lotsaSuggestions.put(new SpellCheckToken("BEE1", 4, 7), BEE);
+ lotsaSuggestions.put(new SpellCheckToken("CEE1", 8, 11), CEE);
- lotsaSuggestions.put(new Token("AYE2", 0, 3), AYE);
- lotsaSuggestions.put(new Token("BEE2", 4, 7), BEE);
- lotsaSuggestions.put(new Token("CEE2", 8, 11), CEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE2", 0, 3), AYE);
+ lotsaSuggestions.put(new SpellCheckToken("BEE2", 4, 7), BEE);
+ lotsaSuggestions.put(new SpellCheckToken("CEE2", 8, 11), CEE);
- lotsaSuggestions.put(new Token("AYE3", 0, 3), AYE);
- lotsaSuggestions.put(new Token("BEE3", 4, 7), BEE);
- lotsaSuggestions.put(new Token("CEE3", 8, 11), CEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE3", 0, 3), AYE);
+ lotsaSuggestions.put(new SpellCheckToken("BEE3", 4, 7), BEE);
+ lotsaSuggestions.put(new SpellCheckToken("CEE3", 8, 11), CEE);
- lotsaSuggestions.put(new Token("AYE4", 0, 3), AYE);
- lotsaSuggestions.put(new Token("BEE4", 4, 7), BEE);
- lotsaSuggestions.put(new Token("CEE4", 8, 11), CEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE4", 0, 3), AYE);
+ lotsaSuggestions.put(new SpellCheckToken("BEE4", 4, 7), BEE);
+ lotsaSuggestions.put(new SpellCheckToken("CEE4", 8, 11), CEE);
PossibilityIterator iter = new PossibilityIterator(lotsaSuggestions, 1000,
10000, false);
int count = 0;
@@ -116,10 +116,10 @@ public class SpellPossibilityIteratorTest extends
SolrTestCaseJ4 {
}
assertEquals(1000, count);
- lotsaSuggestions.put(new Token("AYE_BEE1", 0, 7), AYE_BEE);
- lotsaSuggestions.put(new Token("AYE_BEE2", 0, 7), AYE_BEE);
- lotsaSuggestions.put(new Token("AYE_BEE3", 0, 7), AYE_BEE);
- lotsaSuggestions.put(new Token("AYE_BEE4", 0, 7), AYE_BEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE_BEE1", 0, 7), AYE_BEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE_BEE2", 0, 7), AYE_BEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE_BEE3", 0, 7), AYE_BEE);
+ lotsaSuggestions.put(new SpellCheckToken("AYE_BEE4", 0, 7), AYE_BEE);
iter = new PossibilityIterator(lotsaSuggestions, 1000, 10000, true);
count = 0;
while (iter.hasNext()) {
@@ -131,7 +131,7 @@ public class SpellPossibilityIteratorTest extends
SolrTestCaseJ4 {
@Test
public void testSpellPossibilityIterator() {
- Map<Token, LinkedHashMap<String, Integer>> suggestions = new
LinkedHashMap<>();
+ Map<SpellCheckToken, LinkedHashMap<String, Integer>> suggestions = new
LinkedHashMap<>();
suggestions.put(TOKEN_AYE, AYE);
suggestions.put(TOKEN_BEE, BEE);
suggestions.put(TOKEN_CEE, CEE);
@@ -184,7 +184,8 @@ public class SpellPossibilityIteratorTest extends
SolrTestCaseJ4 {
@Test
public void testOverlappingTokens() {
- Map<Token, LinkedHashMap<String, Integer>> overlappingSuggestions = new
LinkedHashMap<>();
+ Map<SpellCheckToken, LinkedHashMap<String, Integer>>
overlappingSuggestions =
+ new LinkedHashMap<>();
overlappingSuggestions.put(TOKEN_AYE, AYE);
overlappingSuggestions.put(TOKEN_BEE, BEE);
overlappingSuggestions.put(TOKEN_AYE_BEE, AYE_BEE);
@@ -197,10 +198,10 @@ public class SpellPossibilityIteratorTest extends
SolrTestCaseJ4 {
Set<PossibilityIterator.RankedSpellPossibility> dupChecker = new
HashSet<>();
while (iter.hasNext()) {
PossibilityIterator.RankedSpellPossibility rsp = iter.next();
- Token a = null;
- Token b = null;
- Token ab = null;
- Token c = null;
+ SpellCheckToken a = null;
+ SpellCheckToken b = null;
+ SpellCheckToken ab = null;
+ SpellCheckToken c = null;
for (SpellCheckCorrection scc : rsp.corrections) {
if (scc.getOriginal().equals(TOKEN_AYE)) {
a = scc.getOriginal();
diff --git
a/solr/core/src/test/org/apache/solr/spelling/SpellingQueryConverterTest.java
b/solr/core/src/test/org/apache/solr/spelling/SpellingQueryConverterTest.java
index 42172c89e45..d1f16a0d38f 100644
---
a/solr/core/src/test/org/apache/solr/spelling/SpellingQueryConverterTest.java
+++
b/solr/core/src/test/org/apache/solr/spelling/SpellingQueryConverterTest.java
@@ -16,10 +16,15 @@
*/
package org.apache.solr.spelling;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.io.IOException;
import java.util.List;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
+import org.apache.lucene.analysis.core.WhitespaceTokenizer;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.solr.SolrTestCase;
import org.apache.solr.common.util.NamedList;
import org.junit.Test;
@@ -32,17 +37,17 @@ import org.junit.Test;
public class SpellingQueryConverterTest extends SolrTestCase {
@Test
- public void test() {
+ public void test() throws IOException {
SpellingQueryConverter converter = new SpellingQueryConverter();
converter.init(new NamedList<>());
converter.setAnalyzer(new WhitespaceAnalyzer());
- Collection<Token> tokens = converter.convert("field:foo");
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert("field:foo"));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not: " + 1, 1,
tokens.size());
}
@Test
- public void testNumeric() {
+ public void testNumeric() throws IOException {
SpellingQueryConverter converter = new SpellingQueryConverter();
converter.init(new NamedList<>());
converter.setAnalyzer(new WhitespaceAnalyzer());
@@ -58,7 +63,7 @@ public class SpellingQueryConverterTest extends SolrTestCase {
};
int[] tokensToExpect = {1, 1, 2, 2, 2, 2, 2, 2};
for (int i = 0; i < queries.length; i++) {
- Collection<Token> tokens = converter.convert(queries[i]);
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert(queries[i]));
assertEquals(
"tokens Size: " + tokens.size() + " is not: " + tokensToExpect[i],
tokens.size(),
@@ -67,24 +72,24 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
}
@Test
- public void testSpecialChars() {
+ public void testSpecialChars() throws IOException {
SpellingQueryConverter converter = new SpellingQueryConverter();
converter.init(new NamedList<>());
converter.setAnalyzer(new WhitespaceAnalyzer());
String original = "field_with_underscore:value_with_underscore";
- Collection<Token> tokens = converter.convert(original);
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert(original));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
assertTrue("Token offsets do not match", isOffsetCorrect(original,
tokens));
original = "field_with_digits123:value_with_digits123";
- tokens = converter.convert(original);
+ tokens = SpellCheckToken.drain(converter.convert(original));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
assertTrue("Token offsets do not match", isOffsetCorrect(original,
tokens));
original = "field-with-hyphens:value-with-hyphens";
- tokens = converter.convert(original);
+ tokens = SpellCheckToken.drain(converter.convert(original));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
assertTrue("Token offsets do not match", isOffsetCorrect(original,
tokens));
@@ -97,7 +102,7 @@ public class SpellingQueryConverterTest extends SolrTestCase
{
// assertTrue("Token offsets do not match", isOffsetCorrect(original,
tokens));
original = "foo:bar^5.0";
- tokens = converter.convert(original);
+ tokens = SpellCheckToken.drain(converter.convert(original));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
assertTrue("Token offsets do not match", isOffsetCorrect(original,
tokens));
@@ -105,22 +110,16 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
String firstKeyword = "value1";
String secondKeyword = "value2";
original = "field-with-parenthesis:(" + firstKeyword + " " + secondKeyword
+ ")";
- tokens = converter.convert(original);
+ tokens = SpellCheckToken.drain(converter.convert(original));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 2", 2,
tokens.size());
assertTrue("Token offsets do not match", isOffsetCorrect(original,
tokens));
- assertEquals(
- "first Token is not " + firstKeyword,
- new ArrayList<>(tokens).get(0).toString(),
- firstKeyword);
- assertEquals(
- "second Token is not " + secondKeyword,
- new ArrayList<>(tokens).get(1).toString(),
- secondKeyword);
+ assertEquals("first Token is not " + firstKeyword,
tokens.get(0).toString(), firstKeyword);
+ assertEquals("second Token is not " + secondKeyword,
tokens.get(1).toString(), secondKeyword);
}
- private boolean isOffsetCorrect(String s, Collection<Token> tokens) {
- for (Token token : tokens) {
+ private boolean isOffsetCorrect(String s, List<SpellCheckToken> tokens) {
+ for (SpellCheckToken token : tokens) {
int start = token.startOffset();
int end = token.endOffset();
if (!s.substring(start, end).equals(token.toString())) return false;
@@ -129,50 +128,52 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
}
@Test
- public void testUnicode() {
+ public void testUnicode() throws IOException {
SpellingQueryConverter converter = new SpellingQueryConverter();
converter.init(new NamedList<>());
converter.setAnalyzer(new WhitespaceAnalyzer());
// chinese text value
- Collection<Token> tokens = converter.convert("text_field:我购买了道具和服装。");
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(converter.convert("text_field:我购买了道具和服装。"));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
- tokens = converter.convert("text_购field:我购买了道具和服装。");
+ tokens =
SpellCheckToken.drain(converter.convert("text_购field:我购买了道具和服装。"));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
- tokens = converter.convert("text_field:我购xyz买了道具和服装。");
+ tokens =
SpellCheckToken.drain(converter.convert("text_field:我购xyz买了道具和服装。"));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 1", 1,
tokens.size());
}
@Test
- public void testMultipleClauses() {
+ public void testMultipleClauses() throws IOException {
SpellingQueryConverter converter = new SpellingQueryConverter();
converter.init(new NamedList<>());
converter.setAnalyzer(new WhitespaceAnalyzer());
// two field:value pairs should give two tokens
- Collection<Token> tokens = converter.convert("买text_field:我购买了道具和服装。
field2:bar");
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(converter.convert("买text_field:我购买了道具和服装。
field2:bar"));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 2", 2,
tokens.size());
// a field:value pair and a search term should give two tokens
- tokens = converter.convert("text_field:我购买了道具和服装。 bar");
+ tokens = SpellCheckToken.drain(converter.convert("text_field:我购买了道具和服装。
bar"));
assertNotNull("tokens is null and it shouldn't be", tokens);
assertEquals("tokens Size: " + tokens.size() + " is not 2", 2,
tokens.size());
}
@Test
- public void testRequiredOrProhibitedFlags() {
+ public void testRequiredOrProhibitedFlags() throws IOException {
SpellingQueryConverter converter = new SpellingQueryConverter();
converter.init(new NamedList<>());
converter.setAnalyzer(new WhitespaceAnalyzer());
{
- List<Token> tokens = new ArrayList<>(converter.convert("aaa bbb ccc"));
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert("aaa bbb ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 should be optional",
@@ -185,7 +186,7 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
!hasRequiredFlag(tokens.get(2)) &&
!hasProhibitedFlag(tokens.get(2)));
}
{
- List<Token> tokens = new ArrayList<>(converter.convert("+aaa bbb -ccc"));
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert("+aaa bbb -ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 should be required",
@@ -198,7 +199,7 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
!hasRequiredFlag(tokens.get(2)) && hasProhibitedFlag(tokens.get(2)));
}
{
- List<Token> tokens = new ArrayList<>(converter.convert("aaa AND bbb
ccc"));
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert("aaa AND bbb ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 doesn't precede n.b.o.",
@@ -211,7 +212,7 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
!hasNBOFlag(tokens.get(2)) && hasInBooleanFlag(tokens.get(0)));
}
{
- List<Token> tokens = new ArrayList<>(converter.convert("aaa OR bbb OR
ccc"));
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert("aaa OR bbb OR ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 doesn't precede n.b.o.",
@@ -224,7 +225,8 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
!hasNBOFlag(tokens.get(2)) && hasInBooleanFlag(tokens.get(0)));
}
{
- List<Token> tokens = new ArrayList<>(converter.convert("aaa AND bbb NOT
ccc"));
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(converter.convert("aaa AND bbb NOT ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 doesn't precede n.b.o.",
@@ -236,7 +238,8 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
!hasNBOFlag(tokens.get(2)) && hasInBooleanFlag(tokens.get(0)));
}
{
- List<Token> tokens = new ArrayList<>(converter.convert("aaa NOT bbb AND
ccc"));
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(converter.convert("aaa NOT bbb AND ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 precedes n.b.o.", hasNBOFlag(tokens.get(0)) &&
hasInBooleanFlag(tokens.get(0)));
@@ -247,7 +250,8 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
!hasNBOFlag(tokens.get(2)) && hasInBooleanFlag(tokens.get(0)));
}
{
- List<Token> tokens = new ArrayList<>(converter.convert("aaa AND NOT bbb
AND ccc"));
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(converter.convert("aaa AND NOT bbb AND ccc"));
assertTrue("Should have 3 tokens", tokens != null && tokens.size() == 3);
assertTrue(
"token 1 precedes n.b.o.", hasNBOFlag(tokens.get(0)) &&
hasInBooleanFlag(tokens.get(0)));
@@ -259,22 +263,64 @@ public class SpellingQueryConverterTest extends
SolrTestCase {
}
}
- private boolean hasRequiredFlag(Token t) {
- return (t.getFlags() & QueryConverter.REQUIRED_TERM_FLAG) ==
QueryConverter.REQUIRED_TERM_FLAG;
+ /**
+ * A query word whose analysis throws is skipped, and the query's other
words are still converted.
+ */
+ @Test
+ public void testWordFailingAnalysisIsSkipped() throws IOException {
+ SpellingQueryConverter converter = new SpellingQueryConverter();
+ converter.init(new NamedList<>());
+ converter.setAnalyzer(
+ new Analyzer() {
+ @Override
+ protected TokenStreamComponents createComponents(String fieldName) {
+ Tokenizer source = new WhitespaceTokenizer();
+ return new TokenStreamComponents(source, new
FailOnTermFilter(source, "bbb"));
+ }
+ });
+
+ List<SpellCheckToken> tokens =
SpellCheckToken.drain(converter.convert("aaa bbb ccc"));
+
+ assertEquals(List.of("aaa", "ccc"),
tokens.stream().map(SpellCheckToken::text).toList());
+ }
+
+ /** Fails on one term, the way a filter reading an external dictionary would
on a bad read. */
+ private static class FailOnTermFilter extends TokenFilter {
+ private final CharTermAttribute termAtt =
addAttribute(CharTermAttribute.class);
+ private final String failOn;
+
+ FailOnTermFilter(TokenStream input, String failOn) {
+ super(input);
+ this.failOn = failOn;
+ }
+
+ @Override
+ public boolean incrementToken() throws IOException {
+ if (!input.incrementToken()) {
+ return false;
+ }
+ if (failOn.contentEquals(termAtt)) {
+ throw new IOException("analysis of '" + failOn + "' failed");
+ }
+ return true;
+ }
+ }
+
+ private boolean hasRequiredFlag(SpellCheckToken t) {
+ return (t.flags() & QueryConverter.REQUIRED_TERM_FLAG) ==
QueryConverter.REQUIRED_TERM_FLAG;
}
- private boolean hasProhibitedFlag(Token t) {
- return (t.getFlags() & QueryConverter.PROHIBITED_TERM_FLAG)
- == QueryConverter.PROHIBITED_TERM_FLAG;
+ private boolean hasProhibitedFlag(SpellCheckToken t) {
+ return (t.flags() & QueryConverter.PROHIBITED_TERM_FLAG) ==
QueryConverter.PROHIBITED_TERM_FLAG;
}
- private boolean hasNBOFlag(Token t) {
- return (t.getFlags() &
QueryConverter.TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG)
+ private boolean hasNBOFlag(SpellCheckToken t) {
+ return (t.flags() & QueryConverter.TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG)
== QueryConverter.TERM_PRECEDES_NEW_BOOLEAN_OPERATOR_FLAG;
}
- private boolean hasInBooleanFlag(Token t) {
- return (t.getFlags() & QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG)
+ private boolean hasInBooleanFlag(SpellCheckToken t) {
+ return (t.flags() & QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG)
== QueryConverter.TERM_IN_BOOLEAN_QUERY_FLAG;
}
}
diff --git
a/solr/core/src/test/org/apache/solr/spelling/TestSuggestSpellingConverter.java
b/solr/core/src/test/org/apache/solr/spelling/TestSuggestSpellingConverter.java
index e664e408287..ad0729b123b 100644
---
a/solr/core/src/test/org/apache/solr/spelling/TestSuggestSpellingConverter.java
+++
b/solr/core/src/test/org/apache/solr/spelling/TestSuggestSpellingConverter.java
@@ -16,7 +16,7 @@
*/
package org.apache.solr.spelling;
-import java.util.Collection;
+import java.io.IOException;
import java.util.regex.Pattern;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.LowerCaseFilter;
@@ -25,6 +25,7 @@ import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.KeywordTokenizer;
import org.apache.lucene.analysis.miscellaneous.TrimFilter;
import org.apache.lucene.analysis.pattern.PatternReplaceFilter;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.analysis.MockTokenizer;
@@ -66,12 +67,18 @@ public class TestSuggestSpellingConverter extends
BaseTokenStreamTestCase {
}
public void assertConvertsTo(String text, String expected[]) {
- Collection<Token> tokens = converter.convert(text);
- assertEquals(tokens.size(), expected.length);
- int i = 0;
- for (Token token : tokens) {
- assertEquals(token.toString(), expected[i]);
- i++;
+ try (TokenStream stream = converter.convert(text)) {
+ stream.reset();
+ CharTermAttribute termAtt = stream.addAttribute(CharTermAttribute.class);
+ int i = 0;
+ while (stream.incrementToken()) {
+ assertEquals(termAtt.toString(), expected[i]);
+ i++;
+ }
+ stream.end();
+ assertEquals(i, expected.length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
}
}
diff --git
a/solr/core/src/test/org/apache/solr/spelling/WordBreakSolrSpellCheckerTest.java
b/solr/core/src/test/org/apache/solr/spelling/WordBreakSolrSpellCheckerTest.java
index 87db8f6176a..4922b3c5c1e 100644
---
a/solr/core/src/test/org/apache/solr/spelling/WordBreakSolrSpellCheckerTest.java
+++
b/solr/core/src/test/org/apache/solr/spelling/WordBreakSolrSpellCheckerTest.java
@@ -16,8 +16,8 @@
*/
package org.apache.solr.spelling;
-import java.util.Collection;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressTempFileChecks;
@@ -73,7 +73,7 @@ public class WordBreakSolrSpellCheckerTest extends
SolrTestCaseJ4 {
{
// Prior to SOLR-8175, the required term would cause an AIOOBE.
- Collection<Token> tokens = qc.convert("+pine apple good ness");
+ List<SpellCheckToken> tokens = SpellCheckToken.drain(qc.convert("+pine
apple good ness"));
SpellingOptions spellOpts = new SpellingOptions(tokens,
searcher.get().getIndexReader(), 10);
SpellingResult result = checker.getSuggestions(spellOpts);
searcher.decref();
@@ -81,7 +81,8 @@ public class WordBreakSolrSpellCheckerTest extends
SolrTestCaseJ4 {
assertEquals(5, result.getSuggestions().size());
}
- Collection<Token> tokens = qc.convert("paintable pine apple good ness");
+ List<SpellCheckToken> tokens =
+ SpellCheckToken.drain(qc.convert("paintable pine apple good ness"));
SpellingOptions spellOpts = new SpellingOptions(tokens,
searcher.get().getIndexReader(), 10);
SpellingResult result = checker.getSuggestions(spellOpts);
searcher.decref();
@@ -89,8 +90,9 @@ public class WordBreakSolrSpellCheckerTest extends
SolrTestCaseJ4 {
assertTrue(result != null && result.getSuggestions() != null);
assertEquals(9, result.getSuggestions().size());
- for (Map.Entry<Token, LinkedHashMap<String, Integer>> s :
result.getSuggestions().entrySet()) {
- Token orig = s.getKey();
+ for (Map.Entry<SpellCheckToken, LinkedHashMap<String, Integer>> s :
+ result.getSuggestions().entrySet()) {
+ SpellCheckToken orig = s.getKey();
String[] corr = s.getValue().keySet().toArray(new String[0]);
if (orig.toString().equals("paintable")) {
assertEquals(0, orig.startOffset());
diff --git
a/solr/test-framework/src/java/org/apache/solr/handler/component/DummyCustomParamSpellChecker.java
b/solr/test-framework/src/java/org/apache/solr/handler/component/DummyCustomParamSpellChecker.java
index 4c36106fd42..a5e1a214be3 100644
---
a/solr/test-framework/src/java/org/apache/solr/handler/component/DummyCustomParamSpellChecker.java
+++
b/solr/test-framework/src/java/org/apache/solr/handler/component/DummyCustomParamSpellChecker.java
@@ -24,9 +24,9 @@ import java.util.List;
import org.apache.solr.core.SolrCore;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.spelling.SolrSpellChecker;
+import org.apache.solr.spelling.SpellCheckToken;
import org.apache.solr.spelling.SpellingOptions;
import org.apache.solr.spelling.SpellingResult;
-import org.apache.solr.spelling.Token;
/** A Dummy SpellChecker for testing purposes */
public class DummyCustomParamSpellChecker extends SolrSpellChecker {
@@ -54,7 +54,7 @@ public class DummyCustomParamSpellChecker extends
SolrSpellChecker {
int i = 0;
for (String name : lst) {
String value = options.customParams.get(name);
- result.add(new Token(name, i, i + 1), List.of(value));
+ result.add(new SpellCheckToken(name, i, i + 1), List.of(value));
i += 2;
}
return result;