You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by kr...@apache.org on 2016/12/15 21:35:26 UTC

[11/50] [abbrv] lucene-solr:jira/solr-8593: LUCENE-7575: Add UnifiedHighlighter field matcher predicate (AKA requireFieldMatch=false)

LUCENE-7575: Add UnifiedHighlighter field matcher predicate (AKA requireFieldMatch=false)


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/2e948fea
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/2e948fea
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/2e948fea

Branch: refs/heads/jira/solr-8593
Commit: 2e948fea300f883b7dfb586e303d5720d09b3210
Parents: bd8b191
Author: David Smiley <ds...@apache.org>
Authored: Mon Dec 5 16:11:57 2016 -0500
Committer: David Smiley <ds...@apache.org>
Committed: Mon Dec 5 16:11:57 2016 -0500

----------------------------------------------------------------------
 lucene/CHANGES.txt                              |   4 +
 .../uhighlight/MemoryIndexOffsetStrategy.java   |  10 +-
 .../uhighlight/MultiTermHighlighting.java       |  37 +--
 .../lucene/search/uhighlight/PhraseHelper.java  | 158 ++++++++---
 .../search/uhighlight/UnifiedHighlighter.java   |  64 +++--
 .../uhighlight/TestUnifiedHighlighter.java      | 275 +++++++++++++++++++
 .../TestUnifiedHighlighterExtensibility.java    |   3 +-
 7 files changed, 467 insertions(+), 84 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/CHANGES.txt
----------------------------------------------------------------------
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 79e44e1..c6c39ac 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -121,6 +121,10 @@ Improvements
   control how text is analyzed and converted into a query (Matt Weber
   via Mike McCandless)
 
