You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by cp...@apache.org on 2022/04/04 17:12:06 UTC

[solr] branch branch_9x updated: SOLR-16111: hl.queryFieldPattern support (advanced alternative to hl.requireFieldMatch) (#757)

This is an automated email from the ASF dual-hosted git repository.

cpoerschke pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new fba1330e849 SOLR-16111: hl.queryFieldPattern support (advanced alternative to hl.requireFieldMatch) (#757)
fba1330e849 is described below

commit fba1330e8494f20ab9724d7c1764137aeabff2ad
Author: Christine Poerschke <cp...@apache.org>
AuthorDate: Mon Apr 4 18:00:59 2022 +0100

    SOLR-16111: hl.queryFieldPattern support (advanced alternative to hl.requireFieldMatch) (#757)
    
    (cherry picked from commit 96c1577987412004b5df653bdebe2eb1d470e624)
---
 solr/CHANGES.txt                                   |   3 +-
 .../org/apache/solr/highlight/SolrHighlighter.java |   2 +-
 .../solr/highlight/UnifiedSolrHighlighter.java     |  22 +-
 .../apache/solr/search/SolrDocumentFetcher.java    |  20 +-
 .../collection1/conf/schema-unifiedhighlight.xml   |   2 +
 .../solr/highlight/TestUnifiedSolrHighlighter.java | 304 +++++++++++++++++++++
 .../modules/query-guide/pages/highlighting.adoc    |  13 +
 .../apache/solr/common/params/HighlightParams.java |   1 +
 8 files changed, 361 insertions(+), 6 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index fddcf1b6d44..92cb2acf61d 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -8,7 +8,8 @@ https://github.com/apache/solr/blob/main/solr/solr-ref-guide/modules/upgrade-not
 
 New Features
 ---------------------
-(No changes)
+* SOLR-16111: Add hl.queryFieldPattern support, an advanced alternative to the hl.requireFieldMatch boolean flag.
+  (Christine Poerschke, David Smiley)
 
 Improvements
 ---------------------
diff --git a/solr/core/src/java/org/apache/solr/highlight/SolrHighlighter.java b/solr/core/src/java/org/apache/solr/highlight/SolrHighlighter.java
index ff5ca982711..134175d0b02 100644
--- a/solr/core/src/java/org/apache/solr/highlight/SolrHighlighter.java
+++ b/solr/core/src/java/org/apache/solr/highlight/SolrHighlighter.java
@@ -88,7 +88,7 @@ public abstract class SolrHighlighter {
     return (arr == null || arr.length == 0 || arr[0] == null || arr[0].trim().length() == 0);
   }
 
-  private static String[] expandWildcardsInFields(
+  protected static String[] expandWildcardsInFields(
       Supplier<Collection<String>> availableFieldNamesSupplier, String... inFields) {
     Set<String> expandedFields = new LinkedHashSet<String>();
     Collection<String> availableFieldNames = null;
diff --git a/solr/core/src/java/org/apache/solr/highlight/UnifiedSolrHighlighter.java b/solr/core/src/java/org/apache/solr/highlight/UnifiedSolrHighlighter.java
index 890db9af153..f14c43d1fd3 100644
--- a/solr/core/src/java/org/apache/solr/highlight/UnifiedSolrHighlighter.java
+++ b/solr/core/src/java/org/apache/solr/highlight/UnifiedSolrHighlighter.java
@@ -18,6 +18,7 @@ package org.apache.solr.highlight;
 
 import java.io.IOException;
 import java.text.BreakIterator;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
@@ -25,6 +26,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import org.apache.lucene.index.FieldInfo;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.Query;
@@ -236,6 +238,7 @@ public class UnifiedSolrHighlighter extends SolrHighlighter implements PluginInf
   /** From {@link #getHighlighter(org.apache.solr.request.SolrQueryRequest)}. */
   protected static class SolrExtendedUnifiedHighlighter extends UnifiedHighlighter {
     protected static final Predicate<String> NOT_REQUIRED_FIELD_MATCH_PREDICATE = s -> true;
+    private final SolrIndexSearcher solrIndexSearcher;
     protected final SolrParams params;
 
     protected final IndexSchema schema;
@@ -243,6 +246,7 @@ public class UnifiedSolrHighlighter extends SolrHighlighter implements PluginInf
 
     public SolrExtendedUnifiedHighlighter(SolrQueryRequest req) {
       super(req.getSearcher(), req.getSchema().getIndexAnalyzer());
+      this.solrIndexSearcher = req.getSearcher();
       this.params = req.getParams();
       this.schema = req.getSchema();
       this.setMaxLength(params.getInt(HighlightParams.MAX_CHARS, DEFAULT_MAX_CHARS));
@@ -431,14 +435,26 @@ public class UnifiedSolrHighlighter extends SolrHighlighter implements PluginInf
 
     @Override
     protected Predicate<String> getFieldMatcher(String field) {
-      // TODO define hl.queryFieldPattern as a more advanced alternative to hl.requireFieldMatch.
 
       // note that the UH at Lucene level default to effectively "true"
       if (params.getFieldBool(field, HighlightParams.FIELD_MATCH, false)) {
         return field::equals; // requireFieldMatch
-      } else {
-        return NOT_REQUIRED_FIELD_MATCH_PREDICATE;
       }
+
+      String[] queryFieldPattern =
+          params.getFieldParams(field, HighlightParams.QUERY_FIELD_PATTERN);
+      if (queryFieldPattern != null && queryFieldPattern.length != 0) {
+
+        Supplier<Collection<String>> indexedFieldsSupplier =
+            () -> solrIndexSearcher.getDocFetcher().getIndexedFieldNames();
+
+        Set<String> fields =
+            Set.of(expandWildcardsInFields(indexedFieldsSupplier, queryFieldPattern));
+
+        return fields::contains;
+      }
+
+      return NOT_REQUIRED_FIELD_MATCH_PREDICATE;
     }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java b/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java
index 86aeaa82da3..eab3a837315 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrDocumentFetcher.java
@@ -45,6 +45,7 @@ import org.apache.lucene.index.BinaryDocValues;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.index.IndexableFieldType;
@@ -113,6 +114,8 @@ public class SolrDocumentFetcher {
 
   private Collection<String> storedHighlightFieldNames; // lazy populated; use getter
 
+  private Collection<String> indexedFieldNames; // lazy populated; use getter
+
   @SuppressWarnings({"unchecked"})
   SolrDocumentFetcher(SolrIndexSearcher searcher, SolrConfig solrConfig, boolean cachingEnabled) {
     this.searcher = searcher;
@@ -199,7 +202,7 @@ public class SolrDocumentFetcher {
   public Collection<String> getStoredHighlightFieldNames() {
     synchronized (this) {
       if (storedHighlightFieldNames == null) {
-        storedHighlightFieldNames = new LinkedList<>();
+        storedHighlightFieldNames = new ArrayList<>();
         for (FieldInfo fieldInfo : searcher.getFieldInfos()) {
           final String fieldName = fieldInfo.name;
           try {
@@ -219,6 +222,21 @@ public class SolrDocumentFetcher {
     }
   }
 
+  /** Returns a collection of the names of all indexed fields which the index reader knows about. */
+  public Collection<String> getIndexedFieldNames() {
+    synchronized (this) {
+      if (indexedFieldNames == null) {
+        indexedFieldNames = new ArrayList<>();
+        for (FieldInfo fieldInfo : searcher.getFieldInfos()) {
+          if (fieldInfo.getIndexOptions() != IndexOptions.NONE) {
+            indexedFieldNames.add(fieldInfo.name);
+          }
+        }
+      }
+      return indexedFieldNames;
+    }
+  }
+
   /**
    * @see SolrIndexSearcher#doc(int)
    */
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-unifiedhighlight.xml b/solr/core/src/test-files/solr/collection1/conf/schema-unifiedhighlight.xml
index 90b7c524710..2a1ed552a1e 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-unifiedhighlight.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-unifiedhighlight.xml
@@ -39,6 +39,8 @@
   <field name="id" type="string" indexed="true" stored="${solr.tests.id.stored:true}" multiValued="false" docValues="${solr.tests.id.docValues:false}" required="false"/>
   <field name="text" type="text_offsets" indexed="true" stored="true"/>
   <field name="text2" type="text" indexed="true" stored="true"/>
+  <copyField source="text2" dest="text2_indexed_not_stored"/>
+  <field name="text2_indexed_not_stored" type="text" indexed="true" stored="false"/>
   <field name="text3" type="text_offsets" indexed="true" stored="true"         large="true"/>
 
   <uniqueKey>id</uniqueKey>
diff --git a/solr/core/src/test/org/apache/solr/highlight/TestUnifiedSolrHighlighter.java b/solr/core/src/test/org/apache/solr/highlight/TestUnifiedSolrHighlighter.java
index 9d52314775d..67d475a8ebe 100644
--- a/solr/core/src/test/org/apache/solr/highlight/TestUnifiedSolrHighlighter.java
+++ b/solr/core/src/test/org/apache/solr/highlight/TestUnifiedSolrHighlighter.java
@@ -571,6 +571,310 @@ public class TestUnifiedSolrHighlighter extends SolrTestCaseJ4 {
         "count(//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/*)=0");
   }
 
+  public void testRequireFieldMatchWithQueryFieldPattern() {
+    // without requiring of a field match
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text,text2,text3",
+            "hl.queryFieldPattern", "text,text2,text3",
+            "hl.requireFieldMatch", "false",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text']/str='second <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text2']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text2']/str='second <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> <em>document</em>'");
+    // with requiring of a field match
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text,text2,text3",
+            "hl.queryFieldPattern", "text,text2,text3",
+            "hl.requireFieldMatch", "true",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text']/str='second <em>document</em>'",
+        "count(//lst[@name='highlighting']/lst[@name='101']/arr[@name='text2']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/arr[@name='text2']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/*)=0",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> document'");
+  }
+
+  public void testQueryFieldPatternIndexedNotStored() {
+
+    // highlighting on text3 uses all query terms
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2_indexed_not_stored:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> <em>document</em>'");
+
+    // hl.queryFieldPattern==text,text2 uses only some of the query terms
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2_indexed_not_stored:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text,text2",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='crappier <em>document</em>'");
+
+    // hl.queryFieldPattern==text,text2_indexed_not_stored uses only some of the query terms
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2_indexed_not_stored:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text,text2_indexed_not_stored",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='crappy <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> <em>document</em>'");
+
+    // hl.queryFieldPattern==text2* uses only some of the query terms
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2_indexed_not_stored:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text2*",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> document'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> document'");
+  }
+
+  public void testQueryFieldPatternMinimal() {
+
+    // highlighting on text3 uses all query terms
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> <em>document</em>'");
+
+    // hl.requireFieldMatch==true produces no highlights since text3 is not in the query
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.requireFieldMatch", "true",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+
+    // hl.queryFieldPattern==text uses only some of the query terms
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text2:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='crappy <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='crappier <em>document</em>'");
+  }
+
+  public void testQueryFieldPatternComprehensive() {
+
+    // searching on 'text' field and highlighting on 'text' field
+    assertQ(
+        req(
+            "q", "text:document",
+            "hl", "true",
+            "hl.fl", "text",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text']/str='second <em>document</em>'");
+
+    // searching on three fields ('text', 'text2', 'text3') but highlighting only on one of them
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text']/str='second <em>document</em>'");
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text2",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text2']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text2']/str='second <em>document</em>'");
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> <em>document</em>'");
+
+    // searching on three fields ('text', 'text2', 'text3') but highlighting only on one of them,
+    // requiring field match
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text",
+            "hl.requireFieldMatch", "true",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text']/str='second <em>document</em>'");
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text2",
+            "hl.requireFieldMatch", "true",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.requireFieldMatch", "true",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='<em>crappier</em> document'");
+
+    // searching on three fields ('text', 'text2', 'text3') but highlighting only on one of them
+    // field to match one of the three fields (not the one being highlighted on)
+
+    // highlight text matching text2
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text",
+            "hl.queryFieldPattern", "text2",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+    // highlight text matching text3
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text",
+            "hl.queryFieldPattern", "text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+
+    // highlight text2 matching text
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text2",
+            "hl.queryFieldPattern", "text",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text2']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text2']/str='second <em>document</em>'");
+    // highlight text2 matching text3
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text2",
+            "hl.queryFieldPattern", "text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+
+    // highlight text3 matching text
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='crappy <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='crappier <em>document</em>'");
+    // highlight text3 matching text2
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text2",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> document'",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+
+    // searching on three fields ('text', 'text2', 'text3') but highlighting only on one of them
+    // field to match two of the three fields (not the one being highlighted on)
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text",
+            "hl.queryFieldPattern", "text2,text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "count(//lst[@name='highlighting']/lst[@name='101']/*)=0",
+        "count(//lst[@name='highlighting']/lst[@name='102']/*)=0");
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text2",
+            "hl.queryFieldPattern", "text,text3",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text2']/str='<em>document</em> one'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text2']/str='second <em>document</em>'");
+    assertQ(
+        req(
+            "q", "text:document OR text2:crappy OR text3:crappier",
+            "hl", "true",
+            "hl.fl", "text3",
+            "hl.queryFieldPattern", "text,text2",
+            "sort", "id asc"),
+        "count(//lst[@name='highlighting']/*)=2",
+        "//lst[@name='highlighting']/lst[@name='101']/arr[@name='text3']/str='<em>crappy</em> <em>document</em>'",
+        "//lst[@name='highlighting']/lst[@name='102']/arr[@name='text3']/str='crappier <em>document</em>'");
+  }
+
   public void testWeightMatchesDisabled() {
     clearIndex();
     assertU(adoc("text", "alpha bravo charlie", "id", "101"));
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/highlighting.adoc b/solr/solr-ref-guide/modules/query-guide/pages/highlighting.adoc
index 88f561d30f8..6077827b224 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/highlighting.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/highlighting.adoc
@@ -116,6 +116,19 @@ If set to `true`, only query terms aligning with the field being highlighted wil
 If the query references fields different from the field being highlighted and they have different text analysis, the query may not highlight query terms it should have and vice versa.
 The analysis used is that of the field being highlighted (`hl.fl`), not the query fields.
 
+`hl.queryFieldPattern`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `none`
+|===
++
+Similar to `hl.requireFieldMatch` but allows for multiple fields to match e.g. `q=fieldA:one OR fieldB:two OR fieldC:three` `hl.fl=fieldA` `hl.queryFieldPattern=fieldA,fieldB`
++
+Also allows for the `hl.fl` field to be absent in the query e.g. `q=fieldA:one OR fieldB:two` `hl.fl=fieldZ` `hl.queryFieldPattern=fieldA`
++
+If a `hl.queryFieldPattern` and `hl.requireFieldMatch=true` are both specified then the `hl.queryFieldPattern` is silently ignored.
+
 `hl.usePhraseHighlighter`::
 +
 [%autowidth,frame=none]
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/HighlightParams.java b/solr/solrj/src/java/org/apache/solr/common/params/HighlightParams.java
index 37c70dc3047..e0ce04b08f2 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/HighlightParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/HighlightParams.java
@@ -35,6 +35,7 @@ public interface HighlightParams {
   public static final String Q = HIGHLIGHT + ".q"; // all
   public static final String QPARSER = HIGHLIGHT + ".qparser"; // all
   public static final String FIELD_MATCH = HIGHLIGHT + ".requireFieldMatch"; // OH, FVH, UH
+  public static final String QUERY_FIELD_PATTERN = HIGHLIGHT + ".queryFieldPattern"; // UH
   public static final String USE_PHRASE_HIGHLIGHTER =
       HIGHLIGHT + ".usePhraseHighlighter"; // OH, FVH, UH
   public static final String HIGHLIGHT_MULTI_TERM = HIGHLIGHT + ".highlightMultiTerm"; // all