diff --git a/changelog/unreleased/SOLR-18093-add-top-level-queries-in-json-solrj.yml b/changelog/unreleased/SOLR-18093-add-top-level-queries-in-json-solrj.yml new file mode 100644 index 000000000000..0c91f5aa11be --- /dev/null +++ b/changelog/unreleased/SOLR-18093-add-top-level-queries-in-json-solrj.yml @@ -0,0 +1,9 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: Add top-level "queries" support to JsonQueryRequest in SolrJ +type: added +authors: + - name: Sonu Sharma + nick: ercsonusharma +links: + - name: SOLR-18093 + url: https://issues.apache.org/jira/browse/SOLR-18093 diff --git a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java index 58b4596e7064..3897d6c737e0 100644 --- a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java @@ -274,6 +274,10 @@ private void initComponents() { DebugComponent dbgCmp = null; for (String c : list) { SearchComponent comp = core.getSearchComponent(c); + if (comp == null) { + throw new SolrException( + SolrException.ErrorCode.SERVER_ERROR, "Unknown search component: " + c); + } if (comp instanceof DebugComponent && makeDebugLast == true) { dbgCmp = (DebugComponent) comp; } else { diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml index 9efa5d4a29c0..6a960666848c 100644 --- a/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml +++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig.xml @@ -42,7 +42,7 @@ 1000000 2000000 3000000 - 4000000 + 4000000 @@ -380,6 +380,9 @@ + + + diff --git a/solr/core/src/test/org/apache/solr/handler/component/CombinedQuerySolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/component/CombinedQuerySolrCloudTest.java index 448ced51254a..4f5f31d80063 100644 --- a/solr/core/src/test/org/apache/solr/handler/component/CombinedQuerySolrCloudTest.java +++ b/solr/core/src/test/org/apache/solr/handler/component/CombinedQuerySolrCloudTest.java @@ -73,7 +73,7 @@ private synchronized void prepareIndexDocsColocated() throws Exception { del("*:*"); List docs = getSolrDocuments(); for (SolrInputDocument doc : docs) { - doc.setField("id", doc.getFieldValue("mod3_idv") + "!" + doc.getField("id").getValue()); + doc.setField("id", "CO!" + doc.getField("id").getValue()); } for (SolrInputDocument doc : docs) { indexDoc(doc); @@ -284,16 +284,16 @@ public void testQueriesWithFacetAndHighlightsCollapse() throws Exception { "queries": { "lexical1": { "lucene": { - "query": "id:(2!2^2 OR 0!3^1 OR 0!6^2 OR 2!5^1)" + "query": "id:(CO!2^3 OR CO!3^1 OR CO!6^2 OR CO!5^1)" } }, "lexical2": { "lucene": { - "query": "id:(2!8^1 OR 2!5^2 OR 1!7^3 OR 1!10^2)" + "query": "id:(CO!8^1 OR CO!5^2 OR CO!7^3 OR CO!10^2)" } } }, - "limit": 1, + "limit": 3, "fields": [ "id", "score", @@ -304,7 +304,7 @@ public void testQueriesWithFacetAndHighlightsCollapse() throws Exception { "facet": true, "facet.field": "id", "fq": [ - "{!collapse field=mod3_idv sort='id asc'}" + "{!collapse field=mod3_idv sort='id asc, score desc'}" ], "expand": true, "expand.q": "*:*", @@ -319,17 +319,17 @@ public void testQueriesWithFacetAndHighlightsCollapse() throws Exception { }"""; handle.put("expanded", UNORDERED); QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search"); - assertEquals(1, rsp.getResults().size()); - assertFieldValues(rsp.getResults(), id, "2!2"); + assertEquals(3, rsp.getResults().size()); + assertFieldValues(rsp.getResults(), id, "CO!2", "CO!10", "CO!3"); assertEquals("id", rsp.getFacetFields().getFirst().getName()); assertEquals( - "[0!3 (1), 1!10 (1), 2!2 (1), 0!6 (0), 0!9 (0), 1!1 (0), 1!4 (0), 1!7 (0), 2!5 (0), 2!8 (0)]", + "[CO!10 (1), CO!2 (1), CO!3 (1), CO!1 (0), CO!4 (0), CO!5 (0), CO!6 (0), CO!7 (0), CO!8 (0), CO!9 (0)]", rsp.getFacetFields().getFirst().getValues().toString()); - assertEquals(1, rsp.getHighlighting().size()); + assertEquals(3, rsp.getHighlighting().size()); assertEquals( "title test for doc 2", - rsp.getHighlighting().get("2!2").get("title").getFirst()); - assertEquals(1, rsp.getExpandedResults().size()); + rsp.getHighlighting().get("CO!2").get("title").getFirst()); + assertEquals(3, rsp.getExpandedResults().size()); } /** To test that we can force distrib */ diff --git a/solr/server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml b/solr/server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml index a6214acf2ee6..e70e689b1a51 100644 --- a/solr/server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml +++ b/solr/server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml @@ -640,7 +640,14 @@ - + + + + + 2 + + + text diff --git a/solr/solr-ref-guide/modules/query-guide/examples/JsonRequestApiTest.java b/solr/solr-ref-guide/modules/query-guide/examples/JsonRequestApiTest.java index 14aac24c4e7b..16f60ea2dadd 100644 --- a/solr/solr-ref-guide/modules/query-guide/examples/JsonRequestApiTest.java +++ b/solr/solr-ref-guide/modules/query-guide/examples/JsonRequestApiTest.java @@ -83,6 +83,76 @@ public void testSimpleJsonQuery() throws Exception { assertResponseFoundNumDocs(queryResponse, expectedResults); } + /** + * Test json query behaviour in case of multiple query to be executed using Combined Query. + * + * @throws Exception the exception + */ + @Test + public void testSimpleJsonQueryWithQueriesParams() throws Exception { + SolrClient solrClient = cluster.getSolrClient(); + final int expectedResults = 2; + final Map queriesMap = new HashMap<>(); + queriesMap.put( + "query1", + Map.of( + "lucene", + Map.of( + "query", "apache", + "df", "manu"))); + queriesMap.put( + "query2", + Map.of( + "edismax", + Map.of( + "query", "solr", + "df", "name"))); + final JsonQueryRequest query = + new JsonQueryRequest() + .setQueries(queriesMap) + .withFilter("inStock:true") + .withParam("fl", "name") + .withParam("combiner", "true") + .withParam("combiner.query", List.of("query1", "query2")); + query.setPath("/rrf"); + QueryResponse queryResponse = query.process(solrClient, COLLECTION_NAME); + assertResponseFoundNumDocs(queryResponse, expectedResults); + } + + /** + * Test json query behaviour in case of multiple query to be executed using Additional Queries. + * + * @throws Exception the exception + */ + @Test + public void testAdditionalJsonQueries() throws Exception { + SolrClient solrClient = cluster.getSolrClient(); + final int expectedResults = 12; + // tag::solrj-json-query-with-queries[] + final Map queriesMap = new HashMap<>(); + queriesMap.put( + "electronic", + Map.of( + "field", + Map.of( + "query", "electronics", + "f", "cat"))); + queriesMap.put( + "manufacturers", + List.of( + "manu: apple", + Map.of( + "field", + Map.of( + "query", "belkin", + "f", "manu")))); + final JsonQueryRequest query = + new JsonQueryRequest().setQueries(queriesMap).setQuery(Map.of("param", "electronic")); + QueryResponse queryResponse = query.process(solrClient, COLLECTION_NAME); + // end::solrj-json-query-with-queries[] + assertEquals(expectedResults, queryResponse.getResults().getNumFound()); + } + @Test public void testJsonQueryWithJsonQueryParamOverrides() throws Exception { SolrClient solrClient = cluster.getSolrClient(); diff --git a/solr/solr-ref-guide/modules/query-guide/pages/json-combined-query-dsl.adoc b/solr/solr-ref-guide/modules/query-guide/pages/json-combined-query-dsl.adoc index 0af349c4e80d..de62c7a6d1b6 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/json-combined-query-dsl.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/json-combined-query-dsl.adoc @@ -78,14 +78,21 @@ The query structure is similar to JSON Query DSL except for how multiple queries === Example -Below is a sample JSON query payload: +Below is an example with sample JSON query payload: -``` +[tabs#json-query-with-queries] +====== +curl:: ++ +==== +[source,bash] +---- +curl -X POST http://localhost:8983/solr/techproducts/query -d ' { "queries": { "lexical1": { "lucene": { - "query": "title:sales" + "query": "name:apache" } }, "vector": { @@ -97,15 +104,37 @@ Below is a sample JSON query payload: } }, "limit": 5, - "fields": ["id", "score", "title"], + "fields": ["id", "score", "name"], "params": { "combiner": true, "combiner.query": ["lexical1", "vector"], "combiner.algorithm": "rrf", "combiner.rrf.k": "15" } -} -``` +}' +---- +==== +SolrJ:: ++ +==== +[source,java,indent=0] +---- +final Map queriesMap = new HashMap<>(); +queriesMap.put("lexical1", Map.of("lucene", Map.of("query", "apache", "df", "name"))); +queriesMap.put("vector", Map.of("knn", Map.of("query", [0.1,-0.34,0.89,0.02], "f", "vector", "topK", 5))); +final JsonQueryRequest query = + new JsonQueryRequest() + .setQueries(queriesMap) + .withParam("id", "score", "name") + .setLimit(5) + .withParam("combiner", "true") + .withParam("combiner.query", List.of("lexical1", "vector")) + .withParam("combiner.algorithm", "rrf") + .withParam("combiner.rrf.k", "15"); +QueryResponse queryResponse = query.process(solrClient, COLLECTION_NAME); +---- +==== +====== == Combiner Algorithm Plugin diff --git a/solr/solr-ref-guide/modules/query-guide/pages/json-query-dsl.adoc b/solr/solr-ref-guide/modules/query-guide/pages/json-query-dsl.adoc index 104fba52eec2..a52300aac344 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/json-query-dsl.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/json-query-dsl.adoc @@ -388,6 +388,11 @@ Beware of arity for these references. Depending on the context, a reference might be resolved into the first element in the array ignoring the later elements, e.g., if one changes the reference below from `{"param":"electronic"}` to `{"param":"manufacturers"}`, it's equivalent to querying for `manu:apple`, ignoring the later query. These queries don't impact the query result until explicit referencing. +[tabs#json-additional-queries] +====== +curl:: ++ +==== [source,bash] ---- curl -X POST http://localhost:8983/solr/techproducts/query -d ' @@ -402,6 +407,16 @@ curl -X POST http://localhost:8983/solr/techproducts/query -d ' "query":{"param":"electronic"} }' ---- +==== +SolrJ:: ++ +==== +[source,java,indent=0] +---- +include::example$JsonRequestApiTest.java[tag=solrj-json-query-with-queries] +---- +==== +====== Overall this example doesn't make much sense, but just demonstrates the syntax. This feature is useful in xref:json-faceting-domain-changes.adoc#adding-domain-filters[filtering domain] in JSON Facet API xref:json-facet-api.adoc#changing-the-domain[domain changes]. diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java index 421edc72bc83..29ef7d845394 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java @@ -101,6 +101,26 @@ public JsonQueryRequest setQuery(Map queryJson) { return this; } + /** + * Specify the queries parameter sent as a part of JSON request. + * + *

