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 d6dab5dd46c SOLR-17444: Add Passage Sorting Option in
UnifiedSolrHighlighter (#2697)
d6dab5dd46c is described below
commit d6dab5dd46c588931ad58fff1284572dc847d203
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]>
---
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