You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ds...@apache.org on 2026/06/06 19:44:19 UTC
(solr) branch branch_9x updated (ea61a995332 -> a3c387d1c64)
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a change to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
from ea61a995332 SOLR-18239: Fixed NPE during size estimator calculations (#4427)
new a2f523cf88b fix doc typo spacial→spatial (#4457)
new 1cbab3f3371 tests-via-crave: limit paths (#4449)
new bef0827290c SOLR-18227: Add name= local-param with MatchedQueriesComponent (#4402)
new 6601e58a76d build failed-tests-at-end: improve isSuite detection
new a9f697c28fe Test fix testDistributedQueryDoesNotReadFromZk (#4429)
new be771e3eb5f changes2html.py: fix for github nicknames with hyphens (#4481)
new 47f0eea91bb SOLR-18098: Fix replication failure for files with exact MB sizes (#4205)
new f76f061c0a9 SOLR-18098: Fix precommit (#4498)
new 7a002a5ba8a DistributedCombinedQueryComponentTest hangs in nightly (#4497)
new 21bf71b4962 JavaBinCodec: fix self-suppression risk (#4489)
new a3c387d1c64 HttpJettySolrClient: don't throw IllegalStateException if connection lost (#4490)
The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.github/workflows/tests-via-crave.yml | 5 +-
...0-HttpJettySolrClient-IllegalStateException.yml | 9 +
.../SOLR-18098-fix-replication-exact-mb-sizes.yml | 7 +
.../SOLR-18227_named_queries_matched_component.yml | 7 +
.../documentation/changes-to-html/changes2html.py | 2 +-
gradle/testing/failed-tests-at-end.gradle | 2 +-
.../solr/handler/admin/api/ReplicationAPIBase.java | 7 +
.../handler/component/MatchedQueriesComponent.java | 175 +++++++++
.../src/java/org/apache/solr/search/QParser.java | 9 +
.../java/org/apache/solr/search/QueryParsing.java | 1 +
...thandler.xml => solrconfig-matched-queries.xml} | 22 +-
.../handler/IndexFetcherPacketProtocolTest.java | 401 +++++++++++++++++++++
.../DistributedCombinedQueryComponentTest.java | 109 +++---
.../DistributedQueryComponentOptimizationTest.java | 8 +-
.../component/TestMatchedQueriesComponent.java | 301 ++++++++++++++++
.../org/apache/solr/search/QueryEqualityTest.java | 134 +++++++
.../solr/search/TestMmBoolQParserPlugin.java | 22 ++
.../modules/indexing-guide/pages/docvalues.adoc | 2 +-
.../query-guide/pages/common-query-parameters.adoc | 1 +
.../pages/matched-queries-component.adoc | 121 +++++++
.../modules/query-guide/pages/other-parsers.adoc | 25 +-
.../modules/query-guide/querying-nav.adoc | 1 +
.../solr/client/solrj/impl/Http2SolrClient.java | 4 +
.../org/apache/solr/common/util/JavaBinCodec.java | 5 +-
24 files changed, 1305 insertions(+), 75 deletions(-)
create mode 100644 changelog/unreleased/PR#4490-HttpJettySolrClient-IllegalStateException.yml
create mode 100644 changelog/unreleased/SOLR-18098-fix-replication-exact-mb-sizes.yml
create mode 100644 changelog/unreleased/SOLR-18227_named_queries_matched_component.yml
create mode 100644 solr/core/src/java/org/apache/solr/handler/component/MatchedQueriesComponent.java
copy solr/core/src/test-files/solr/collection1/conf/{solrconfig-withgethandler.xml => solrconfig-matched-queries.xml} (77%)
create mode 100644 solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java
create mode 100644 solr/core/src/test/org/apache/solr/handler/component/TestMatchedQueriesComponent.java
create mode 100644 solr/solr-ref-guide/modules/query-guide/pages/matched-queries-component.adoc
(solr) 10/11: JavaBinCodec: fix self-suppression risk (#4489)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 21bf71b49623db419b2e366b527f5edf44a3680e
Author: David Smiley <ds...@apache.org>
AuthorDate: Wed Jun 3 00:24:37 2026 -0400
JavaBinCodec: fix self-suppression risk (#4489)
(cherry picked from commit ffd773b8dcb778407dd34aece4cab7322f85dd4b)
---
solr/solrj/src/java/org/apache/solr/common/util/JavaBinCodec.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/JavaBinCodec.java b/solr/solrj/src/java/org/apache/solr/common/util/JavaBinCodec.java
index 7e4a96e50d9..da3dfcbe28f 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/JavaBinCodec.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/JavaBinCodec.java
@@ -1483,7 +1483,10 @@ public class JavaBinCodec implements PushWriter {
@Override
public void close() throws IOException {
- if (daos != null) {
+ // marshal() already flushes in its own finally block, so skip the redundant flush.
+ // Flushing again after a marshal failure would re-throw the same exception from the broken
+ // stream, causing "Self-suppression not permitted" in the caller's try-with-resources.
+ if (daos != null && !alreadyMarshalled) {
daos.flushBuffer();
}
}
(solr) 02/11: tests-via-crave: limit paths (#4449)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 1cbab3f3371bb456a00afd3220ecfab0c777c584
Author: David Smiley <ds...@apache.org>
AuthorDate: Tue May 19 16:40:46 2026 -0400
tests-via-crave: limit paths (#4449)
(cherry picked from commit b0dd510d0ac45e33ee798c7483c009b8557c7df4)
---
.github/workflows/tests-via-crave.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/tests-via-crave.yml b/.github/workflows/tests-via-crave.yml
index b8b7e0b9db5..1623ed29f33 100644
--- a/.github/workflows/tests-via-crave.yml
+++ b/.github/workflows/tests-via-crave.yml
@@ -1,9 +1,12 @@
-name: Solr Tests
+name: Solr Tests via Crave
on:
pull_request:
branches:
- '*'
+ paths:
+ - 'solr/**/src/**'
+ - 'gradle/libs.versions.toml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
(solr) 03/11: SOLR-18227: Add name= local-param with MatchedQueriesComponent (#4402)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit bef0827290c292f9008da1888c62cf85e28db910
Author: squallsama <sq...@gmail.com>
AuthorDate: Sat May 23 09:04:15 2026 -0400
SOLR-18227: Add name= local-param with MatchedQueriesComponent (#4402)
to identify which named sub-queries matched each document.
Uses org.apache.lucene.search.NamedMatches.
(cherry picked from commit d17c00ee37eaad6c2d84d368709802001037bae8)
---
.../SOLR-18227_named_queries_matched_component.yml | 7 +
.../handler/component/MatchedQueriesComponent.java | 175 ++++++++++++
.../src/java/org/apache/solr/search/QParser.java | 9 +
.../java/org/apache/solr/search/QueryParsing.java | 1 +
.../conf/solrconfig-matched-queries.xml | 56 ++++
.../component/TestMatchedQueriesComponent.java | 301 +++++++++++++++++++++
.../org/apache/solr/search/QueryEqualityTest.java | 134 +++++++++
.../solr/search/TestMmBoolQParserPlugin.java | 22 ++
.../query-guide/pages/common-query-parameters.adoc | 1 +
.../pages/matched-queries-component.adoc | 121 +++++++++
.../modules/query-guide/pages/other-parsers.adoc | 25 +-
.../modules/query-guide/querying-nav.adoc | 1 +
12 files changed, 850 insertions(+), 3 deletions(-)
diff --git a/changelog/unreleased/SOLR-18227_named_queries_matched_component.yml b/changelog/unreleased/SOLR-18227_named_queries_matched_component.yml
new file mode 100644
index 00000000000..84f43cb169d
--- /dev/null
+++ b/changelog/unreleased/SOLR-18227_named_queries_matched_component.yml
@@ -0,0 +1,7 @@
+title: "Add `name` local parameter support and `MatchedQueriesComponent` to identify which named sub-queries matched each document."
+type: added
+authors:
+ - name: Dmitrii Tikhonov
+links:
+ - name: SOLR-18227
+ url: https://issues.apache.org/jira/browse/SOLR-18227
diff --git a/solr/core/src/java/org/apache/solr/handler/component/MatchedQueriesComponent.java b/solr/core/src/java/org/apache/solr/handler/component/MatchedQueriesComponent.java
new file mode 100644
index 00000000000..56040116f9e
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/component/MatchedQueriesComponent.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import com.carrotsearch.hppc.IntObjectHashMap;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.ReaderUtil;
+import org.apache.lucene.search.Matches;
+import org.apache.lucene.search.NamedMatches;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Weight;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.search.DocIterator;
+import org.apache.solr.search.DocList;
+import org.apache.solr.search.SolrDocumentFetcher;
+import org.apache.solr.search.SolrIndexSearcher;
+
+/**
+ * Search component that enriches the response with named-match information for each document in the
+ * top-N hits.
+ *
+ * <p>Activation: Add {@code matched_queries=true} (or {@code mq=true}) to the request.
+ *
+ * <p>Output:
+ *
+ * <ul>
+ * <li>{@code matched_queries_per_hit}: map of unique-key value → list of names that matched that
+ * document. Documents that matched no named query are absent.
+ * <li>{@code matched_queries_summary}: map of name → ordered list of unique-key values of
+ * documents it matched.
+ * </ul>
+ *
+ * <p>Implementation: We use the {@link Weight#matches(LeafReaderContext, int)} API which performs a
+ * separate, post-search pass over each requested document. {@link NamedMatches} become identifiable
+ * through {@link NamedMatches#findNamedMatches(Matches)} on the returned Matches tree. {@link
+ * org.apache.lucene.search.ScoreMode#COMPLETE_NO_SCORES} is used for the matches Weight because
+ * matching does not need scoring and this lets Lucene skip score computation entirely for this
+ * pass.
+ */
+public class MatchedQueriesComponent extends SearchComponent {
+
+ public static final String COMPONENT_NAME = "matched_queries";
+ public static final String PARAM_ENABLE = "matched_queries";
+ public static final String PARAM_ENABLE_SHORT = "mq";
+
+ @Override
+ public void prepare(ResponseBuilder rb) {
+ // nothing to prepare
+ }
+
+ @Override
+ public void process(ResponseBuilder rb) throws IOException {
+ if (!isEnabled(rb)) {
+ return;
+ }
+
+ DocList docList = rb.getResults() == null ? null : rb.getResults().docList;
+ if (docList == null || docList.size() == 0) {
+ return;
+ }
+
+ Query query = rb.getQuery();
+ if (query == null) {
+ return;
+ }
+
+ SolrIndexSearcher searcher = rb.req.getSearcher();
+ String idField = searcher.getSchema().getUniqueKeyField().getName();
+
+ // Build a Weight for matching only (no scoring needed)
+ Query rewritten = searcher.rewrite(query);
+ Weight matchesWeight = searcher.createWeight(rewritten, ScoreMode.COMPLETE_NO_SCORES, 1.0f);
+
+ // Collect: per global doc id → ordered set of names
+ Map<Integer, Set<String>> perDocNames = new LinkedHashMap<>();
+ // Collect: per name → list of global doc ids (preserves document order)
+ Map<String, List<Integer>> perNameDocs = new LinkedHashMap<>();
+ IntObjectHashMap<String> idCache = new IntObjectHashMap<>(docList.size());
+
+ List<LeafReaderContext> leaves = searcher.getTopReaderContext().leaves();
+ SolrDocumentFetcher docFetcher = searcher.getDocFetcher();
+
+ DocIterator it = docList.iterator();
+ while (it.hasNext()) {
+ int globalDoc = it.nextDoc();
+
+ LeafReaderContext leaf = leaves.get(ReaderUtil.subIndex(globalDoc, leaves));
+ int leafDoc = globalDoc - leaf.docBase;
+
+ Matches matches = matchesWeight.matches(leaf, leafDoc);
+ if (matches == null) {
+ continue;
+ }
+ List<NamedMatches> named = NamedMatches.findNamedMatches(matches);
+ if (named.isEmpty()) {
+ continue;
+ }
+
+ Set<String> names = new LinkedHashSet<>();
+ for (NamedMatches nm : named) {
+ names.add(nm.getName());
+ }
+ perDocNames.put(globalDoc, names);
+ idCache.put(globalDoc, readUniqueKeyValue(docFetcher, idField, globalDoc));
+ for (String name : names) {
+ perNameDocs.computeIfAbsent(name, k -> new ArrayList<>()).add(globalDoc);
+ }
+ }
+
+ if (perDocNames.isEmpty()) {
+ return;
+ }
+
+ // Annotate each hit: we add a parallel structure (docId → matched names)
+ // because mutating SolrDocument inline requires DocTransformer plumbing.
+ // The hits-keyed map is keyed by the document's unique-key value (string)
+ // for client convenience.
+ SimpleOrderedMap<Object> perHit = new SimpleOrderedMap<>();
+ for (Map.Entry<Integer, Set<String>> e : perDocNames.entrySet()) {
+ perHit.add(idCache.get(e.getKey()), new ArrayList<>(e.getValue()));
+ }
+
+ // Summary: name → [id1, id2, ...]
+ SimpleOrderedMap<Object> summary = new SimpleOrderedMap<>();
+ for (Map.Entry<String, List<Integer>> e : perNameDocs.entrySet()) {
+ List<String> ids = new ArrayList<>(e.getValue().size());
+ for (Integer luceneId : e.getValue()) {
+ ids.add(idCache.get(luceneId));
+ }
+ summary.add(e.getKey(), ids);
+ }
+
+ NamedList<Object> response = rb.rsp.getValues();
+ response.add("matched_queries_per_hit", perHit);
+ response.add("matched_queries_summary", summary);
+ }
+
+ private String readUniqueKeyValue(SolrDocumentFetcher docFetcher, String idField, int globalDoc)
+ throws IOException {
+ return docFetcher.doc(globalDoc, Set.of(idField)).get(idField);
+ }
+
+ private boolean isEnabled(ResponseBuilder rb) {
+ var p = rb.req.getParams();
+ return p.getBool(PARAM_ENABLE, false) || p.getBool(PARAM_ENABLE_SHORT, false);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Adds NamedMatches information to query response";
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/search/QParser.java b/solr/core/src/java/org/apache/solr/search/QParser.java
index 1a292186542..bd674579992 100644
--- a/solr/core/src/java/org/apache/solr/search/QParser.java
+++ b/solr/core/src/java/org/apache/solr/search/QParser.java
@@ -25,6 +25,7 @@ import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.FunctionScoreQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.QueryValueSource;
+import org.apache.lucene.search.NamedMatches;
import org.apache.lucene.search.Query;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
@@ -194,6 +195,14 @@ public abstract class QParser {
query = parse();
if (localParams != null) {
+ // MUST come before extendedQuery() calls below: NamedMatches is not an ExtendedQuery,
+ // so wrapping must happen first so that extendedQuery() can wrap it in a WrappedQuery
+ // that preserves the cache/cost settings as the outermost layer.
+ String name = localParams.get(QueryParsing.NAME);
+ if (name != null && !name.isBlank() && query != null) {
+ query = NamedMatches.wrapQuery(name, query);
+ }
+
String cacheStr = localParams.get(CommonParams.CACHE);
if (cacheStr != null) {
if (CommonParams.FALSE.equals(cacheStr)) {
diff --git a/solr/core/src/java/org/apache/solr/search/QueryParsing.java b/solr/core/src/java/org/apache/solr/search/QueryParsing.java
index 2abe8b8f6d3..cfa2b8c28b4 100644
--- a/solr/core/src/java/org/apache/solr/search/QueryParsing.java
+++ b/solr/core/src/java/org/apache/solr/search/QueryParsing.java
@@ -53,6 +53,7 @@ public class QueryParsing {
public static final char LOCALPARAM_END = '}';
// true if the value was specified by the "v" param (i.e. v=myval, or v=$param)
public static final String VAL_EXPLICIT = "__VAL_EXPLICIT__";
+ public static final String NAME = "name";
/**
* @param txt Text to parse
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-matched-queries.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-matched-queries.xml
new file mode 100644
index 00000000000..95cca5d940f
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-matched-queries.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" ?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<config>
+
+ <dataDir>${solr.data.dir:}</dataDir>
+
+ <directoryFactory name="DirectoryFactory"
+ class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+ <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+ <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+ <xi:include href="solrconfig.snippet.randomindexconfig.xml" xmlns:xi="http://www.w3.org/2001/XInclude"/>
+
+ <updateHandler class="solr.DirectUpdateHandler2">
+ <commitWithin>
+ <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+ </commitWithin>
+ </updateHandler>
+
+ <requestHandler name="/select" class="solr.SearchHandler">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <str name="df">text</str>
+ </lst>
+ </requestHandler>
+
+ <searchComponent name="matched_queries" class="org.apache.solr.handler.component.MatchedQueriesComponent"/>
+
+ <requestHandler name="/matched-queries" class="solr.SearchHandler">
+ <arr name="last-components">
+ <str>matched_queries</str>
+ </arr>
+ <lst name="defaults">
+ <str name="df">text</str>
+ </lst>
+ </requestHandler>
+
+</config>
diff --git a/solr/core/src/test/org/apache/solr/handler/component/TestMatchedQueriesComponent.java b/solr/core/src/test/org/apache/solr/handler/component/TestMatchedQueriesComponent.java
new file mode 100644
index 00000000000..a0a4b891742
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/component/TestMatchedQueriesComponent.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestMatchedQueriesComponent extends SolrTestCaseJ4 {
+
+ static final String HANDLER = "/matched-queries";
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig-matched-queries.xml", "schema.xml");
+
+ // 4 fantasy books (ids 1–4), 2 of which are also childrens (ids 2–3)
+ assertU(adoc("id", "1", "cat_s", "fantasy", "author_s1", "Lev Grossman"));
+ assertU(
+ adoc("id", "2", "cat_s", "fantasy", "cat_s", "childrens", "author_s1", "Robert Jordan"));
+ assertU(
+ adoc("id", "3", "cat_s", "fantasy", "cat_s", "childrens", "author_s1", "Robert Jordan"));
+ assertU(adoc("id", "4", "cat_s", "fantasy", "author_s1", "N.K. Jemisin"));
+ assertU(commit());
+ // 3 scifi books (ids 5–7), in a separate segment
+ assertU(adoc("id", "5", "cat_s", "scifi", "author_s1", "Ursula K. Le Guin"));
+ assertU(adoc("id", "6", "cat_s", "scifi", "author_s1", "Ursula K. Le Guin"));
+ assertU(adoc("id", "7", "cat_s", "scifi", "author_s1", "Isaac Asimov"));
+ assertU(commit());
+ }
+
+ /** Component must be a no-op when the activation param is absent. */
+ @Test
+ public void testNotEnabledByDefault() throws Exception {
+ assertJQ(
+ req("qt", HANDLER, "q", "{!term name=fantasy_cat f=cat_s}fantasy", "sort", "id asc"),
+ "!/matched_queries_per_hit==null",
+ "!/matched_queries_summary==null");
+ }
+
+ /** A single named term query: all 4 matching docs appear in per_hit and summary. */
+ @Test
+ public void testSingleNamedTermQuery() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!term name=fantasy_cat f=cat_s}fantasy",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ "/matched_queries_per_hit/1/[0]=='fantasy_cat'",
+ "/matched_queries_per_hit/2/[0]=='fantasy_cat'",
+ "/matched_queries_per_hit/3/[0]=='fantasy_cat'",
+ "/matched_queries_per_hit/4/[0]=='fantasy_cat'",
+ "/matched_queries_summary/fantasy_cat/[0]=='1'",
+ "/matched_queries_summary/fantasy_cat/[3]=='4'");
+ }
+
+ /** The short alias {@code mq=true} must work identically to {@code matched_queries=true}. */
+ @Test
+ public void testShortParamAlias() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!term name=fantasy_cat f=cat_s}fantasy",
+ "mq", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ "/matched_queries_summary/fantasy_cat/[0]=='1'");
+ }
+
+ /**
+ * Boolean OR of two named term queries: fantasy docs carry "fantasy_cat", scifi docs carry
+ * "scifi_cat", no doc carries both.
+ */
+ @Test
+ public void testTwoNamedQueriesOr() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q",
+ "({!term name=fantasy_cat f=cat_s}fantasy) OR ({!term name=scifi_cat f=cat_s}scifi)",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==7",
+ "/matched_queries_per_hit/1/[0]=='fantasy_cat'",
+ "/matched_queries_per_hit/5/[0]=='scifi_cat'",
+ "/matched_queries_summary/fantasy_cat/[3]=='4'",
+ "/matched_queries_summary/scifi_cat/[2]=='7'");
+ }
+
+ /** An unnamed term query must produce no matched_queries output even when mq=true. */
+ @Test
+ public void testUnnamedQueryProducesNoOutput() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!term f=cat_s}fantasy",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ "!/matched_queries_per_hit==null",
+ "!/matched_queries_summary==null");
+ }
+
+ /** Per-hit list carries exactly the names that match for that document. */
+ @Test
+ public void testMultiValuedFieldBothNamesPresent() throws Exception {
+ // docs 2 and 3 match both fantasy_cat and childrens_cat
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q",
+ "({!term name=fantasy_cat f=cat_s}fantasy) OR ({!term name=childrens_cat f=cat_s}childrens)",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ "/matched_queries_summary/fantasy_cat/[3]=='4'",
+ "/matched_queries_summary/childrens_cat/[0]=='2'",
+ "/matched_queries_summary/childrens_cat/[1]=='3'");
+ }
+
+ /**
+ * {@code {!terms}} with a single {@code _name}: all matching docs — across both index segments —
+ * are tagged with that name.
+ */
+ @Test
+ public void testTermsNamedQuery() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!terms name=genre_all f=cat_s}fantasy,scifi",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==7",
+ "/matched_queries_per_hit/1/[0]=='genre_all'",
+ "/matched_queries_per_hit/5/[0]=='genre_all'",
+ "/matched_queries_summary/genre_all/[0]=='1'",
+ "/matched_queries_summary/genre_all/[6]=='7'");
+ }
+
+ /**
+ * Outer {@code {!bool name=...}} plus inner named {@code {!term name=...}} SHOULD clauses: the
+ * outer name appears on every hit; inner names appear only on the docs whose specific clause
+ * fired. All three names are independent entries in the summary.
+ */
+ @Test
+ public void testBoolOuterAndInnerNamesComposed() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q",
+ "{!bool name=all_books"
+ + " should='{!term name=fantasy_cat f=cat_s}fantasy'"
+ + " should='{!term name=scifi_cat f=cat_s}scifi'}",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==7",
+ // every doc carries all_books (outer name)
+ "/matched_queries_summary/all_books/[6]=='7'",
+ // inner names split correctly
+ "/matched_queries_summary/fantasy_cat/[3]=='4'",
+ "/matched_queries_summary/scifi_cat/[2]=='7'",
+ // spot-check: doc 1 has both all_books and fantasy_cat
+ "/matched_queries_per_hit/1/[0]=='all_books'",
+ "/matched_queries_per_hit/1/[1]=='fantasy_cat'",
+ // spot-check: doc 5 has both all_books and scifi_cat
+ "/matched_queries_per_hit/5/[0]=='all_books'",
+ "/matched_queries_per_hit/5/[1]=='scifi_cat'");
+ }
+
+ /**
+ * {@code {!bool}} with multiple named SHOULD clauses: each doc is tagged only with the clause(s)
+ * it actually matched — same semantics as an explicit OR but exercising the BoolQParserPlugin /
+ * FiltersQParser code path.
+ */
+ @Test
+ public void testBoolMultipleShouldNamedTerms() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q",
+ "{!bool should='{!term name=fantasy_cat f=cat_s}fantasy'"
+ + " should='{!term name=scifi_cat f=cat_s}scifi'}",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==7",
+ "/matched_queries_per_hit/1/[0]=='fantasy_cat'",
+ "/matched_queries_per_hit/4/[0]=='fantasy_cat'",
+ "/matched_queries_per_hit/5/[0]=='scifi_cat'",
+ "/matched_queries_per_hit/7/[0]=='scifi_cat'",
+ "/matched_queries_summary/fantasy_cat/[3]=='4'",
+ "/matched_queries_summary/scifi_cat/[2]=='7'");
+ }
+
+ /**
+ * {@code {!bool}} with an unnamed MUST clause and a named SHOULD clause: the MUST clause drives
+ * which docs are returned; the named SHOULD clause fires only for the subset that also matches
+ * it. Docs that satisfy the MUST but not the SHOULD must be absent from {@code
+ * matched_queries_per_hit} and must not inflate the summary count.
+ */
+ @Test
+ public void testBoolMustWithNamedShould() throws Exception {
+ // MUST: all 4 fantasy docs; named SHOULD: only docs 2 and 3 (childrens)
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q",
+ "{!bool must='{!term f=cat_s}fantasy'"
+ + " should='{!term name=childrens_cat f=cat_s}childrens'}",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ // docs 2 and 3 matched the named SHOULD
+ "/matched_queries_per_hit/2/[0]=='childrens_cat'",
+ "/matched_queries_per_hit/3/[0]=='childrens_cat'",
+ // docs 1 and 4 matched only the unnamed MUST — no entry for them
+ "!/matched_queries_per_hit/1==null",
+ "!/matched_queries_per_hit/4==null",
+ "/matched_queries_summary/childrens_cat/[0]=='2'",
+ "/matched_queries_summary/childrens_cat/[1]=='3'");
+ }
+
+ /**
+ * {@code {!prefix}} with {@code _name}: all fantasy docs (cat_s starting with "fanta") are
+ * tagged.
+ */
+ @Test
+ public void testPrefixNamedQuery() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!prefix name=fanta_prefix f=cat_s}fanta",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ "/matched_queries_summary/fanta_prefix/[3]=='4'",
+ "/matched_queries_per_hit/1/[0]=='fanta_prefix'",
+ "/matched_queries_per_hit/4/[0]=='fanta_prefix'");
+ }
+
+ /**
+ * {@code {!edismax}} with {@code _name}: extended DisMax query; all matching docs carry the name.
+ */
+ @Test
+ public void testEdismaxNamedQuery() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!edismax name=fantasy_edismax qf=cat_s}fantasy",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==4",
+ "/matched_queries_summary/fantasy_edismax/[3]=='4'",
+ "/matched_queries_per_hit/1/[0]=='fantasy_edismax'",
+ "/matched_queries_per_hit/4/[0]=='fantasy_edismax'");
+ }
+
+ /**
+ * {@code {!lucene}} with {@code _name}: standard Lucene query syntax; all matching docs tagged.
+ */
+ @Test
+ public void testLuceneNamedQuery() throws Exception {
+ assertJQ(
+ req(
+ "qt", HANDLER,
+ "q", "{!lucene name=scifi_lucene df=cat_s}scifi",
+ "matched_queries", "true",
+ "sort", "id asc",
+ "rows", "10"),
+ "/response/numFound==3",
+ "/matched_queries_summary/scifi_lucene/[2]=='7'",
+ "/matched_queries_per_hit/5/[0]=='scifi_lucene'",
+ "/matched_queries_per_hit/7/[0]=='scifi_lucene'");
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
index d21e6d9ef72..239f29491db 100644
--- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
+++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
@@ -20,9 +20,16 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FuzzyQuery;
+import org.apache.lucene.search.NamedMatches;
import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermInSetQuery;
+import org.apache.lucene.search.TermQuery;
import org.apache.lucene.tests.search.QueryUtils;
+import org.apache.lucene.util.BytesRef;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.request.SolrQueryRequest;
@@ -30,6 +37,7 @@ import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.SolrQueryResponse;
import org.junit.AfterClass;
import org.junit.BeforeClass;
+import org.junit.Test;
/**
* Sanity checks that queries (generated by the QParser and ValueSourceParser framework) are
@@ -1838,6 +1846,132 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
"bool", "{!bool must='{!lucene}foo_s:a'}", "{!bool should='{!lucene}foo_s:a'}"));
}
+ /**
+ * The {@code name} local parameter is handled centrally in {@link QParser#getQuery()} and wraps
+ * the parsed query with {@link NamedMatches}. Verify that the wrapping is consistent (equals +
+ * hashCode) across the parsers.
+ */
+ public void testNameLocalParam() throws Exception {
+ assertQueryEquals(
+ "term", "{!term name=my_label f=foo_s}hello", "{!term name=my_label f=foo_s v=hello}");
+
+ assertQueryEquals(
+ "prefix", "{!prefix name=my_label f=foo_s}hel", "{!prefix name=my_label f=foo_s v=hel}");
+
+ assertQueryEquals(
+ "fuzzy", "{!fuzzy name=my_label f=foo_s}hello", "{!fuzzy name=my_label f=foo_s v=hello}");
+
+ assertQueryEquals(
+ "terms", "{!terms name=my_label f=foo_s}a,b", "{!terms name=my_label f=foo_s v='a,b'}");
+
+ assertQueryEquals(
+ "bool", "{!bool name=my_label must=foo_s:hello}", "{!bool name=my_label must=foo_s:hello}");
+
+ assertQueryEquals(
+ "dismax",
+ "{!dismax name=my_label qf=foo_s}hello",
+ "{!dismax name=my_label qf=foo_s v=hello}");
+
+ assertQueryEquals(
+ "edismax",
+ "{!edismax name=my_label qf=foo_s}hello",
+ "{!edismax name=my_label qf=foo_s v=hello}");
+ }
+
+ @Test
+ public void testNamedEdismaxQuery() throws Exception {
+ Query inner = QParser.getParser("{!edismax qf=name}Zapp", req()).getQuery();
+ Query named = QParser.getParser("{!edismax name=edismax_q qf=name}Zapp", req()).getQuery();
+ assertEquals(NamedMatches.wrapQuery("edismax_q", inner), named);
+ }
+
+ @Test
+ public void testNamedTermQuery() throws Exception {
+ Query actual = QParser.getParser("{!term name=title_left f=t_title}left", req()).getQuery();
+ assertEquals(
+ NamedMatches.wrapQuery("title_left", new TermQuery(new Term("t_title", "left"))), actual);
+ }
+
+ @Test
+ public void testNamedTermsQuery() throws Exception {
+ Query actual =
+ QParser.getParser("{!terms name=genre_fiction f=cat_s}fantasy,scifi", req()).getQuery();
+ assertEquals(
+ NamedMatches.wrapQuery(
+ "genre_fiction",
+ new TermInSetQuery(
+ "cat_s", Arrays.asList(new BytesRef("fantasy"), new BytesRef("scifi")))),
+ actual);
+ }
+
+ @Test
+ public void testNamedFuzzyQuery() throws Exception {
+ // cat_s is a string field — no multi-term analysis, term is used verbatim
+ Query actual = QParser.getParser("{!fuzzy name=cat_fuzzy f=cat_s}fantasy", req()).getQuery();
+ assertEquals(
+ NamedMatches.wrapQuery("cat_fuzzy", new FuzzyQuery(new Term("cat_s", "fantasy"))), actual);
+ }
+
+ @Test
+ public void testNamedFuzzyQueryCustomMaxEdits() throws Exception {
+ Query actual =
+ QParser.getParser("{!fuzzy name=cat_fuzzy1 f=cat_s maxEdits=1}fantasy", req()).getQuery();
+ assertEquals(
+ NamedMatches.wrapQuery("cat_fuzzy1", new FuzzyQuery(new Term("cat_s", "fantasy"), 1)),
+ actual);
+ }
+
+ @Test
+ public void testNamedBoolQuery() throws Exception {
+ Query actual =
+ QParser.getParser("{!bool name=my_bool must=name:foo should=name:bar}", req()).getQuery();
+
+ BooleanQuery inner =
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.MUST)
+ .add(new TermQuery(new Term("name", "bar")), BooleanClause.Occur.SHOULD)
+ .setMinimumNumberShouldMatch(0)
+ .build();
+ assertEquals(NamedMatches.wrapQuery("my_bool", inner), actual);
+ }
+
+ @Test
+ public void testNamedBoolQueryWithMinShouldMatch() throws Exception {
+ Query actual =
+ QParser.getParser(
+ "{!bool name=at_least_two should=name:foo should=name:bar should=name:qux mm=2}",
+ req())
+ .getQuery();
+
+ BooleanQuery inner =
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.SHOULD)
+ .add(new TermQuery(new Term("name", "bar")), BooleanClause.Occur.SHOULD)
+ .add(new TermQuery(new Term("name", "qux")), BooleanClause.Occur.SHOULD)
+ .setMinimumNumberShouldMatch(2)
+ .build();
+ assertEquals(NamedMatches.wrapQuery("at_least_two", inner), actual);
+ }
+
+ @Test
+ public void testNamedBoolQueryWithExcludeTags() throws Exception {
+ // excludeTags filters one of the $ref clauses; name wraps what remains
+ SolrQueryRequest req =
+ req(
+ "ref", "{!tag=t1}foo",
+ "ref", "{!tag=t2}bar",
+ "df", "name");
+
+ Query actual =
+ QParser.getParser("{!bool name=my_ref must=$ref excludeTags=t2}", req).getQuery();
+ BooleanQuery inner =
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.MUST)
+ .build();
+
+ assertEquals(NamedMatches.wrapQuery("my_ref", inner), actual);
+ }
+
public void testHashRangeQuery() throws Exception {
assertQueryEquals(
"hash_range",
diff --git a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
index 57790c330d7..84f6bf1cbed 100644
--- a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
+++ b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
@@ -20,6 +20,7 @@ package org.apache.solr.search;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.NamedMatches;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.solr.SolrTestCaseJ4;
@@ -112,4 +113,25 @@ public class TestMmBoolQParserPlugin extends SolrTestCaseJ4 {
NumberFormatException.class,
() -> parseQuery(req("q", "{!bool should=name:foo mm=2.9}")));
}
+
+ /** cache and cost local params must survive alongside name: WrappedQuery wraps NamedMatches. */
+ @Test
+ public void testNameWithCacheAndCostPreserved() throws Exception {
+ Query actual = parseQuery(req("q", "{!bool name=my_bool cache=false cost=200 must=name:foo}"));
+
+ assertTrue("expected WrappedQuery", actual instanceof WrappedQuery);
+ WrappedQuery wrapped = (WrappedQuery) actual;
+ assertFalse("cache=false must be preserved", wrapped.getCache());
+ assertEquals("cost=200 must be preserved", 200, wrapped.getCost());
+
+ BooleanQuery inner =
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.MUST)
+ .setMinimumNumberShouldMatch(0)
+ .build();
+ assertEquals(
+ "WrappedQuery must wrap NamedMatches",
+ NamedMatches.wrapQuery("my_bool", inner),
+ wrapped.getWrappedQuery());
+ }
}
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc
index ae96bc0eb4d..1a1f467dd87 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc
@@ -288,6 +288,7 @@ The `debug` parameter can be specified multiple times and supports the following
* `debug=timing`: return debug information about how long the query took to process.
* `debug=results`: return debug information about the score results (also known as "explain").
** By default, score explanations are returned as large string values, using newlines and tab indenting for structure & readability, but an additional `debug.explain.structured=true` parameter may be specified to return this information as nested data structures native to the response format requested by `wt`.
+** To see which named sub-queries matched each document (rather than score breakdowns), see xref:matched-queries-component.adoc[].
* `debug=all`: return all available debug information about the request request.
An alternative usage is `debug=true`.
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/matched-queries-component.adoc b/solr/solr-ref-guide/modules/query-guide/pages/matched-queries-component.adoc
new file mode 100644
index 00000000000..92f4ddaa0e9
--- /dev/null
+++ b/solr/solr-ref-guide/modules/query-guide/pages/matched-queries-component.adoc
@@ -0,0 +1,121 @@
+= Matched Queries Component
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+The Matched Queries component enriches search responses with named-match information:
+for each top-N hit it reports which named sub-queries matched that document,
+and provides a summary across all hits.
+
+This is useful when a query is composed of multiple named logical conditions and you want to know, per document, which conditions fired.
+
+== Activation
+
+Add `matched_queries=true` (or the short alias `mq=true`) as a request parameter.
+Without this parameter the component is a no-op and adds nothing to the response.
+
+== Naming queries with `name`
+
+Any query parser accepts the `name` local parameter.
+When present, every document matched by that sub-query is tagged with the given name.
+
+[source,text]
+----
+{!term name=fantasy_cat f=cat}fantasy
+----
+
+Named queries may be freely composed:
+
+[source,text]
+----
+({!term name=fantasy_cat f=cat}fantasy) OR ({!term name=scifi_cat f=cat}scifi)
+----
+
+[source,text]
+----
+{!bool name=all_genres
+ should='{!term name=fantasy_cat f=cat}fantasy'
+ should='{!term name=scifi_cat f=cat}scifi'}
+----
+
+== Response
+
+When the component is active and at least one named query matches, two top-level keys are added to the response:
+
+`matched_queries_per_hit`::
+A map from each matching document's unique-key value to the list of names that matched it.
+Documents that matched no named query are absent from this map.
+
+`matched_queries_summary`::
+A map from each name that fired to the ordered list of unique-key values of documents it matched.
+
+=== Example Response
+
+Request:
+
+[source,text]
+----
+q=({!term name=fantasy_cat f=cat}fantasy) OR ({!term name=scifi_cat f=cat}scifi)
+&matched_queries=true&rows=3&sort=id asc
+----
+
+Response (abbreviated):
+
+[source,json]
+----
+{
+ "response": { "numFound": 7, "docs": [ ... ] },
+ "matched_queries_per_hit": {
+ "1": ["fantasy_cat"],
+ "2": ["fantasy_cat"],
+ "5": ["scifi_cat"]
+ },
+ "matched_queries_summary": {
+ "fantasy_cat": ["1", "2", "3", "4"],
+ "scifi_cat": ["5", "6", "7"]
+ }
+}
+----
+
+Documents that matched only an unnamed clause (e.g., a plain MUST filter) appear in `response/docs` as normal but are absent from both output maps.
+
+== Parameters
+
+`matched_queries` (or `mq`)::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `false`
+|===
++
+Set to `true` to activate the component for this request.
+Both `matched_queries=true` and `mq=true` are equivalent.
+
+== Configuration
+
+Register the component in the request handler in `solrconfig.xml`:
+
+[source,xml]
+----
+<requestHandler name="/matched-queries" class="solr.SearchHandler">
+ <arr name="components">
+ <str>query</str>
+ <str>matched_queries</str>
+ </arr>
+</requestHandler>
+----
+
+The component name `matched_queries` is pre-registered by Solr and does not require an explicit `<searchComponent>` declaration.
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
index 326c480720e..f46e6f69781 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
@@ -1145,8 +1145,17 @@ Find all documents with the phrase "foo bar" where term "foo" has a payload grea
`PrefixQParser` extends the `QParserPlugin` by creating a prefix query from the input value.
Currently, no analysis or value transformation is done to create this prefix query.
-The parameter is `f`, the field.
-The string after the prefix declaration is treated as a wildcard query.
+*Parameters*
+
+`f`::
++
+[%autowidth,frame=none]
+|===
+|Required |Default: none
+|===
++
+The field on which to run the prefix query.
+The value that follows the local params declaration is treated as the prefix.
Example:
@@ -1363,7 +1372,17 @@ Using the example configuration below, clients can optionally specify the custom
`TermQParser` extends the `QParserPlugin` by creating a single term query from the input value equivalent to `readableToIndexed()`.
This is useful for generating filter queries from the external human-readable terms returned by the faceting or terms components.
-The only parameter is `f`, for the field.
+
+*Parameters*
+
+`f`::
++
+[%autowidth,frame=none]
+|===
+|Required |Default: none
+|===
++
+The field on which to run the term query.
Example:
diff --git a/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc b/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
index ecdd4603dcf..90dc99aacac 100644
--- a/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
@@ -57,6 +57,7 @@
** xref:terms-component.adoc[]
** xref:term-vector-component.adoc[]
** xref:stats-component.adoc[]
+** xref:matched-queries-component.adoc[]
* Controlling Results
** xref:faceting.adoc[]
(solr) 09/11: DistributedCombinedQueryComponentTest hangs in nightly (#4497)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 7a002a5ba8a6f1dd6d20fc08718800754739b554
Author: Jan Høydahl <ja...@apache.org>
AuthorDate: Fri Jun 5 00:57:51 2026 +0200
DistributedCombinedQueryComponentTest hangs in nightly (#4497)
(cherry picked from commit de1e74298fa1e99eaf9a6802386c97da0789aee9)
---
.../DistributedCombinedQueryComponentTest.java | 109 ++++++++++-----------
1 file changed, 53 insertions(+), 56 deletions(-)
diff --git a/solr/core/src/test/org/apache/solr/handler/component/DistributedCombinedQueryComponentTest.java b/solr/core/src/test/org/apache/solr/handler/component/DistributedCombinedQueryComponentTest.java
index dc586ebf17b..e6b2805236f 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/DistributedCombinedQueryComponentTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/DistributedCombinedQueryComponentTest.java
@@ -60,9 +60,8 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
*
* @throws Exception if any error occurs during the indexing process.
*/
- private synchronized void prepareIndexDocs() throws Exception {
+ private void prepareIndexDocs() throws Exception {
List<SolrInputDocument> docs = new ArrayList<>();
- fixShardCount(2);
for (int i = 1; i <= NUM_DOCS; i++) {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", Integer.toString(i));
@@ -116,18 +115,16 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @throws Exception if any exception occurs during the test execution
*/
@Test
+ @ShardsFixed(num = 2)
public void testSingleLexicalQuery() throws Exception {
prepareIndexDocs();
- QueryResponse rsp =
- query(
- CommonParams.JSON,
- "{\"queries\":"
- + "{\"lexical1\":{\"lucene\":{\"query\":\"id:2^10\"}}},"
- + "\"limit\":5,"
- + "\"fields\":[\"id\",\"score\",\"title\"],"
- + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\"]}}",
- CommonParams.QT,
- "/search");
+ String jsonQuery =
+ "{\"queries\":"
+ + "{\"lexical1\":{\"lucene\":{\"query\":\"id:2^10\"}}},"
+ + "\"limit\":5,"
+ + "\"fields\":[\"id\",\"score\",\"title\"],"
+ + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\"]}}";
+ QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search");
assertEquals(1, rsp.getResults().size());
assertFieldValues(rsp.getResults(), id, "2");
}
@@ -150,6 +147,7 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @throws Exception if any error occurs during the test execution
*/
@Test
+ @ShardsFixed(num = 2)
public void testMultipleLexicalQuery() throws Exception {
prepareIndexDocs();
String jsonQuery =
@@ -170,13 +168,14 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @throws Exception the exception
*/
@Test
+ @ShardsFixed(num = 2)
public void testMultipleQueryWithSort() throws Exception {
prepareIndexDocs();
String jsonQuery =
"{\"queries\":"
+ "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
+ "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
- + "\"limit\":5,\"sort\":\"mod3_idv desc, score desc\""
+ + "\"limit\":5,\"sort\":\"mod3_idv desc, score desc\","
+ "\"fields\":[\"id\",\"score\",\"title\"],"
+ "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}";
QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search");
@@ -190,42 +189,34 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @throws Exception if any unexpected error occurs during the test execution.
*/
@Test
+ @ShardsFixed(num = 2)
public void testHybridQueryWithPagination() throws Exception {
prepareIndexDocs();
- QueryResponse rsp =
- query(
- CommonParams.JSON,
- "{\"queries\":"
- + "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
- + "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
- + "\"fields\":[\"id\",\"score\",\"title\"],"
- + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
- CommonParams.QT,
- "/search");
+ String jsonQueryAll =
+ "{\"queries\":"
+ + "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
+ + "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
+ + "\"fields\":[\"id\",\"score\",\"title\"],"
+ + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}";
+ QueryResponse rsp = query(CommonParams.JSON, jsonQueryAll, CommonParams.QT, "/search");
assertFieldValues(rsp.getResults(), id, "5", "7", "2", "6", "3", "10", "4");
- rsp =
- query(
- CommonParams.JSON,
- "{\"queries\":"
- + "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
- + "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
- + "\"limit\":4,"
- + "\"fields\":[\"id\",\"score\",\"title\"],"
- + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
- CommonParams.QT,
- "/search");
+ String jsonQueryLimit4 =
+ "{\"queries\":"
+ + "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
+ + "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
+ + "\"limit\":4,"
+ + "\"fields\":[\"id\",\"score\",\"title\"],"
+ + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}";
+ rsp = query(CommonParams.JSON, jsonQueryLimit4, CommonParams.QT, "/search");
assertFieldValues(rsp.getResults(), id, "5", "7", "2", "6");
- rsp =
- query(
- CommonParams.JSON,
- "{\"queries\":"
- + "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
- + "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
- + "\"limit\":4,\"offset\":3,"
- + "\"fields\":[\"id\",\"score\",\"title\"],"
- + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
- CommonParams.QT,
- "/search");
+ String jsonQueryPage =
+ "{\"queries\":"
+ + "{\"lexical1\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}},"
+ + "\"lexical2\":{\"lucene\":{\"query\":\"id:(4^1 OR 5^2 OR 7^3 OR 10^2)\"}}},"
+ + "\"limit\":4,\"offset\":3,"
+ + "\"fields\":[\"id\",\"score\",\"title\"],"
+ + "\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}";
+ rsp = query(CommonParams.JSON, jsonQueryPage, CommonParams.QT, "/search");
assertEquals(4, rsp.getResults().size());
assertFieldValues(rsp.getResults(), id, "6", "3", "10", "4");
}
@@ -236,15 +227,16 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @throws Exception if any unexpected error occurs during the test execution.
*/
@Test
+ @ShardsFixed(num = 2)
public void testQueryWithFaceting() throws Exception {
prepareIndexDocs();
String jsonQuery =
"{\"queries\":"
+ "{\"lexical\":{\"lucene\":{\"query\":\"id:(2^2 OR 3^1 OR 6^2 OR 5^1)\"}}},"
- + "\"limit\":3,\"offset\":1"
+ + "\"limit\":3,\"offset\":1,"
+ "\"fields\":[\"id\",\"score\",\"title\"],"
- + "\"params\":{\"combiner\":true,\"facet\":true,\"facet.field\":\"mod3_idv\",\"facet.mincount\":1,"
- + "\"combiner.query\":[\"lexical\"]}}";
+ + "\"params\":{\"combiner\":true,\"facet\":true,\"facet.field\":\"mod3_idv\","
+ + "\"facet.mincount\":1,\"combiner.query\":[\"lexical\"]}}";
QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search");
assertEquals(3, rsp.getResults().size());
assertEquals(4, rsp.getResults().getNumFound());
@@ -257,6 +249,7 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @throws Exception if any unexpected error occurs during the test execution.
*/
@Test
+ @ShardsFixed(num = 2)
public void testQueriesWithFacetAndHighlights() throws Exception {
prepareIndexDocs();
String jsonQuery =
@@ -266,8 +259,8 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
+ "\"limit\":4,"
+ "\"fields\":[\"id\",\"score\",\"title\"],"
+ "\"params\":{\"combiner\":true,\"facet\":true,\"facet.field\":\"mod3_idv\","
- + "\"combiner.query\":[\"lexical1\",\"lexical2\"], \"hl\": true,"
- + "\"hl.fl\": \"title\",\"hl.q\":\"test doc\"}}";
+ + "\"combiner.query\":[\"lexical1\",\"lexical2\"],\"hl\":true,\"hl.fl\":\"title\","
+ + "\"hl.q\":\"test doc\"}}";
QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search");
assertEquals(4, rsp.getResults().size());
assertFieldValues(rsp.getResults(), id, "5", "7", "2", "6");
@@ -286,6 +279,7 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* @see org.apache.solr.handler.component.CombinedQuerySolrCloudTest#testForcedDistrib()
*/
@Test
+ @ShardsFixed(num = 2)
public void testForcedDistrib() throws Exception {
QueryResponse rsp = query("qt", "/forcedDistribTest", "q", "*:*", "rows", "0");
// ForcedDistribSearchHandler would trigger a failure if this didn't work
@@ -318,6 +312,7 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
* collapse field.
*/
@Test
+ @ShardsFixed(num = 2)
public void testCollapseWithCombinedQueryProducesDuplicates() throws Exception {
del("*:*");
@@ -356,9 +351,6 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
// After collapse, each query returns 3 docs (one per mod3_idv value 0,1,2).
// But the group heads for mod3_idv=1 differ: query1 picks id=1, query2 picks id=4.
// simpleCombine() merges by doc ID, so both id=1 and id=4 survive → duplicate on mod3_idv=1.
- // q1 -> 1,2,5,3,6 q2 -> 2,4,6,3,5
- // q1 -> 1,2,2,0,0 q2 -> 2,1,0,0,2
- // q1 -> 1 for sure, 2,3 => 4 for sure, 3 and 2
String jsonQuery =
"{"
+ " \"queries\": {"
@@ -399,7 +391,12 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
+ "}";
handle.put("expanded", UNORDERED);
- QueryResponse rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search");
+ QueryResponse rsp;
+ try {
+ rsp = query(CommonParams.JSON, jsonQuery, CommonParams.QT, "/search");
+ } finally {
+ handle.remove("expanded");
+ }
// Collect mod3_idv values from the result to check for duplicates
List<Integer> collapseValues = new ArrayList<>();
@@ -415,8 +412,8 @@ public class DistributedCombinedQueryComponentTest extends BaseDistributedSearch
"Expected no duplicate collapse field values in combined results, "
+ "but got collapseValues="
+ collapseValues,
- uniqueCollapseValues,
- collapseValues.size());
+ collapseValues.size(),
+ uniqueCollapseValues);
assertEquals("Expected exactly 3 groups (mod3_idv values 0, 1, 2)", 3, collapseValues.size());
assertEquals("id", rsp.getFacetFields().get(0).getName());
assertEquals(
(solr) 08/11: SOLR-18098: Fix precommit (#4498)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit f76f061c0a9e76a52be6bc9301c9a2f059e092c5
Author: Anshum Gupta <an...@apache.org>
AuthorDate: Thu Jun 4 13:57:39 2026 -0700
SOLR-18098: Fix precommit (#4498)
---
.../org/apache/solr/handler/IndexFetcherPacketProtocolTest.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java b/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java
index 050994e98d1..6307c067bd9 100644
--- a/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java
@@ -18,10 +18,12 @@ package org.apache.solr.handler;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.store.Directory;
@@ -275,7 +277,7 @@ public class IndexFetcherPacketProtocolTest extends SolrTestCaseJ4 {
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- Method writeMethod = stream.getClass().getMethod("write", java.io.OutputStream.class);
+ Method writeMethod = stream.getClass().getMethod("write", OutputStream.class);
writeMethod.invoke(stream, baos);
return baos.toByteArray();
} finally {
@@ -377,7 +379,7 @@ public class IndexFetcherPacketProtocolTest extends SolrTestCaseJ4 {
private Object createMockFileInterface(ByteArrayOutputStream output) throws Exception {
Class<?> fileInterfaceClass = getFileInterfaceClass();
- return java.lang.reflect.Proxy.newProxyInstance(
+ return Proxy.newProxyInstance(
fileInterfaceClass.getClassLoader(),
new Class<?>[] {fileInterfaceClass},
(proxy, method, args) -> {
(solr) 04/11: build failed-tests-at-end: improve isSuite detection
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 6601e58a76d354f1a669f01b17b5f1e26507a336
Author: David Smiley <ds...@apache.org>
AuthorDate: Sun May 24 18:00:19 2026 -0400
build failed-tests-at-end: improve isSuite detection
---
gradle/testing/failed-tests-at-end.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/testing/failed-tests-at-end.gradle b/gradle/testing/failed-tests-at-end.gradle
index aebb615b9ac..5ffb6758268 100644
--- a/gradle/testing/failed-tests-at-end.gradle
+++ b/gradle/testing/failed-tests-at-end.gradle
@@ -22,7 +22,7 @@ import org.apache.lucene.gradle.ErrorReportingTestListener
def failedTests = new LinkedHashSet() // for dedupe due to weird afterTest classMethod issue
def genFailInfo(def task, TestDescriptor desc) {
- boolean isSuite = (desc.name == 'classMethod')
+ boolean isSuite = (desc.name == 'classMethod' || desc.name == 'initializationError')
def name = isSuite ? desc.className : "${desc.className}.${desc.name}"
def historyUrl = "https://develocity.apache.org/scans/tests?search.rootProjectNames=solr-root&tests.container=$desc.className"
if (!isSuite) { // is test method specific
(solr) 06/11: changes2html.py: fix for github nicknames with hyphens (#4481)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit be771e3eb5f26a85546a01892cc4ef8080429aa3
Author: David Smiley <ds...@apache.org>
AuthorDate: Sun May 31 09:51:33 2026 -0400
changes2html.py: fix for github nicknames with hyphens (#4481)
(cherry picked from commit 4862cdd810c21f5f07317613de87fb81b9f5e2db)
---
gradle/documentation/changes-to-html/changes2html.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle/documentation/changes-to-html/changes2html.py b/gradle/documentation/changes-to-html/changes2html.py
index 8ecb4832b22..1d52618834e 100755
--- a/gradle/documentation/changes-to-html/changes2html.py
+++ b/gradle/documentation/changes-to-html/changes2html.py
@@ -213,7 +213,7 @@ class HTMLGenerator:
# Extract markdown link: [text](url)
markdown_link_match = re.search(r'\[([^\]]+)\]\(([^)]+)\)', author_text)
# Extract GitHub handle: @username
- github_match = re.search(r'@(\w+)', author_text)
+ github_match = re.search(r'@([\w-]+)', author_text)
if markdown_link_match:
# Has markdown link
(solr) 11/11: HttpJettySolrClient: don't throw IllegalStateException if connection lost (#4490)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit a3c387d1c640499140babdaf729e1962fcae4e2a
Author: David Smiley <ds...@apache.org>
AuthorDate: Wed Jun 3 00:25:39 2026 -0400
HttpJettySolrClient: don't throw IllegalStateException if connection lost (#4490)
HttpJettySolrClient could throw IllegalStateException on connection lost,
which foiled LBSolrClient's attempts to classify a request as retry-able.
(cherry picked from commit 60397085723de228799dd9b6e26910c7d2e5380c)
---
.../PR#4490-HttpJettySolrClient-IllegalStateException.yml | 9 +++++++++
.../java/org/apache/solr/client/solrj/impl/Http2SolrClient.java | 4 ++++
2 files changed, 13 insertions(+)
diff --git a/changelog/unreleased/PR#4490-HttpJettySolrClient-IllegalStateException.yml b/changelog/unreleased/PR#4490-HttpJettySolrClient-IllegalStateException.yml
new file mode 100644
index 00000000000..cf5b8961c98
--- /dev/null
+++ b/changelog/unreleased/PR#4490-HttpJettySolrClient-IllegalStateException.yml
@@ -0,0 +1,9 @@
+title: >
+ HttpJettySolrClient could throw IllegalStateException on connection lost,
+ which foiled LBSolrClient's attempts to classify a request as retry-able.
+type: fixed
+authors:
+ - name: David Smiley
+links:
+ - name: PR#4490
+ url: https://github.com/apache/solr/pull/4490
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
index b995edc664d..ad014b6a0e0 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
@@ -640,6 +640,10 @@ public class Http2SolrClient extends HttpSolrClientBase {
"IOException occurred when talking to server at: " + url, cause);
}
throw new SolrServerException(cause.getMessage(), cause);
+ } catch (IllegalStateException e) {
+ // Jetty HTTP/2 throws IllegalStateException ("session closed") when the connection is lost.
+ abortCause = e;
+ throw new SolrServerException("Connection lost at: " + url, new IOException(e));
} catch (SolrServerException | RuntimeException sse) {
abortCause = sse;
throw sse;
(solr) 01/11: fix doc typo spacial→spatial (#4457)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit a2f523cf88b0468892caa186be81b55188fe5043
Author: Ken Geis <kg...@berkeley.edu>
AuthorDate: Mon May 25 07:36:04 2026 -0700
fix doc typo spacial→spatial (#4457)
---
solr/solr-ref-guide/modules/indexing-guide/pages/docvalues.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/solr/solr-ref-guide/modules/indexing-guide/pages/docvalues.adoc b/solr/solr-ref-guide/modules/indexing-guide/pages/docvalues.adoc
index 34b836def78..9717315a382 100644
--- a/solr/solr-ref-guide/modules/indexing-guide/pages/docvalues.adoc
+++ b/solr/solr-ref-guide/modules/indexing-guide/pages/docvalues.adoc
@@ -49,7 +49,7 @@ DocValues are enabled by default for most field types that support them *when us
** `ICUCollationField`
** `SortableTextField`
** `SortableBinaryField`
-* `LatLonPointSpacialField` (not `PointType`, which is different than `PointField`)
+* `LatLonPointSpatialField` (not `PointType`, which is different than `PointField`)
When using an earlier schemaVersion (\<= 1.6), you only need to enable docValues for a field that you will use it with.
As with all schema design, you need to define a field type and then define fields of that type with docValues enabled.
(solr) 07/11: SOLR-18098: Fix replication failure for files with exact MB sizes (#4205)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit 47f0eea91bb404f470a4e860baaaf69478fac016
Author: Shubham Ranjan <sh...@users.noreply.github.com>
AuthorDate: Wed Jun 3 22:40:54 2026 +0530
SOLR-18098: Fix replication failure for files with exact MB sizes (#4205)
---
.../SOLR-18098-fix-replication-exact-mb-sizes.yml | 7 +
.../solr/handler/admin/api/ReplicationAPIBase.java | 7 +
.../handler/IndexFetcherPacketProtocolTest.java | 399 +++++++++++++++++++++
3 files changed, 413 insertions(+)
diff --git a/changelog/unreleased/SOLR-18098-fix-replication-exact-mb-sizes.yml b/changelog/unreleased/SOLR-18098-fix-replication-exact-mb-sizes.yml
new file mode 100644
index 00000000000..9729678a651
--- /dev/null
+++ b/changelog/unreleased/SOLR-18098-fix-replication-exact-mb-sizes.yml
@@ -0,0 +1,7 @@
+title: Fix replication failure for files with exact MB sizes
+type: fixed
+authors:
+ - name: Shubham Ranjan
+links:
+ - name: SOLR-18098
+ url: https://issues.apache.org/jira/browse/SOLR-18098
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
index 3e627b35d72..132448d1c27 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplicationAPIBase.java
@@ -393,6 +393,13 @@ public abstract class ReplicationAPIBase extends JerseyResource {
while (true) {
offset = offset == -1 ? 0 : offset;
int read = (int) Math.min(buf.length, filelen - offset);
+
+ if (read <= 0) {
+ writeNothingAndFlush();
+ fos.close();
+ break;
+ }
+
in.readBytes(buf, 0, read);
fos.writeInt(read);
diff --git a/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java b/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java
new file mode 100644
index 00000000000..050994e98d1
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/IndexFetcherPacketProtocolTest.java
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.FastInputStream;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.DirectoryFactory;
+import org.apache.solr.handler.admin.api.CoreReplication;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+import org.apache.solr.request.SolrQueryRequestBase;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests the replication packet protocol between DirectoryFileStream (sender) and
+ * IndexFetcher.FileFetcher.fetchPackets (receiver).
+ *
+ * <p>The packet protocol is:
+ *
+ * <ul>
+ * <li>Data packet: {@code int(size) + long(checksum) + byte[size]}
+ * <li>EOF marker: {@code int(0)} alone (no checksum)
+ * </ul>
+ *
+ * <p>These tests verify correct handling of:
+ *
+ * <ul>
+ * <li>Files that are exact multiples of PACKET_SZ (1 MB) - sender must not produce spurious
+ * zero-length data packets
+ * <li>Files of arbitrary sizes - boundary conditions and edge cases
+ * <li>Checksum verification and error handling
+ * </ul>
+ */
+public class IndexFetcherPacketProtocolTest extends SolrTestCaseJ4 {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private static final int PACKET_SZ = ReplicationAPIBase.PACKET_SZ;
+ private static final int NO_CONTENT = 1;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ System.setProperty("solr.disable.allowUrls", "true");
+ initCore("solrconfig.xml", "schema.xml");
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ System.clearProperty("solr.disable.allowUrls");
+ }
+
+ // Tests for files that are exact multiples of PACKET_SZ
+ @Test
+ public void testExactlyOnePacketSize() throws Exception {
+ assertFetchPacketsSuccess(PACKET_SZ, "test-1mb.bin");
+ }
+
+ @Test
+ public void testExactlyTwoPacketSizes() throws Exception {
+ assertFetchPacketsSuccess(2 * PACKET_SZ, "test-2mb.bin");
+ }
+
+ @Test
+ public void testExactlyThreePacketSizes() throws Exception {
+ assertFetchPacketsSuccess(3 * PACKET_SZ, "test-3mb.bin");
+ }
+
+ @Test
+ public void testExactly63PacketSizes() throws Exception {
+ assertFetchPacketsSuccess(63 * PACKET_SZ, "test-63mb.bin");
+ }
+
+ // Tests for files that are not exact multiples of PACKET_SZ
+ @Test
+ public void testEmptyFile() throws Exception {
+ assertFetchPacketsSuccess(0, "test-empty.bin");
+ }
+
+ @Test
+ public void testSingleByte() throws Exception {
+ assertFetchPacketsSuccess(1, "test-1byte.bin");
+ }
+
+ @Test
+ public void testSmallFile100Bytes() throws Exception {
+ assertFetchPacketsSuccess(100, "test-100bytes.bin");
+ }
+
+ @Test
+ public void testSmallFile100KB() throws Exception {
+ assertFetchPacketsSuccess(100 * 1024, "test-100kb.bin");
+ }
+
+ @Test
+ public void testHalfPacketSize() throws Exception {
+ assertFetchPacketsSuccess(PACKET_SZ / 2, "test-512kb.bin");
+ }
+
+ @Test
+ public void testJustUnderOnePacketSize() throws Exception {
+ assertFetchPacketsSuccess(PACKET_SZ - 1, "test-under-1mb.bin");
+ }
+
+ @Test
+ public void testJustOverOnePacketSize() throws Exception {
+ assertFetchPacketsSuccess(PACKET_SZ + 1, "test-over-1mb.bin");
+ }
+
+ @Test
+ public void testOneAndHalfPacketSizes() throws Exception {
+ assertFetchPacketsSuccess(PACKET_SZ + PACKET_SZ / 2, "test-1.5mb.bin");
+ }
+
+ @Test
+ public void testJustUnderTwoPacketSizes() throws Exception {
+ assertFetchPacketsSuccess(2 * PACKET_SZ - 1, "test-under-2mb.bin");
+ }
+
+ @Test
+ public void testJustOverTwoPacketSizes() throws Exception {
+ assertFetchPacketsSuccess(2 * PACKET_SZ + 1, "test-over-2mb.bin");
+ }
+
+ @Test
+ public void testArbitrarySize() throws Exception {
+ assertFetchPacketsSuccess(PACKET_SZ + 500000, "test-arbitrary.bin");
+ }
+
+ // Additional tests for comprehensive coverage
+
+ @Test
+ public void testMultipleExactPacketSizeFiles() throws Exception {
+ // Ensures exact-multiple-of-PACKET_SZ handling works correctly in succession
+ for (int i = 1; i <= 5; i++) {
+ assertFetchPacketsSuccess(i * PACKET_SZ, "test-" + i + "mb-successive.bin");
+ }
+ }
+
+ @Test
+ public void testChecksumMismatchReturnsError() throws Exception {
+ int fileSize = 1024;
+ String fileName = "test-checksum-mismatch.bin";
+
+ byte[] expectedContent = createDeterministicContent(fileSize);
+ byte[] streamBytes = serializeFileToPacketStream(expectedContent, fileName);
+
+ // Corrupt the checksum bytes (bytes 4-11 after packet size)
+ // Stream format: int(size) + long(checksum) + data
+ if (streamBytes.length > 12) {
+ streamBytes[5] ^= 0xFF; // Flip bits in checksum
+ }
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ int result = invokeFetchPackets(streamBytes, output, fileName, fileSize);
+
+ assertEquals("fetchPackets should return 1 for checksum mismatch", 1, result);
+ }
+
+ @Test
+ public void testLargeFileTriggersBufferBehavior() throws Exception {
+ // Test with file larger than initial buffer allocation
+ // Initial buffer is min(fileSize, PACKET_SZ), so for 5MB file, buffer starts at 1MB
+ // This ensures the code handles multi-packet transfers correctly
+ assertFetchPacketsSuccess(5 * PACKET_SZ + 12345, "test-large-5mb.bin");
+ }
+
+ private void assertFetchPacketsSuccess(int fileSize, String fileName) throws Exception {
+ log.info("Testing file transfer: {} ({} bytes)", fileName, fileSize);
+
+ byte[] expectedContent = createDeterministicContent(fileSize);
+ byte[] streamBytes = serializeFileToPacketStream(expectedContent, fileName);
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ int result = invokeFetchPackets(streamBytes, output, fileName, fileSize);
+
+ int expectedResult = (fileSize == 0) ? NO_CONTENT : 0;
+ assertEquals("fetchPackets return code mismatch", expectedResult, result);
+ assertEquals("Output size should match input size", fileSize, output.size());
+ assertArrayEquals(
+ "Output content should match expected for " + fileName,
+ expectedContent,
+ output.toByteArray());
+
+ log.info("Successfully verified transfer of {} ({} bytes)", fileName, fileSize);
+ }
+
+ private byte[] createDeterministicContent(int size) {
+ byte[] content = new byte[size];
+ for (int i = 0; i < size; i++) {
+ content[i] = (byte) (i % 256);
+ }
+ return content;
+ }
+
+ /** Writes file content to a directory and serializes it using the replication packet protocol. */
+ private byte[] serializeFileToPacketStream(byte[] content, String fileName) throws Exception {
+ DirectoryFactory directoryFactory = h.getCore().getDirectoryFactory();
+ Directory dir =
+ directoryFactory.get(
+ h.getCore().getNewIndexDir(),
+ DirectoryFactory.DirContext.DEFAULT,
+ h.getCore().getSolrConfig().indexConfig.lockType);
+
+ try {
+ if (content.length > 0) {
+ try (IndexOutput out = dir.createOutput(fileName, IOContext.DEFAULT)) {
+ out.writeBytes(content, content.length);
+ }
+ }
+
+ CoreReplication replicationAPI =
+ new CoreReplication(
+ h.getCore(),
+ new SolrQueryRequestBase(h.getCore(), new ModifiableSolrParams()) {},
+ new SolrQueryResponse());
+
+ MethodHandles.Lookup lookup =
+ MethodHandles.privateLookupIn(ReplicationAPIBase.class, MethodHandles.lookup());
+ Class<?> directoryFileStreamClass =
+ Class.forName(ReplicationAPIBase.class.getName() + "$DirectoryFileStream");
+ MethodHandle doFetchFileHandle =
+ lookup.findVirtual(
+ ReplicationAPIBase.class,
+ "doFetchFile",
+ MethodType.methodType(
+ directoryFileStreamClass,
+ String.class,
+ String.class,
+ String.class,
+ String.class,
+ boolean.class,
+ boolean.class,
+ double.class,
+ Long.class));
+
+ Object stream;
+ try {
+ stream =
+ doFetchFileHandle.invoke(
+ replicationAPI, fileName, "file", null, null, false, true, 0.0, null);
+ } catch (Exception e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Method writeMethod = stream.getClass().getMethod("write", java.io.OutputStream.class);
+ writeMethod.invoke(stream, baos);
+ return baos.toByteArray();
+ } finally {
+ if (content.length > 0) {
+ try {
+ dir.deleteFile(fileName);
+ } catch (Exception e) {
+ // ignored
+ }
+ }
+ directoryFactory.release(dir);
+ }
+ }
+
+ /**
+ * Deserializes packet stream bytes using FileFetcher.fetchPackets and returns the result code.
+ */
+ private int invokeFetchPackets(
+ byte[] streamBytes, ByteArrayOutputStream output, String fileName, long expectedSize)
+ throws Exception {
+
+ Class<?> fileFetcherClass = null;
+ for (Class<?> innerClass : IndexFetcher.class.getDeclaredClasses()) {
+ if (innerClass.getSimpleName().equals("FileFetcher")) {
+ fileFetcherClass = innerClass;
+ break;
+ }
+ }
+ assertNotNull("FileFetcher inner class should exist", fileFetcherClass);
+
+ IndexFetcher indexFetcher = createIndexFetcher();
+
+ try {
+ Map<String, Object> fileDetails = new HashMap<>();
+ fileDetails.put("name", fileName);
+ fileDetails.put("size", expectedSize);
+
+ Object mockFileInterface = createMockFileInterface(output);
+
+ MethodHandles.Lookup lookup =
+ MethodHandles.privateLookupIn(fileFetcherClass, MethodHandles.lookup());
+ MethodHandle ctorHandle =
+ lookup.findConstructor(
+ fileFetcherClass,
+ MethodType.methodType(
+ void.class,
+ IndexFetcher.class,
+ getFileInterfaceClass(),
+ Map.class,
+ String.class,
+ String.class,
+ long.class));
+
+ Object fileFetcher;
+ try {
+ fileFetcher =
+ ctorHandle.invoke(indexFetcher, mockFileInterface, fileDetails, fileName, "file", 0L);
+ } catch (Exception e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+
+ MethodHandle fetchPacketsHandle =
+ lookup.findVirtual(
+ fileFetcherClass,
+ "fetchPackets",
+ MethodType.methodType(int.class, FastInputStream.class));
+
+ FastInputStream fis = new FastInputStream(new ByteArrayInputStream(streamBytes));
+ try {
+ return (int) fetchPacketsHandle.invoke(fileFetcher, fis);
+ } catch (Exception e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ } finally {
+ indexFetcher.destroy();
+ }
+ }
+
+ private IndexFetcher createIndexFetcher() throws Exception {
+ NamedList<Object> initArgs = new NamedList<>();
+ initArgs.add("leaderUrl", "http://localhost:8983/solr/collection1");
+ return new IndexFetcher(initArgs, null, h.getCore());
+ }
+
+ private Class<?> getFileInterfaceClass() throws Exception {
+ for (Class<?> innerClass : IndexFetcher.class.getDeclaredClasses()) {
+ if (innerClass.getSimpleName().equals("FileInterface")) {
+ return innerClass;
+ }
+ }
+ throw new AssertionError("FileInterface not found");
+ }
+
+ /** Creates a mock FileInterface that captures written bytes to the provided output stream. */
+ private Object createMockFileInterface(ByteArrayOutputStream output) throws Exception {
+ Class<?> fileInterfaceClass = getFileInterfaceClass();
+
+ return java.lang.reflect.Proxy.newProxyInstance(
+ fileInterfaceClass.getClassLoader(),
+ new Class<?>[] {fileInterfaceClass},
+ (proxy, method, args) -> {
+ switch (method.getName()) {
+ case "write":
+ byte[] buf = (byte[]) args[0];
+ int packetSize = (Integer) args[1];
+ output.write(buf, 0, packetSize);
+ return null;
+ case "sync":
+ case "close":
+ case "delete":
+ return null;
+ default:
+ throw new UnsupportedOperationException("Unexpected method: " + method.getName());
+ }
+ });
+ }
+}
(solr) 05/11: Test fix testDistributedQueryDoesNotReadFromZk (#4429)
Posted by ds...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit a9f697c28fe31a708e55d0f99c5c9ce87aef607b
Author: Matthew Biscocho <ml...@apache.org>
AuthorDate: Mon May 25 09:21:02 2026 -0400
Test fix testDistributedQueryDoesNotReadFromZk (#4429)
My suspicion is that there is a race condition making it flaky where the jetty node has not yet seen the updated state of the collection that it is ready but the client has because we were waiting on separate zkStateReader which is separate from the cached state from the actually jetty node and collection. So instead, we wait on that jetty nodes ready state using it's zkStateReader and not clients.
---
.../component/DistributedQueryComponentOptimizationTest.java | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/solr/core/src/test/org/apache/solr/handler/component/DistributedQueryComponentOptimizationTest.java b/solr/core/src/test/org/apache/solr/handler/component/DistributedQueryComponentOptimizationTest.java
index 7b65c88caac..02bcb11a84b 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/DistributedQueryComponentOptimizationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/DistributedQueryComponentOptimizationTest.java
@@ -728,7 +728,12 @@ public class DistributedQueryComponentOptimizationTest extends SolrCloudTestCase
CollectionAdminRequest.createCollection(secondColl, "conf", 1, 1)
.setCreateNodeSet(jettys.get(0).getNodeName())
.processAndWait(cluster.getSolrClient(), DEFAULT_TIMEOUT);
- cluster
+
+ // Wait on node 1's ZkStateReader (not the cluster client's) to check for ready state
+ JettySolrRunner nodeWithoutSecondColl = jettys.get(1);
+ nodeWithoutSecondColl
+ .getCoreContainer()
+ .getZkController()
.getZkStateReader()
.waitForState(
secondColl,
@@ -739,7 +744,6 @@ public class DistributedQueryComponentOptimizationTest extends SolrCloudTestCase
try {
// Node 1 hosts COLLECTION but not secondColl.
// Send a multi-collection query to trigger LazyCollectionRef get call
- JettySolrRunner nodeWithoutSecondColl = jettys.get(1);
try (SolrClient client =
new Http2SolrClient.Builder(nodeWithoutSecondColl.getBaseUrl().toString()).build()) {