This method would be helpful in setting the queries parameter specially for Combined Query + * Component {@code CombinedQueryComponent} use case. + * + *

Example: You wish to send the JSON request: + * + *

{@code {'limit': 5, 'queries': {'query1': {'lucene':
+   * {'df':'genre_s', 'query': 'scifi'}}}, 'query2': {'knn': {'f': 'vector', 'query': [0.1, 0.43]}}}}
+   * 
+ * + * @param queriesJson a Map of values representing the query subtree of the JSON request you wish + * to send. + */ + public JsonQueryRequest setQueries(Map queriesJson) { + jsonRequestMap.put("queries", queriesJson); + return this; + } + /** * Specify the query sent as a part of this JSON request. * diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java index dc516e00f3ab..b43310e8fe17 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.solr.client.solrj.request.AbstractUpdateRequest; import org.apache.solr.client.solrj.request.CollectionAdminRequest; @@ -45,6 +46,7 @@ public class JsonQueryRequestIntegrationTest extends SolrCloudTestCase { private static final int NUM_SCIFI_BOOKS = 2; private static final int NUM_IN_STOCK = 8; private static final int NUM_IN_STOCK_AND_FIRST_IN_SERIES = 5; + private static final int NUM_MARTIN_BOOKS = 3; @BeforeClass public static void setupCluster() throws Exception { @@ -88,6 +90,29 @@ public void testQueriesCanUseLocalParamsSyntax() throws Exception { assertEquals(NUM_SCIFI_BOOKS, queryResponse.getResults().getNumFound()); } + /** + * Test multiple queries can use local params syntax with Combined Query Component. + * + * @throws Exception the exception + */ + @Test + public void testMultipleQueriesCanUseLocalParamsSyntax() throws Exception { + final Map queriesMap = new HashMap<>(); + queriesMap.put("query1", "{!lucene df=genre_s v='scifi'}"); + queriesMap.put("query2", "{!edismax df=author_t v='martin'}"); + final JsonQueryRequest query = + new JsonQueryRequest() + .setQueries(queriesMap) + .withFilter("inStock:true") + .withParam("fl", "name") + .withParam("combiner", "true") + .withParam("combiner.query", List.of("query1", "query2")); + query.setPath("/rrf"); + QueryResponse queryResponse = query.process(cluster.getSolrClient(), COLLECTION_NAME); + assertEquals(0, queryResponse.getStatus()); + assertEquals(NUM_SCIFI_BOOKS + NUM_MARTIN_BOOKS, queryResponse.getResults().size()); + } + @Test public void testQueriesCanUseExpandedSyntax() throws Exception { // Construct a tree representing the JSON: {lucene: {df:'genre_s', 'query': 'scifi'}} diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java index 6297c56cd6c0..51a407ce5265 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java @@ -88,6 +88,29 @@ public void testWritesProvidedQueryMapToJsonCorrectly() { requestBody, containsString("\"query\":{\"lucene\":{\"q\":\"*:*\",\"df\":\"text\"}}")); } + @Test + public void testWritesProvidedQueriesMapToJsonCorrectly() { + final Map queriesMap = new HashMap<>(); + queriesMap.put( + "query1", + Map.of( + "lucene", + Map.of( + "query", "*:*", + "df", "text"))); + queriesMap.put( + "query2", + Map.of( + "edismax", + Map.of( + "query", "solr", + "df", "text"))); + final JsonQueryRequest request = new JsonQueryRequest().setQueries(queriesMap); + final String requestBody = writeRequestToJson(request); + assertThat(requestBody, containsString("\"query1\":{\"lucene\":")); + assertThat(requestBody, containsString("\"query2\":{\"edismax\":")); + } + @Test public void testWritesProvidedQueryMapWriterToJsonCorrectly() { final MapWriter queryWriter =