This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch branch_10_1 in repository https://gitbox.apache.org/repos/asf/solr.git
commit e0290fa7586d7e29dfbf481a4f20d09a484e6cc6 Author: Nick <[email protected]> AuthorDate: Wed Sep 30 14:04:40 2026 -0600 SOLR-8291, SOLR-13217: Skip null per-leaf export bitsets (#4953) (cherry picked from commit 56ec140e3636d5f4150fa87fbf7103529536ac99) (cherry picked from commit 3c5f6397082cd8a0138b23f7d91eedaadff284e9) --- changelog/unreleased/SOLR-8291.yml | 9 ++ .../apache/solr/handler/export/ExportWriter.java | 5 + .../collection1/conf/schema-export-usefilter.xml | 22 ++++ .../conf/solrconfig-export-usefilter.xml | 43 ++++++++ .../solr/handler/export/TestExportWriter.java | 43 ++++++++ .../TestExportWriterUseFilterForSortedQuery.java | 113 +++++++++++++++++++++ 6 files changed, 235 insertions(+) diff --git a/changelog/unreleased/SOLR-8291.yml b/changelog/unreleased/SOLR-8291.yml new file mode 100644 index 00000000000..58cd1447f0b --- /dev/null +++ b/changelog/unreleased/SOLR-8291.yml @@ -0,0 +1,9 @@ +title: Skip null per-leaf bitsets in /export instead of throwing NPE. +type: fixed +authors: + - name: Nick Shanin +links: + - name: SOLR-8291 + url: https://issues.apache.org/jira/browse/SOLR-8291 + - name: SOLR-13217 + url: https://issues.apache.org/jira/browse/SOLR-13217 diff --git a/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java b/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java index 2b9731152fe..7b82ad28b14 100644 --- a/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java +++ b/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java @@ -857,6 +857,11 @@ public class ExportWriter implements SolrCore.RawWriter, Closeable { private void topDocs() throws IOException { try { + // null bitset = leaf was never collected + if (bits == null) { + index = -1; + return; + } queue.reset(); SortDoc top = queue.top(); this.sortDoc.setNextReader(context); diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-export-usefilter.xml b/solr/core/src/test-files/solr/collection1/conf/schema-export-usefilter.xml new file mode 100644 index 00000000000..e49148e503e --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/schema-export-usefilter.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!-- + 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. +--> +<schema name="export-usefilter" version="1.7"> + <fieldType name="string" class="solr.StrField" /> + <field name="id" type="string" indexed="true" stored="true" docValues="true" required="true" /> + <uniqueKey>id</uniqueKey> +</schema> diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-export-usefilter.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-export-usefilter.xml new file mode 100644 index 00000000000..e29ec1b0640 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-export-usefilter.xml @@ -0,0 +1,43 @@ +<?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. +--> + +<!-- Minimal export config that forces the useFilterForSortedQuery / sortDocSet + path. solrconfig-sortingresponse.xml has neither the flag nor a filterCache, + so it cannot reproduce SOLR-8291. --> +<config> + <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion> + <dataDir>${solr.data.dir:}</dataDir> + <indexConfig> + <mergePolicyFactory class="${solr.tests.mergePolicyFactory:org.apache.solr.index.LogDocMergePolicyFactory}" /> + </indexConfig> + <directoryFactory name="DirectoryFactory" class="${solr.directoryFactory:solr.MockDirectoryFactory}"/> + <schemaFactory class="ClassicIndexSchemaFactory"/> + <requestHandler name="/select" class="solr.SearchHandler" /> + + <query> + <enableLazyFieldLoading>true</enableLazyFieldLoading> + <queryResultWindowSize>20</queryResultWindowSize> + <queryResultMaxDocsCached>200</queryResultMaxDocsCached> + <filterCache size="50" initialSize="50" autowarmCount="0"/> + <useFilterForSortedQuery>true</useFilterForSortedQuery> + <documentCache size="512" + initialSize="512" + autowarmCount="0"/> + </query> +</config> diff --git a/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java b/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java index 5954a72a690..61384d50553 100644 --- a/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java +++ b/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java @@ -1754,4 +1754,47 @@ public class TestExportWriter extends SolrTestCaseJ4 { "Error message should mention DocValues requirement", ex.getMessage().contains("DocValues")); } + + /** + * SOLR-13217: collapse only replays group heads, so trailing leaves with no heads leave a null + * export bitset. {@code /export} must skip those leaves instead of NPEing in {@code + * BitSetIterator}. + */ + @Test + public void testCollapseWithTrailingEmptyLeaf() throws Exception { + assertU(adoc("id", "1", "group_s_dv", "A", "intdv", "10")); + assertU(commit()); + assertU(adoc("id", "2", "group_s_dv", "A", "intdv", "1")); + assertU(commit()); + + assertJQ( + req( + "q", + "*:*", + "fq", + "{!collapse field=group_s_dv sort='intdv desc'}", + "fl", + "id", + "sort", + "id asc"), + "/response/numFound==1", + "/response/docs/[0]/id=='1'"); + + String resp = + h.query( + req( + "q", + "*:*", + "qt", + "/export", + "fq", + "{!collapse field=group_s_dv sort='intdv desc'}", + "fl", + "id,group_s_dv,intdv", + "sort", + "id asc")); + assertJsonEquals( + resp, + "{\"responseHeader\":{\"status\":0},\"response\":{\"numFound\":1,\"docs\":[{\"id\":\"1\",\"group_s_dv\":\"A\",\"intdv\":10}]}}"); + } } diff --git a/solr/core/src/test/org/apache/solr/handler/export/TestExportWriterUseFilterForSortedQuery.java b/solr/core/src/test/org/apache/solr/handler/export/TestExportWriterUseFilterForSortedQuery.java new file mode 100644 index 00000000000..e1dc726d956 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/handler/export/TestExportWriterUseFilterForSortedQuery.java @@ -0,0 +1,113 @@ +/* + * 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.export; + +import org.apache.solr.SolrTestCase; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.request.QueryRequest; +import org.apache.solr.client.solrj.request.SolrQuery; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.client.solrj.response.json.CanonicalJsonResponseParser; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.index.LogDocMergePolicyFactory; +import org.apache.solr.util.EmbeddedSolrServerTestRule; +import org.apache.solr.util.ExternalPaths; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +/** + * SOLR-8291: {@code useFilterForSortedQuery=true} sends {@code /export} through {@code sortDocSet}, + * which never calls {@code ExportCollector.getLeafCollector} for trailing unmatched leaves. The + * writer must treat those null bitsets as empty leaves instead of constructing a {@code + * BitSetIterator}. + */ +public class TestExportWriterUseFilterForSortedQuery extends SolrTestCase { + + @ClassRule + public static final EmbeddedSolrServerTestRule solrTestRule = new EmbeddedSolrServerTestRule(); + + @BeforeClass + public static void beforeClass() throws Exception { + System.setProperty("solr.tests.mergePolicyFactory", LogDocMergePolicyFactory.class.getName()); + solrTestRule.startSolr(); + solrTestRule + .newCollection() + .withConfigSet( + ExternalPaths.SOURCE_HOME.resolve("core/src/test-files/solr/collection1/conf")) + .withConfigFile("solrconfig-export-usefilter.xml") + .withSchemaFile("schema-export-usefilter.xml") + .create(); + } + + @Before + public void clearIndex() throws Exception { + SolrClient client = solrTestRule.getSolrClient(); + client.deleteByQuery("*:*"); + client.commit(); + } + + @Test + public void testZeroHitsDoesNotNpe() throws Exception { + SolrClient client = solrTestRule.getSolrClient(); + client.add(doc("1")); + client.commit(); + + QueryResponse response = query(client, "id:does-not-exist"); + assertEquals(0, response.getResults().getNumFound()); + } + + @Test + public void testHitsOnlyInEarlierSegment() throws Exception { + SolrClient client = solrTestRule.getSolrClient(); + client.add(doc("1")); + client.commit(); + client.add(doc("2")); + client.commit(); + + QueryResponse response = query(client, "id:1"); + assertEquals(1, response.getResults().getNumFound()); + assertEquals("1", response.getResults().get(0).getFieldValue("id")); + + // Null must mean "this leaf is empty", not "skip the rest of the index". + response = query(client, "*:*"); + assertEquals(2, response.getResults().getNumFound()); + assertEquals("1", response.getResults().get(0).getFieldValue("id")); + assertEquals("2", response.getResults().get(1).getFieldValue("id")); + } + + private static SolrQuery exportQuery(String queryString) { + SolrQuery query = new SolrQuery(queryString); + query.setRequestHandler("/export"); + query.setFields("id"); + query.setSort("id", SolrQuery.ORDER.asc); + return query; + } + + private static QueryResponse query(SolrClient client, String queryString) throws Exception { + QueryRequest request = new QueryRequest(exportQuery(queryString)); + request.setResponseParser(new CanonicalJsonResponseParser()); + return request.process(client); + } + + private static SolrInputDocument doc(String id) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", id); + return doc; + } +}
