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
