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

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

commit d7eb4fd25600619d765ab526f9ccb8d003fce8cf
Author: 정승한(Seunghan Jung)/Search Platform <[email protected]>
AuthorDate: Tue Sep 8 06:30:59 2026 +0900

    SOLR-17444: Add Passage Sorting Option in UnifiedSolrHighlighter (#2697)
    
    The UnifiedHighlighter now supports an hl.passageSort option to control 
passage ordering within a document by startOffset, endOffset, or score.
    
    Co-authored-by: David Smiley <[email protected]>
    Co-authored-by: Claude Sonnet 5 <[email protected]>
    (cherry picked from commit d6dab5dd46c588931ad58fff1284572dc847d203)
---
 changelog/unreleased/SOLR-17444-passage-sort.yml   |  8 ++++
 .../solr/highlight/UnifiedSolrHighlighter.java     | 22 +++++++++
 .../solr/highlight/TestUnifiedSolrHighlighter.java | 55 ++++++++++++++++++++++
 .../modules/query-guide/pages/highlighting.adoc    | 10 ++++
 .../apache/solr/common/params/HighlightParams.java |  1 +
 5 files changed, 96 insertions(+)

diff --git a/changelog/unreleased/SOLR-17444-passage-sort.yml 
b/changelog/unreleased/SOLR-17444-passage-sort.yml
new file mode 100644
index 00000000000..07f1f67505e
--- /dev/null
+++ b/changelog/unreleased/SOLR-17444-passage-sort.yml
@@ -0,0 +1,8 @@
+title: The UnifiedHighlighter now supports an hl.passageSort option to control 
passage ordering within a document by startOffset, endOffset, or score.
+type: added
+authors:
+  - name: Seunghan Jung
+    nick: Seunghan-Jung
+links:
+  - name: SOLR-17444
+    url: https://issues.apache.org/jira/browse/SOLR-17444
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 3ae334db8b5..e1314790197 100644
--- a/solr/core/src/java/org/apache/solr/highlight/UnifiedSolrHighlighter.java
+++ b/solr/core/src/java/org/apache/solr/highlight/UnifiedSolrHighlighter.java
@@ -19,6 +19,7 @@ package org.apache.solr.highlight;
 import java.io.IOException;
 import java.text.BreakIterator;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
@@ -32,6 +33,7 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.search.uhighlight.CustomSeparatorBreakIterator;
 import org.apache.lucene.search.uhighlight.DefaultPassageFormatter;
 import org.apache.lucene.search.uhighlight.LengthGoalBreakIterator;
+import org.apache.lucene.search.uhighlight.Passage;
 import org.apache.lucene.search.uhighlight.PassageFormatter;
 import org.apache.lucene.search.uhighlight.PassageScorer;
 import org.apache.lucene.search.uhighlight.UnifiedHighlighter;
@@ -313,6 +315,26 @@ public class UnifiedSolrHighlighter extends 
SolrHighlighter implements PluginInf
       return new DefaultPassageFormatter(preTag, postTag, ellipsis, 
"html".equals(encoder));
     }
 
