Skip to content

Commit 947d9dd

Browse files
SOLR-18271: QueryElevation support for Combined Query feature (#4476)
Adds QueryElevationComponent support to the Combined Query feature, so that elevated docIds (via elevateIds / configured elevations) are honored when results from multiple subqueries are combined and merged across shards.
1 parent 8c49326 commit 947d9dd

6 files changed

Lines changed: 190 additions & 33 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
title: Support for using Query Elevation with CombinedQueryComponent (RRF)
2+
type: added
3+
authors:
4+
- name: Sonu Sharma
5+
nick: ercsonusharma
6+
links:
7+
- name: SOLR-18271
8+
url: https://issues.apache.org/jira/browse/SOLR-18271

solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ public void prepare(ResponseBuilder rb) throws IOException {
151151
final var unparsedQuery = params.get(queryKey);
152152
ResponseBuilder rbNew = new ResponseBuilder(rb.req, new SolrQueryResponse(), rb.components);
153153
rbNew.setQueryString(unparsedQuery);
154+
rbNew.setDebug(rb.isDebug());
154155
super.prepare(rbNew);
155156
crb.setFilters(rbNew.getFilters());
156157
crb.responseBuilders.add(rbNew);

solr/core/src/java/org/apache/solr/handler/component/QueryElevationComponent.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public class QueryElevationComponent extends SearchComponent implements SolrCore
112112
@VisibleForTesting static final String FIELD_TYPE = "queryFieldType";
113113
@VisibleForTesting static final String CONFIG_FILE = "config-file";
114114
private static final String EXCLUDE = "exclude";
115+
private static final String DEBUG_QUERY_BOOSTING = "queryBoosting";
115116

116117
/**
117118
* @see #getBoostDocs(SolrIndexSearcher, Set, Map)
@@ -488,6 +489,45 @@ public void prepare(ResponseBuilder rb) throws IOException {
488489
return;
489490
}
490491

492+
if (rb instanceof CombinedQueryResponseBuilder crb) {
493+
prepareCombined(crb);
494+
} else {
495+
prepareElevationComponent(rb);
496+
}
497+
}
498+
499+
/**
500+
* Elevates each subquery and mirrors the resulting SortSpec/filters onto the parent crb so {@link
501+
* CombinedQueryComponent#mergeIds} can read {@code _elevate_} from each shard's {@code
502+
* sort_values_i} during distributed merge.
503+
*/
504+
private void prepareCombined(CombinedQueryResponseBuilder crb) throws IOException {
505+
if (crb.responseBuilders.isEmpty()) {
506+
return;
507+
}
508+
for (ResponseBuilder thisRb : crb.responseBuilders) {
509+
prepareElevationComponent(thisRb);
510+
}
511+
// Subqueries get identical elevation treatment, so any one is representative.
512+
ResponseBuilder representative = crb.responseBuilders.getFirst();
513+
crb.setSortSpec(representative.getSortSpec());
514+
crb.setFilters(representative.getFilters());
515+
516+
if (crb.isDebug() && crb.isDebugQuery()) {
517+
List<Object> debugPerSubquery = new ArrayList<>(crb.responseBuilders.size());
518+
for (ResponseBuilder thisRb : crb.responseBuilders) {
519+
Object queryBoosting = thisRb.getDebugInfo().get(DEBUG_QUERY_BOOSTING);
520+
if (queryBoosting != null) {
521+
debugPerSubquery.add(queryBoosting);
522+
}
523+
}
524+
if (!debugPerSubquery.isEmpty()) {
525+
crb.addDebugInfo(DEBUG_QUERY_BOOSTING, debugPerSubquery);
526+
}
527+
}
528+
}
529+
530+
private void prepareElevationComponent(ResponseBuilder rb) throws IOException {
491531
Elevation elevation = getElevation(rb);
492532
if (elevation != null) {
493533
setQuery(rb, elevation);
@@ -767,7 +807,7 @@ private void addDebugInfo(ResponseBuilder rb, Elevation elevation) {
767807
SimpleOrderedMap<Object> dbg = new SimpleOrderedMap<>();
768808
dbg.add("q", rb.getQueryString());
769809
dbg.add("match", match);
770-
rb.addDebugInfo("queryBoosting", dbg);
810+
rb.addDebugInfo(DEBUG_QUERY_BOOSTING, dbg);
771811
}
772812

773813
// ---------------------------------------------------------------------------------

solr/core/src/test-files/solr/collection1/conf/solrconfig-combined-query.xml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,18 @@
9898
<requestHandler name="/search" class="solr.CombinedQuerySearchHandler">
9999
</requestHandler>
100100

101-
<searchComponent class="solr.CombinedQueryComponent" name="combined_query">
101+
<requestHandler name="/search-elevate" class="solr.CombinedQuerySearchHandler">
102+
<arr name="last-components">
103+
<str>elevator</str>
104+
</arr>
105+
</requestHandler>
106+
107+
<searchComponent name="elevator" class="solr.QueryElevationComponent">>
108+
<str name="queryFieldType">string</str>
109+
<str name="config-file">elevate.xml</str>
110+
</searchComponent>
111+
112+
<searchComponent class="solr.CombinedQueryComponent" name="combined_query">
102113
<int name="maxCombinerQueries">2</int>
103114
<lst name="combiners">
104115
<lst name="test">
@@ -159,7 +170,7 @@
159170
</highlighting>
160171
</searchComponent>
161172

162-
<initParams path="/select,/search">
173+
<initParams path="/select,/search,/search-elevate">
163174
<lst name="defaults">
164175
<str name="df">text</str>
165176
</lst>

solr/core/src/test/org/apache/solr/handler/component/CombinedQuerySolrCloudTest.java

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
*/
1717
package org.apache.solr.handler.component;
1818

19+
import static org.apache.solr.cloud.AbstractZkTestCase.SOLRHOME;
20+
1921
import java.io.IOException;
2022
import java.util.ArrayList;
2123
import java.util.Arrays;
2224
import java.util.List;
2325
import org.apache.solr.client.solrj.response.QueryResponse;
2426
import org.apache.solr.cloud.AbstractFullDistribZkTestBase;
27+
import org.apache.solr.cloud.ZkTestServer;
2528
import org.apache.solr.common.SolrInputDocument;
2629
import org.apache.solr.common.params.CommonParams;
2730
import org.apache.solr.common.params.ShardParams;
@@ -49,6 +52,12 @@ public CombinedQuerySolrCloudTest() {
4952
configString = "solrconfig-combined-query.xml";
5053
}
5154

55+
@Override
56+
public void distribSetUp() throws Exception {
57+
super.distribSetUp();
58+
ZkTestServer.putConfig("conf1", zkServer.getZkClient(), SOLRHOME, "elevate.xml");
59+
}
60+
5261
@Override
5362
protected String getCloudSchemaFile() {
5463
return "schema-vector-catchall.xml";
@@ -121,7 +130,7 @@ public void testSingleLexicalQuery() throws Exception {
121130
query(
122131
CommonParams.JSON,
123132
"{\"queries\":"
124-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:2^10\"}}},"
133+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:2^=10\"}}},"
125134
+ "\"limit\":5,"
126135
+ "\"fields\":[\"id\",\"score\",\"title\"],"
127136
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\"]}}",
@@ -141,8 +150,8 @@ public void testMultipleLexicalQuery() throws Exception {
141150
prepareIndexDocs();
142151
String jsonQuery =
143152
"{\"queries\":"
144-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
145-
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
153+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^=4 OR 3^=2 OR 6^=3 OR 5^=1)\"}},"
154+
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^=1 OR 5^=3 OR 7^=4 OR 10^=2)\"}}},"
146155
+ "\"limit\":5,"
147156
+ "\"fields\":[\"id\",\"score\",\"title\"],"
148157
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}";
@@ -161,8 +170,8 @@ public void testMultipleQueryWithSort() throws Exception {
161170
prepareIndexDocs();
162171
String jsonQuery =
163172
"{\"queries\":"
164-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
165-
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^1 OR 5^2 OR 7^3 OR 10^1)\"}}},"
173+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^=4 OR 3^=2 OR 6^=3 OR 5^=1)\"}},"
174+
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^=1 OR 5^=3 OR 7^=4 OR 10^=2)\"}}},"
166175
+ "\"limit\":5,\"sort\":\"mod3_idv desc, score desc\""
167176
+ "\"fields\":[\"id\",\"score\",\"title\"],"
168177
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}";
@@ -183,8 +192,8 @@ public void testHybridQueryWithPagination() throws Exception {
183192
query(
184193
CommonParams.JSON,
185194
"{\"queries\":"
186-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
187-
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
195+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^=4 OR 3^=2 OR 6^=3 OR 5^=1)\"}},"
196+
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^=1 OR 5^=3 OR 7^=4 OR 10^=2)\"}}},"
188197
+ "\"fields\":[\"id\",\"score\",\"title\"],"
189198
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
190199
CommonParams.QT,
@@ -194,8 +203,8 @@ public void testHybridQueryWithPagination() throws Exception {
194203
query(
195204
CommonParams.JSON,
196205
"{\"queries\":"
197-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
198-
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
206+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^=4 OR 3^=2 OR 6^=3 OR 5^=1)\"}},"
207+
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^=1 OR 5^=3 OR 7^=4 OR 10^=2)\"}}},"
199208
+ "\"limit\":4,"
200209
+ "\"fields\":[\"id\",\"score\",\"title\"],"
201210
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
@@ -206,8 +215,8 @@ public void testHybridQueryWithPagination() throws Exception {
206215
query(
207216
CommonParams.JSON,
208217
"{\"queries\":"
209-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
210-
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
218+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^=4 OR 3^=2 OR 6^=3 OR 5^=1)\"}},"
219+
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^=1 OR 5^=3 OR 7^=4 OR 10^=2)\"}}},"
211220
+ "\"limit\":4,\"offset\":3,"
212221
+ "\"fields\":[\"id\",\"score\",\"title\"],"
213222
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
@@ -227,7 +236,7 @@ public void testQueryWithFaceting() throws Exception {
227236
prepareIndexDocs();
228237
String jsonQuery =
229238
"{\"queries\":"
230-
+ "{\"lexical\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}}},"
239+
+ "{\"lexical\":{\"lucene\":{\"query\":\"id:(2^=2 OR 3^=1 OR 6^=2 OR 5^=1)\"}}},"
231240
+ "\"limit\":3,\"offset\":1"
232241
+ "\"fields\":[\"id\",\"score\",\"title\"],"
233242
+ "\"params\":{\"combiner\":true,\"facet\":true,\"facet.field\":\"mod3_idv\",\"facet.mincount\":1,"
@@ -248,8 +257,8 @@ public void testQueriesWithFacetAndHighlights() throws Exception {
248257
prepareIndexDocs();
249258
String jsonQuery =
250259
"{\"queries\":"
251-
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
252-
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
260+
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^=4 OR 3^=2 OR 6^=3 OR 5^=1)\"}},"
261+
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(8^=1 OR 5^=3 OR 7^=4 OR 10^=2)\"}}},"
253262
+ "\"limit\":4,"
254263
+ "\"fields\":[\"id\",\"score\",\"title\"],"
255264
+ "\"params\":{\"combiner\":true,\"facet\":true,\"facet.field\":\"mod3_idv\","
@@ -269,6 +278,48 @@ public void testQueriesWithFacetAndHighlights() throws Exception {
269278
rsp.getHighlighting().get("5").get("title").getFirst());
270279
}
271280

281+
/**
282+
* Tests the combined query feature with faceting, highlighting and elevation.
283+
*
284+
* @throws Exception if any unexpected error occurs during the test execution.
285+
*/
286+
@Test
287+
public void testElevatedQueriesWithFacetAndHighlights() throws Exception {
288+
prepareIndexDocs();
289+
String jsonQuery =
290+
"""
291+
{
292+
"queries": {
293+
"lexical1": {"lucene": {"query": "id:(2^=3 OR 3^=1 OR 6^=2 OR 5^=2)"}},
294+
"lexical2": {"lucene": {"query": "id:(4^=1 OR 5^=2 OR 7^=3 OR 10^=2)"}}
295+
},
296+
"limit": 4,
297+
"fields": ["id", "score", "title"],
298+
"params": {
299+
"combiner": true,
300+
"elevateIds": "6,10",
301+
"combiner.query": ["lexical1", "lexical2"],
302+
"facet": true,
303+
"facet.field": "mod3_idv",
304+
"hl": true,
305+
"hl.fl": "title",
306+
"hl.q": "test doc"
307+
}
308+
}""";
309+
QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search-elevate");
310+
assertEquals(4, rsp.getResults().size());
311+
assertFieldValues(rsp.getResults(), id, "6", "10", "5", "7");
312+
assertEquals("mod3_idv", rsp.getFacetFields().getFirst().getName());
313+
assertEquals("[1 (3), 0 (2), 2 (2)]", rsp.getFacetFields().getFirst().getValues().toString());
314+
assertEquals(4, rsp.getHighlighting().size());
315+
assertEquals(
316+
"title <em>test</em> for <em>doc</em> 10",
317+
rsp.getHighlighting().get("10").get("title").getFirst());
318+
assertEquals(
319+
"title <em>test</em> for <em>doc</em> 5",
320+
rsp.getHighlighting().get("5").get("title").getFirst());
321+
}
322+
272323
/**
273324
* Tests the combined query feature with faceting, highlighting and collapse.
274325
*
@@ -284,12 +335,12 @@ public void testQueriesWithFacetAndHighlightsCollapse() throws Exception {
284335
"queries": {
285336
"lexical1": {
286337
"lucene": {
287-
"query": "id:(CO!2^3 OR CO!3^1 OR CO!6^2 OR CO!5^1)"
338+
"query": "id:(CO!2^=3 OR CO!3^=1 OR CO!6^=2 OR CO!5^=1)"
288339
}
289340
},
290341
"lexical2": {
291342
"lucene": {
292-
"query": "id:(CO!8^1 OR CO!5^2 OR CO!7^3 OR CO!10^2)"
343+
"query": "id:(CO!8^=1 OR CO!5^=2 OR CO!7^=3 OR CO!10^=2)"
293344
}
294345
}
295346
},

0 commit comments

Comments
 (0)