+* LUCENE-7575: UnifiedHighlighter can now highlight fields with queries that don't
+  necessarily refer to that field (AKA requireFieldMatch==false). Disabled by default.
+  See UH get/setFieldMatcher. (Jim Ferenczi via David Smiley)
+
 Optimizations
 
 * LUCENE-7568: Optimize merging when index sorting is used but the

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
----------------------------------------------------------------------
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
index 4028912..0001a80 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MemoryIndexOffsetStrategy.java
@@ -23,6 +23,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.FilteringTokenFilter;
@@ -49,7 +50,7 @@ public class MemoryIndexOffsetStrategy extends AnalysisOffsetStrategy {
   private final LeafReader leafReader;
   private final CharacterRunAutomaton preMemIndexFilterAutomaton;
 
-  public MemoryIndexOffsetStrategy(String field, BytesRef[] extractedTerms, PhraseHelper phraseHelper,
+  public MemoryIndexOffsetStrategy(String field, Predicate<String> fieldMatcher, BytesRef[] extractedTerms, PhraseHelper phraseHelper,
                                    CharacterRunAutomaton[] automata, Analyzer analyzer,
                                    Function<Query, Collection<Query>> multiTermQueryRewrite) {
     super(field, extractedTerms, phraseHelper, automata, analyzer);
@@ -57,13 +58,14 @@ public class MemoryIndexOffsetStrategy extends AnalysisOffsetStrategy {
     memoryIndex = new MemoryIndex(true, storePayloads);//true==store offsets
     leafReader = (LeafReader) memoryIndex.createSearcher().getIndexReader(); // appears to be re-usable
     // preFilter for MemoryIndex
-    preMemIndexFilterAutomaton = buildCombinedAutomaton(field, terms, this.automata, phraseHelper, multiTermQueryRewrite);
+    preMemIndexFilterAutomaton = buildCombinedAutomaton(fieldMatcher, terms, this.automata, phraseHelper, multiTermQueryRewrite);
   }
 
   /**
    * Build one {@link CharacterRunAutomaton} matching any term the query might match.
    */
-  private static CharacterRunAutomaton buildCombinedAutomaton(String field, BytesRef[] terms,
+  private static CharacterRunAutomaton buildCombinedAutomaton(Predicate<String> fieldMatcher,
+                                                              BytesRef[] terms,
                                                               CharacterRunAutomaton[] automata,
                                                               PhraseHelper strictPhrases,
                                                               Function<Query, Collection<Query>> multiTermQueryRewrite) {
@@ -74,7 +76,7 @@ public class MemoryIndexOffsetStrategy extends AnalysisOffsetStrategy {
     Collections.addAll(allAutomata, automata);
     for (SpanQuery spanQuery : strictPhrases.getSpanQueries()) {
       Collections.addAll(allAutomata,
-          MultiTermHighlighting.extractAutomata(spanQuery, field, true, multiTermQueryRewrite));//true==lookInSpan
+          MultiTermHighlighting.extractAutomata(spanQuery, fieldMatcher, true, multiTermQueryRewrite));//true==lookInSpan
     }
 
     if (allAutomata.size() == 1) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
----------------------------------------------------------------------
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
index fd6a26a..267d603 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/MultiTermHighlighting.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.AutomatonQuery;
@@ -56,50 +57,52 @@ class MultiTermHighlighting {
   }
 
   /**
-   * Extracts all MultiTermQueries for {@code field}, and returns equivalent
-   * automata that will match terms.
+   * Extracts MultiTermQueries that match the provided field predicate.
+   * Returns equivalent automata that will match terms.
    */
-  public static CharacterRunAutomaton[] extractAutomata(Query query, String field, boolean lookInSpan,
+  public static CharacterRunAutomaton[] extractAutomata(Query query,
+                                                        Predicate<String> fieldMatcher,
+                                                        boolean lookInSpan,
                                                         Function<Query, Collection<Query>> preRewriteFunc) {
     List<CharacterRunAutomaton> list = new ArrayList<>();
     Collection<Query> customSubQueries = preRewriteFunc.apply(query);
     if (customSubQueries != null) {
       for (Query sub : customSubQueries) {
-        list.addAll(Arrays.asList(extractAutomata(sub, field, lookInSpan, preRewriteFunc)));
+        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
       }
     } else if (query instanceof BooleanQuery) {
       for (BooleanClause clause : (BooleanQuery) query) {
         if (!clause.isProhibited()) {
-          list.addAll(Arrays.asList(extractAutomata(clause.getQuery(), field, lookInSpan, preRewriteFunc)));
+          list.addAll(Arrays.asList(extractAutomata(clause.getQuery(), fieldMatcher, lookInSpan, preRewriteFunc)));
         }
       }
     } else if (query instanceof ConstantScoreQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((ConstantScoreQuery) query).getQuery(), field, lookInSpan,
+      list.addAll(Arrays.asList(extractAutomata(((ConstantScoreQuery) query).getQuery(), fieldMatcher, lookInSpan,
           preRewriteFunc)));
     } else if (query instanceof DisjunctionMaxQuery) {
       for (Query sub : ((DisjunctionMaxQuery) query).getDisjuncts()) {
-        list.addAll(Arrays.asList(extractAutomata(sub, field, lookInSpan, preRewriteFunc)));
+        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
       }
     } else if (lookInSpan && query instanceof SpanOrQuery) {
       for (Query sub : ((SpanOrQuery) query).getClauses()) {
-        list.addAll(Arrays.asList(extractAutomata(sub, field, lookInSpan, preRewriteFunc)));
+        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
       }
     } else if (lookInSpan && query instanceof SpanNearQuery) {
       for (Query sub : ((SpanNearQuery) query).getClauses()) {
-        list.addAll(Arrays.asList(extractAutomata(sub, field, lookInSpan, preRewriteFunc)));
+        list.addAll(Arrays.asList(extractAutomata(sub, fieldMatcher, lookInSpan, preRewriteFunc)));
       }
     } else if (lookInSpan && query instanceof SpanNotQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanNotQuery) query).getInclude(), field, lookInSpan,
+      list.addAll(Arrays.asList(extractAutomata(((SpanNotQuery) query).getInclude(), fieldMatcher, lookInSpan,
           preRewriteFunc)));
     } else if (lookInSpan && query instanceof SpanPositionCheckQuery) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanPositionCheckQuery) query).getMatch(), field, lookInSpan,
+      list.addAll(Arrays.asList(extractAutomata(((SpanPositionCheckQuery) query).getMatch(), fieldMatcher, lookInSpan,
           preRewriteFunc)));
     } else if (lookInSpan && query instanceof SpanMultiTermQueryWrapper) {
-      list.addAll(Arrays.asList(extractAutomata(((SpanMultiTermQueryWrapper<?>) query).getWrappedQuery(), field,
-          lookInSpan, preRewriteFunc)));
+      list.addAll(Arrays.asList(extractAutomata(((SpanMultiTermQueryWrapper<?>) query).getWrappedQuery(),
+          fieldMatcher, lookInSpan, preRewriteFunc)));
     } else if (query instanceof AutomatonQuery) {
       final AutomatonQuery aq = (AutomatonQuery) query;
-      if (aq.getField().equals(field)) {
+      if (fieldMatcher.test(aq.getField())) {
         list.add(new CharacterRunAutomaton(aq.getAutomaton()) {
           @Override
           public String toString() {
@@ -110,7 +113,7 @@ class MultiTermHighlighting {
     } else if (query instanceof PrefixQuery) {
       final PrefixQuery pq = (PrefixQuery) query;
       Term prefix = pq.getPrefix();
-      if (prefix.field().equals(field)) {
+      if (fieldMatcher.test(prefix.field())) {
         list.add(new CharacterRunAutomaton(Operations.concatenate(Automata.makeString(prefix.text()),
             Automata.makeAnyString())) {
           @Override
@@ -121,7 +124,7 @@ class MultiTermHighlighting {
       }
     } else if (query instanceof FuzzyQuery) {
       final FuzzyQuery fq = (FuzzyQuery) query;
-      if (fq.getField().equals(field)) {
+      if (fieldMatcher.test(fq.getField())) {
         String utf16 = fq.getTerm().text();
         int termText[] = new int[utf16.codePointCount(0, utf16.length())];
         for (int cp, i = 0, j = 0; i < utf16.length(); i += Character.charCount(cp)) {
@@ -142,7 +145,7 @@ class MultiTermHighlighting {
       }
     } else if (query instanceof TermRangeQuery) {
       final TermRangeQuery tq = (TermRangeQuery) query;
-      if (tq.getField().equals(field)) {
+      if (fieldMatcher.test(tq.getField())) {
         final CharsRef lowerBound;
         if (tq.getLowerTerm() == null) {
           lowerBound = null;

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
----------------------------------------------------------------------
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
index cde17ba..d7e8671 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/PhraseHelper.java
@@ -16,17 +16,50 @@
  */
 package org.apache.lucene.search.uhighlight;
 
-import org.apache.lucene.index.*;
-import org.apache.lucene.search.*;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.Fields;
+import org.apache.lucene.index.FilterLeafReader;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.MultiTermQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TwoPhaseIterator;
 import org.apache.lucene.search.highlight.WeightedSpanTerm;
 import org.apache.lucene.search.highlight.WeightedSpanTermExtractor;
-import org.apache.lucene.search.spans.*;
+import org.apache.lucene.search.spans.SpanCollector;
+import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper;
+import org.apache.lucene.search.spans.SpanQuery;
+import org.apache.lucene.search.spans.SpanWeight;
+import org.apache.lucene.search.spans.Spans;
 import org.apache.lucene.util.BytesRef;
 
-import java.io.IOException;
-import java.util.*;
-import java.util.function.Function;
-
 /**
  * Helps the {@link FieldOffsetStrategy} with strict position highlighting (e.g. highlight phrases correctly).
  * This is a stateful class holding information about the query, but it can (and is) re-used across highlighting
@@ -40,7 +73,7 @@ import java.util.function.Function;
 public class PhraseHelper {
 
   public static final PhraseHelper NONE = new PhraseHelper(new MatchAllDocsQuery(), "_ignored_",
-      spanQuery -> null, query -> null, true);
+      (s) -> false, spanQuery -> null, query -> null, true);
 
   //TODO it seems this ought to be a general thing on Spans?
   private static final Comparator<? super Spans> SPANS_COMPARATOR = (o1, o2) -> {
@@ -59,10 +92,11 @@ public class PhraseHelper {
     }
   };
 
-  private final String fieldName; // if non-null, only look at queries/terms for this field
+  private final String fieldName;
   private final Set<Term> positionInsensitiveTerms; // (TermQuery terms)
   private final Set<SpanQuery> spanQueries;
   private final boolean willRewrite;
+  private final Predicate<String> fieldMatcher;
 
   /**
    * Constructor.
@@ -73,14 +107,15 @@ public class PhraseHelper {
    * to be set before the {@link WeightedSpanTermExtractor}'s extraction is invoked.
    * {@code ignoreQueriesNeedingRewrite} effectively ignores any query clause that needs to be "rewritten", which is
    * usually limited to just a {@link SpanMultiTermQueryWrapper} but could be other custom ones.
+   * {@code fieldMatcher} The field name predicate to use for extracting the query part that must be highlighted.
    */
-  public PhraseHelper(Query query, String field, Function<SpanQuery, Boolean> rewriteQueryPred,
+  public PhraseHelper(Query query, String field, Predicate<String> fieldMatcher, Function<SpanQuery, Boolean> rewriteQueryPred,
                       Function<Query, Collection<Query>> preExtractRewriteFunction,
                       boolean ignoreQueriesNeedingRewrite) {
-    this.fieldName = field; // if null then don't require field match
+    this.fieldName = field;
+    this.fieldMatcher = fieldMatcher;
     // filter terms to those we want
-    positionInsensitiveTerms = field != null ? new FieldFilteringTermHashSet(field) : new HashSet<>();
-    // requireFieldMatch optional
+    positionInsensitiveTerms = new FieldFilteringTermSet();
     spanQueries = new HashSet<>();
 
     // TODO Have toSpanQuery(query) Function as an extension point for those with custom Query impls
@@ -131,11 +166,11 @@ public class PhraseHelper {
       @Override
       protected void extractWeightedSpanTerms(Map<String, WeightedSpanTerm> terms, SpanQuery spanQuery,
                                               float boost) throws IOException {
-        if (field != null) {
-          // if this span query isn't for this field, skip it.
-          Set<String> fieldNameSet = new HashSet<>();//TODO reuse.  note: almost always size 1
-          collectSpanQueryFields(spanQuery, fieldNameSet);
-          if (!fieldNameSet.contains(field)) {
+        // if this span query isn't for this field, skip it.
+        Set<String> fieldNameSet = new HashSet<>();//TODO reuse.  note: almost always size 1
+        collectSpanQueryFields(spanQuery, fieldNameSet);
+        for (String spanField : fieldNameSet) {
+          if (!fieldMatcher.test(spanField)) {
             return;
           }
         }
@@ -190,10 +225,11 @@ public class PhraseHelper {
     if (spanQueries.isEmpty()) {
       return Collections.emptyMap();
     }
+    final LeafReader filteredReader = new SingleFieldFilterLeafReader(leafReader, fieldName);
     // for each SpanQuery, collect the member spans into a map.
     Map<BytesRef, Spans> result = new HashMap<>();
     for (SpanQuery spanQuery : spanQueries) {
-      getTermToSpans(spanQuery, leafReader.getContext(), doc, result);
+      getTermToSpans(spanQuery, filteredReader.getContext(), doc, result);
     }
     return result;
   }
@@ -203,15 +239,14 @@ public class PhraseHelper {
                               int doc, Map<BytesRef, Spans> result)
       throws IOException {
     // note: in WSTE there was some field specific looping that seemed pointless so that isn't here.
-    final IndexSearcher searcher = new IndexSearcher(readerContext);
+    final IndexSearcher searcher = new IndexSearcher(readerContext.reader());
     searcher.setQueryCache(null);
     if (willRewrite) {
       spanQuery = (SpanQuery) searcher.rewrite(spanQuery); // searcher.rewrite loops till done
     }
 
     // Get the underlying query terms
-
-    TreeSet<Term> termSet = new TreeSet<>(); // sorted so we can loop over results in order shortly...
+    TreeSet<Term> termSet = new FieldFilteringTermSet(); // sorted so we can loop over results in order shortly...
     searcher.createWeight(spanQuery, false, 1.0f).extractTerms(termSet);//needsScores==false
 
     // Get Spans by running the query against the reader
@@ -240,9 +275,6 @@ public class PhraseHelper {
     for (final Term queryTerm : termSet) {
       // note: we expect that at least one query term will pass these filters. This is because the collected
       //   spanQuery list were already filtered by these conditions.
-      if (fieldName != null && fieldName.equals(queryTerm.field()) == false) {
-        continue;
-      }
       if (positionInsensitiveTerms.contains(queryTerm)) {
         continue;
       }
@@ -375,19 +407,17 @@ public class PhraseHelper {
   }
 
   /**
-   * Simple HashSet that filters out Terms not matching a desired field on {@code add()}.
+   * Simple TreeSet that filters out Terms not matching the provided predicate on {@code add()}.
    */
-  private static class FieldFilteringTermHashSet extends HashSet<Term> {
-    private final String field;
-
-    FieldFilteringTermHashSet(String field) {
-      this.field = field;
-    }
-
+  private class FieldFilteringTermSet extends TreeSet<Term> {
     @Override
     public boolean add(Term term) {
-      if (term.field().equals(field)) {
-        return super.add(term);
+      if (fieldMatcher.test(term.field())) {
+        if (term.field().equals(fieldName)) {
+          return super.add(term);
+        } else {
+          return super.add(new Term(fieldName, term.bytes()));
+        }
       } else {
         return false;
       }
@@ -500,6 +530,64 @@ public class PhraseHelper {
   }
 
   /**
+   * This reader will just delegate every call to a single field in the wrapped
+   * LeafReader. This way we ensure that all queries going through this reader target the same field.
+  */
+  static final class SingleFieldFilterLeafReader extends FilterLeafReader {
+    final String fieldName;
+    SingleFieldFilterLeafReader(LeafReader in, String fieldName) {
+      super(in);
+      this.fieldName = fieldName;
+    }
+
+    @Override
+    public FieldInfos getFieldInfos() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Fields fields() throws IOException {
+      return new FilterFields(super.fields()) {
+        @Override
+        public Terms terms(String field) throws IOException {
+          return super.terms(fieldName);
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+          return Collections.singletonList(fieldName).iterator();
+        }
+
+        @Override
+        public int size() {
+          return 1;
+        }
+      };
+    }
+
+    @Override
+    public NumericDocValues getNumericDocValues(String field) throws IOException {
+      return super.getNumericDocValues(fieldName);
+    }
+
+    @Override
+    public BinaryDocValues getBinaryDocValues(String field) throws IOException {
+      return super.getBinaryDocValues(fieldName);
+    }
+
+    @Override
+    public SortedDocValues getSortedDocValues(String field) throws IOException {
+      return super.getSortedDocValues(fieldName);
+    }
+
+    @Override
+    public NumericDocValues getNormValues(String field) throws IOException {
+      return super.getNormValues(fieldName);
+    }
+  }
+
+
+  /**
    * A Spans based on a list of cached spans for one doc.  It is pre-positioned to this doc.
    */
   private static class CachedSpans extends Spans {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
----------------------------------------------------------------------
diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
index ac5f0f6..bbcfd5b 100644
--- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
+++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java
@@ -24,6 +24,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -31,6 +32,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 import org.apache.lucene.analysis.Analyzer;
@@ -58,7 +60,6 @@ import org.apache.lucene.search.Weight;
 import org.apache.lucene.search.spans.SpanQuery;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.InPlaceMergeSorter;
-import org.apache.lucene.util.UnicodeUtil;
 import org.apache.lucene.util.automaton.CharacterRunAutomaton;
 
 /**
@@ -119,13 +120,13 @@ public class UnifiedHighlighter {
 
   private boolean defaultPassageRelevancyOverSpeed = true; //For analysis, prefer MemoryIndexOffsetStrategy
 
-  // private boolean defaultRequireFieldMatch = true; TODO
-
   private int maxLength = DEFAULT_MAX_LENGTH;
 
   // BreakIterator is stateful so we use a Supplier factory method
   private Supplier<BreakIterator> defaultBreakIterator = () -> BreakIterator.getSentenceInstance(Locale.ROOT);
 
+  private Predicate<String> defaultFieldMatcher;
+
   private PassageScorer defaultScorer = new PassageScorer();
 
   private PassageFormatter defaultFormatter = new DefaultPassageFormatter();
@@ -140,8 +141,8 @@ public class UnifiedHighlighter {
   /**
    * Calls {@link Weight#extractTerms(Set)} on an empty index for the query.
    */
-  protected static SortedSet<Term> extractTerms(Query query) throws IOException {
-    SortedSet<Term> queryTerms = new TreeSet<>();
+  protected static Set<Term> extractTerms(Query query) throws IOException {
+    Set<Term> queryTerms = new HashSet<>();
     EMPTY_INDEXSEARCHER.createNormalizedWeight(query, false).extractTerms(queryTerms);
     return queryTerms;
   }
@@ -197,6 +198,10 @@ public class UnifiedHighlighter {
     this.cacheFieldValCharsThreshold = cacheFieldValCharsThreshold;
   }
 
+  public void setFieldMatcher(Predicate<String> predicate) {
+    this.defaultFieldMatcher = predicate;
+  }
+
   /**
    * Returns whether {@link MultiTermQuery} derivatives will be highlighted.  By default it's enabled.  MTQ
    * highlighting can be expensive, particularly when using offsets in postings.
@@ -220,6 +225,18 @@ public class UnifiedHighlighter {
     return defaultPassageRelevancyOverSpeed;
   }
 
+  /**
+   * Returns the predicate to use for extracting the query part that must be highlighted.
+   * By default only queries that target the current field are kept. (AKA requireFieldMatch)
+   */
+  protected Predicate<String> getFieldMatcher(String field) {
+    if (defaultFieldMatcher != null) {
+      return defaultFieldMatcher;
+    } else {
+      // requireFieldMatch = true
+      return (qf) -> field.equals(qf);
+    }
+  }
 
   /**
    * The maximum content size to process.  Content will be truncated to this size before highlighting. Typically
@@ -548,7 +565,7 @@ public class UnifiedHighlighter {
     copyAndSortFieldsWithMaxPassages(fieldsIn, maxPassagesIn, fields, maxPassages); // latter 2 are "out" params
 
     // Init field highlighters (where most of the highlight logic lives, and on a per field basis)
-    SortedSet<Term> queryTerms = extractTerms(query);
+    Set<Term> queryTerms = extractTerms(query);
     FieldHighlighter[] fieldHighlighters = new FieldHighlighter[fields.length];
     int numTermVectors = 0;
     int numPostings = 0;
@@ -718,13 +735,13 @@ public class UnifiedHighlighter {
           getClass().getSimpleName() + " without an IndexSearcher.");
     }
     Objects.requireNonNull(content, "content is required");
-    SortedSet<Term> queryTerms = extractTerms(query);
+    Set<Term> queryTerms = extractTerms(query);
     return getFieldHighlighter(field, query, queryTerms, maxPassages)
         .highlightFieldForDoc(null, -1, content);
   }
 
-  protected FieldHighlighter getFieldHighlighter(String field, Query query, SortedSet<Term> allTerms, int maxPassages) {
-    BytesRef[] terms = filterExtractedTerms(field, allTerms);
+  protected FieldHighlighter getFieldHighlighter(String field, Query query, Set<Term> allTerms, int maxPassages) {
+    BytesRef[] terms = filterExtractedTerms(getFieldMatcher(field), allTerms);
     Set<HighlightFlag> highlightFlags = getFlags(field);
     PhraseHelper phraseHelper = getPhraseHelper(field, query, highlightFlags);
     CharacterRunAutomaton[] automata = getAutomata(field, query, highlightFlags);
@@ -738,19 +755,15 @@ public class UnifiedHighlighter {
         getFormatter(field));
   }
 
-  protected static BytesRef[] filterExtractedTerms(String field, SortedSet<Term> queryTerms) {
-    // TODO consider requireFieldMatch
-    Term floor = new Term(field, "");
-    Term ceiling = new Term(field, UnicodeUtil.BIG_TERM);
-    SortedSet<Term> fieldTerms = queryTerms.subSet(floor, ceiling);
-
-    // Strip off the redundant field:
-    BytesRef[] terms = new BytesRef[fieldTerms.size()];
-    int termUpto = 0;
-    for (Term term : fieldTerms) {
-      terms[termUpto++] = term.bytes();
+  protected static BytesRef[] filterExtractedTerms(Predicate<String> fieldMatcher, Set<Term> queryTerms) {
+    // Strip off the redundant field and sort the remaining terms
+    SortedSet<BytesRef> filteredTerms = new TreeSet<>();
+    for (Term term : queryTerms) {
+      if (fieldMatcher.test(term.field())) {
+        filteredTerms.add(term.bytes());
+      }
     }
-    return terms;
+    return filteredTerms.toArray(new BytesRef[filteredTerms.size()]);
   }
 
   protected Set<HighlightFlag> getFlags(String field) {
@@ -771,14 +784,13 @@ public class UnifiedHighlighter {
     boolean highlightPhrasesStrictly = highlightFlags.contains(HighlightFlag.PHRASES);
     boolean handleMultiTermQuery = highlightFlags.contains(HighlightFlag.MULTI_TERM_QUERY);
     return highlightPhrasesStrictly ?
-        new PhraseHelper(query, field, this::requiresRewrite, this::preSpanQueryRewrite, !handleMultiTermQuery) :
-        PhraseHelper.NONE;
+        new PhraseHelper(query, field, getFieldMatcher(field),
+            this::requiresRewrite, this::preSpanQueryRewrite, !handleMultiTermQuery) : PhraseHelper.NONE;
   }
 
   protected CharacterRunAutomaton[] getAutomata(String field, Query query, Set<HighlightFlag> highlightFlags) {
     return highlightFlags.contains(HighlightFlag.MULTI_TERM_QUERY)
-        ? MultiTermHighlighting.extractAutomata(query, field, !highlightFlags.contains(HighlightFlag.PHRASES),
-          this::preMultiTermQueryRewrite)
+        ? MultiTermHighlighting.extractAutomata(query, getFieldMatcher(field), !highlightFlags.contains(HighlightFlag.PHRASES), this::preMultiTermQueryRewrite)
         : ZERO_LEN_AUTOMATA_ARRAY;
   }
 
@@ -826,7 +838,7 @@ public class UnifiedHighlighter {
           //skip using a memory index since it's pure term filtering
           return new TokenStreamOffsetStrategy(field, terms, phraseHelper, automata, getIndexAnalyzer());
         } else {
-          return new MemoryIndexOffsetStrategy(field, terms, phraseHelper, automata, getIndexAnalyzer(),
+          return new MemoryIndexOffsetStrategy(field, getFieldMatcher(field), terms, phraseHelper, automata, getIndexAnalyzer(),
               this::preMultiTermQueryRewrite);
         }
       case NONE_NEEDED:

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java
----------------------------------------------------------------------
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java
index 0fd7d3d..ddf8a92 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java
@@ -25,6 +25,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 import org.apache.lucene.analysis.MockAnalyzer;
@@ -32,14 +33,17 @@ import org.apache.lucene.analysis.MockTokenizer;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.document.FieldType;
+import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.FuzzyQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.PhraseQuery;
+import org.apache.lucene.search.PrefixQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.ScoreDoc;
 import org.apache.lucene.search.Sort;
@@ -959,4 +963,275 @@ public class TestUnifiedHighlighter extends LuceneTestCase {
     ir.close();
   }
 
+  private IndexReader indexSomeFields() throws IOException {
+    RandomIndexWriter iw = new RandomIndexWriter(random(), dir, indexAnalyzer);
+    FieldType ft = new FieldType();
+    ft.setIndexOptions(IndexOptions.NONE);
+    ft.setTokenized(false);
+    ft.setStored(true);
+    ft.freeze();
+
+    Field title = new Field("title", "", fieldType);
+    Field text = new Field("text", "", fieldType);
+    Field category = new Field("category", "", fieldType);
+
+    Document doc = new Document();
+    doc.add(title);
+    doc.add(text);
+    doc.add(category);
+    title.setStringValue("This is the title field.");
+    text.setStringValue("This is the text field. You can put some text if you want.");
+    category.setStringValue("This is the category field.");
+    iw.addDocument(doc);
+
+    IndexReader ir = iw.getReader();
+    iw.close();
+    return ir;
+  }
+
+  public void testFieldMatcherTermQuery() throws Exception {
+    IndexReader ir = indexSomeFields();
+    IndexSearcher searcher = newSearcher(ir);
+    UnifiedHighlighter highlighterNoFieldMatch = new UnifiedHighlighter(searcher, indexAnalyzer) {
+      @Override
+      protected Predicate<String> getFieldMatcher(String field) {
+        // requireFieldMatch=false
+        return (qf) -> true;
+      }
+    };
+    UnifiedHighlighter highlighterFieldMatch = new UnifiedHighlighter(searcher, indexAnalyzer);
+    BooleanQuery.Builder queryBuilder =
+        new BooleanQuery.Builder()
+            .add(new TermQuery(new Term("text", "some")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("text", "field")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("text", "this")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("title", "is")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("title", "this")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("category", "this")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("category", "some")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("category", "category")), BooleanClause.Occur.SHOULD);
+    Query query = queryBuilder.build();
+
+    // title
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the title <b>field</b>.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the title field.", snippets[0]);
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "text".equals(fq));
+      snippets = highlighterFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> is the title <b>field</b>.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+
+    // text
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the text <b>field</b>. You can put <b>some</b> text if you want.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> is the text <b>field</b>. You can put <b>some</b> text if you want.", snippets[0]);
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "title".equals(fq));
+      snippets = highlighterFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the text field. ", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+
+    // category
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the <b>category</b> <b>field</b>.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> is the <b>category</b> field.", snippets[0]);
+
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "title".equals(fq));
+      snippets = highlighterFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the category field.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+    ir.close();
+  }
+
+  public void testFieldMatcherMultiTermQuery() throws Exception {
+    IndexReader ir = indexSomeFields();
+    IndexSearcher searcher = newSearcher(ir);
+    UnifiedHighlighter highlighterNoFieldMatch = new UnifiedHighlighter(searcher, indexAnalyzer) {
+      @Override
+      protected Predicate<String> getFieldMatcher(String field) {
+        // requireFieldMatch=false
+        return (qf) -> true;
+      }
+    };
+    UnifiedHighlighter highlighterFieldMatch = new UnifiedHighlighter(searcher, indexAnalyzer);
+    BooleanQuery.Builder queryBuilder =
+        new BooleanQuery.Builder()
+            .add(new FuzzyQuery(new Term("text", "sime"), 1), BooleanClause.Occur.SHOULD)
+            .add(new PrefixQuery(new Term("text", "fie")), BooleanClause.Occur.SHOULD)
+            .add(new PrefixQuery(new Term("text", "thi")), BooleanClause.Occur.SHOULD)
+            .add(new TermQuery(new Term("title", "is")), BooleanClause.Occur.SHOULD)
+            .add(new PrefixQuery(new Term("title", "thi")), BooleanClause.Occur.SHOULD)
+            .add(new PrefixQuery(new Term("category", "thi")), BooleanClause.Occur.SHOULD)
+            .add(new FuzzyQuery(new Term("category", "sime"), 1), BooleanClause.Occur.SHOULD)
+            .add(new PrefixQuery(new Term("category", "categ")), BooleanClause.Occur.SHOULD);
+    Query query = queryBuilder.build();
+
+    // title
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the title <b>field</b>.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the title field.", snippets[0]);
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "text".equals(fq));
+      snippets = highlighterFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> is the title <b>field</b>.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+
+    // text
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the text <b>field</b>. You can put <b>some</b> text if you want.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> is the text <b>field</b>. You can put <b>some</b> text if you want.", snippets[0]);
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "title".equals(fq));
+      snippets = highlighterFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the text field. ", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+
+    // category
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the <b>category</b> <b>field</b>.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> is the <b>category</b> field.", snippets[0]);
+
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "title".equals(fq));
+      snippets = highlighterFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the category field.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+    ir.close();
+  }
+
+  public void testFieldMatcherPhraseQuery() throws Exception {
+    IndexReader ir = indexSomeFields();
+    IndexSearcher searcher = newSearcher(ir);
+    UnifiedHighlighter highlighterNoFieldMatch = new UnifiedHighlighter(searcher, indexAnalyzer) {
+      @Override
+      protected Predicate<String> getFieldMatcher(String field) {
+        // requireFieldMatch=false
+        return (qf) -> true;
+      }
+    };
+    UnifiedHighlighter highlighterFieldMatch = new UnifiedHighlighter(searcher, indexAnalyzer);
+    BooleanQuery.Builder queryBuilder =
+        new BooleanQuery.Builder()
+            .add(new PhraseQuery("title", "this", "is", "the", "title"), BooleanClause.Occur.SHOULD)
+            .add(new PhraseQuery(2, "category", "this", "is", "the", "field"), BooleanClause.Occur.SHOULD)
+            .add(new PhraseQuery("text", "this", "is"), BooleanClause.Occur.SHOULD)
+            .add(new PhraseQuery("category", "this", "is"), BooleanClause.Occur.SHOULD)
+            .add(new PhraseQuery(1, "text", "you", "can", "put", "text"), BooleanClause.Occur.SHOULD);
+    Query query = queryBuilder.build();
+
+    // title
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> <b>the</b> <b>title</b> <b>field</b>.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> <b>the</b> <b>title</b> field.", snippets[0]);
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "text".equals(fq));
+      snippets = highlighterFieldMatch.highlight("title", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the title field.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+
+    // text
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> <b>the</b> <b>text</b> <b>field</b>. <b>You</b> <b>can</b> <b>put</b> some <b>text</b> if you want.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the <b>text</b> field. <b>You</b> <b>can</b> <b>put</b> some <b>text</b> if you want.", snippets[0]);
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "title".equals(fq));
+      snippets = highlighterFieldMatch.highlight("text", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("This is the text field. You can put some text if you want.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+
+    // category
+    {
+      TopDocs topDocs = searcher.search(query, 10, Sort.INDEXORDER);
+      assertEquals(1, topDocs.totalHits);
+      String[] snippets = highlighterNoFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> <b>the</b> category <b>field</b>.", snippets[0]);
+
+      snippets = highlighterFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> <b>the</b> category <b>field</b>.", snippets[0]);
+
+
+      highlighterFieldMatch.setFieldMatcher((fq) -> "text".equals(fq));
+      snippets = highlighterFieldMatch.highlight("category", query, topDocs, 10);
+      assertEquals(1, snippets.length);
+      assertEquals("<b>This</b> <b>is</b> the category field.", snippets[0]);
+      highlighterFieldMatch.setFieldMatcher(null);
+    }
+    ir.close();
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2e948fea/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/visibility/TestUnifiedHighlighterExtensibility.java
----------------------------------------------------------------------
diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/visibility/TestUnifiedHighlighterExtensibility.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/visibility/TestUnifiedHighlighterExtensibility.java
index d150940..10757a5 100644
--- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/visibility/TestUnifiedHighlighterExtensibility.java
+++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/visibility/TestUnifiedHighlighterExtensibility.java
@@ -23,7 +23,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.SortedSet;
 
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.MockAnalyzer;
@@ -144,7 +143,7 @@ public class TestUnifiedHighlighterExtensibility extends LuceneTestCase {
       }
 
       @Override
-      protected FieldHighlighter getFieldHighlighter(String field, Query query, SortedSet<Term> allTerms, int maxPassages) {
+      protected FieldHighlighter getFieldHighlighter(String field, Query query, Set<Term> allTerms, int maxPassages) {
         return super.getFieldHighlighter(field, query, allTerms, maxPassages);
       }