+    @Override
+    protected Comparator<Passage> getPassageSortComparator(String fieldName) {
+      String passageSort = params.getFieldParam(fieldName, 
HighlightParams.PASSAGE_SORT);
+      if (passageSort == null) {
+        return super.getPassageSortComparator(fieldName);
+      }
+      switch (passageSort) {
+        case "startOffset":
+          return Comparator.comparingInt(Passage::getStartOffset);
+        case "endOffset":
+          return Comparator.comparingInt(Passage::getEndOffset);
+        case "score":
+          return Comparator.comparingDouble(Passage::getScore).reversed();
+        default:
+          throw new SolrException(
+              SolrException.ErrorCode.BAD_REQUEST,
+              "Invalid " + HighlightParams.PASSAGE_SORT + " value: '" + 
passageSort + "'");
+      }
+    }
+
     @Override
     protected PassageScorer getScorer(String fieldName) {
       float k1 = params.getFieldFloat(fieldName, HighlightParams.SCORE_K1, 
1.2f);
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 fe903ed2475..4b0d2a7b4ba 100644
--- 
a/solr/core/src/test/org/apache/solr/highlight/TestUnifiedSolrHighlighter.java
+++ 
b/solr/core/src/test/org/apache/solr/highlight/TestUnifiedSolrHighlighter.java
@@ -17,6 +17,7 @@
 package org.apache.solr.highlight;
 
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.schema.IndexSchema;
 import org.junit.BeforeClass;
@@ -135,6 +136,60 @@ public class TestUnifiedSolrHighlighter extends 
SolrTestCaseJ4 {
         
"//lst[@name='highlighting']/lst[@name='101']/arr/str[2]='<em>Document</em> 
snippet two.'");
   }
 
+  public void testPassageSort() {
+    clearIndex();
+    // The last sentence scores highest (two matches) but comes last by offset.
+    assertU(
+        adoc(
+            "text",
+            "Document here. Nothing to see in this filler sentence. Document 
plus document again.",
+            "id",
+            "101"));
+    assertU(commit());
+    String arr = "//lst[@name='highlighting']/lst[@name='101']/arr/str";
+    String early = "'<em>Document</em> here. '";
+    String late = "'<em>Document</em> plus <em>document</em> again.'";
+
+    // Default (no hl.passageSort) sorts by startOffset: the earlier passage 
comes first.
+    assertQ(
+        req(
+            "q",
+            "text:document",
+            "hl",
+            "true",
+            "hl.snippets",
+            "2",
+            "hl.bs.type",
+            "SENTENCE",
+            "hl.fragsize",
+            "-1"),
+        arr + "[1]=" + early,
+        arr + "[2]=" + late);
+    // hl.passageSort=score puts the highest-scoring passage first.
+    assertQ(
+        req(
+            "q",
+            "text:document",
+            "hl",
+            "true",
+            "hl.snippets",
+            "2",
+            "hl.bs.type",
+            "SENTENCE",
+            "hl.fragsize",
+            "-1",
+            "hl.passageSort",
+            "score"),
+        arr + "[1]=" + late,
+        arr + "[2]=" + early);
+
+    SolrException e =
+        expectThrows(
+            SolrException.class,
+            () -> h.query(req("q", "text:document", "hl", "true", 
"hl.passageSort", "bogus")));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
+  }
+
   public void testStrictPhrasesEnabledByDefault() {
     clearIndex();
     assertU(
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 e857c1ff476..5b51f940050 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/highlighting.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/highlighting.adoc
@@ -509,6 +509,16 @@ Furthermore, phrases will be highlighted as a whole 
instead of word by word.  Cu
 +
 If either `hl.usePhraseHighlighter` or `hl.multiTermQuery` are set to `false`, 
then this setting is effectively `false` no matter what you set it to.
 
+`hl.passageSort`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `startOffset`
+|===
++
+The UH scores each passage to select the best `hl.snippets` candidates; this 
option then controls the order in which those selected passages are returned.
+The choices are `startOffset` (the passage's position in the field, the 
default), `endOffset`, and `score` (highest-scoring passage first).
+
 == Original Highlighter
 
 The Original Highlighter supports these following additional parameters to the 
ones listed earlier:
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 42625a605d3..030375ecf07 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
@@ -84,6 +84,7 @@ public interface HighlightParams {
   String SCORE_K1 = HIGHLIGHT + "." + SCORE + ".k1"; // UH
   String SCORE_B = HIGHLIGHT + "." + SCORE + ".b"; // UH
   String SCORE_PIVOT = HIGHLIGHT + "." + SCORE + ".pivot"; // UH
+  String PASSAGE_SORT = HIGHLIGHT + ".passageSort"; // UH
 
   // misc
   String MAX_CHARS = HIGHLIGHT + ".maxAnalyzedChars"; // all

Reply via email to