This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 812de21  SOLR-15595: Partial results from shard queries needlessly 
discarded for queries without sort fields (#267)
812de21 is described below

commit 812de21e75de018982788b9017261a719e1a7e8f
Author: Michael Kosten <[email protected]>
AuthorDate: Fri Sep 3 15:02:01 2021 -0700

    SOLR-15595: Partial results from shard queries needlessly discarded for 
queries without sort fields (#267)
    
    * No longer skip merging partial shard results for non-sort queries
    
    * Add unit test for QueryComponent. Add javadoc for new SortSpec method. 
Refactor QueryComponent changes.
    
    * Improve javadocs. Fix parameter name in unit test class method.
    
    * Add comments to QueryComponent. Extract inner test class for added test 
and use a singleton for test ShardRequest.
---
 solr/CHANGES.txt                                   |   2 +
 .../solr/handler/component/QueryComponent.java     |  20 +++-
 .../src/java/org/apache/solr/search/SortSpec.java  |  23 ++++
 .../handler/component/MockResponseBuilder.java     |  70 ++++++++++++
 .../solr/handler/component/MockShardRequest.java   |  52 +++++++++
 .../handler/component/MockSortSpecBuilder.java     |  52 +++++++++
 .../QueryComponentPartialResultsTest.java          | 119 +++++++++++++++++++++
 .../apache/solr/search/SortSpecParsingTest.java    |  29 +++++
 8 files changed, 363 insertions(+), 4 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 37e4f0b..45b68e7 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -446,6 +446,8 @@ Bug Fixes
 
 * SOLR-14457: SolrClient leaks connections on compressed responses if the 
response is malformed (Houston Putman)
 
+* SOLR-15595: Partial results from shard queries needlessly discarded for 
queries without sort field (Michael Kosten via Mike Drob, Eric Pugh)
+
 Other Changes
 ---------------------
 * SOLR-15566: Clarify ref guide documentation about SQL queries with `SELECT 
*` requiring a `LIMIT` clause (Timothy Potter)
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java 
b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
index d89427a..241b966 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
@@ -955,11 +955,23 @@ public class QueryComponent extends SearchComponent
 
         @SuppressWarnings("unchecked")
         NamedList<List<Object>> sortFieldValues = 
(NamedList<List<Object>>)(srsp.getSolrResponse().getResponse().get("sort_values"));
-        if ((null == sortFieldValues || sortFieldValues.size()==0) && // we 
bypass merging this response only if it's partial itself
-                            thisResponseIsPartial) { // but not the previous 
one!!
-          continue; //fsv timeout yields empty sort_vlaues
+        if (null == sortFieldValues) {
+          sortFieldValues = new NamedList<>();
         }
-        NamedList<List<Object>> unmarshalledSortFieldValues = 
unmarshalSortValues(ss, sortFieldValues, schema);
+
+        // if the SortSpec contains a field besides score or the Lucene docid, 
then the values will need to be unmarshalled from
+        // sortFieldValues.
+        boolean needsUnmarshalling = ss.includesNonScoreOrDocField();
+
+        // if we need to unmarshal the sortFieldValues for sorting but we have 
none, which can happen if partial results are
+        // being returned from the shard, then skip merging the results for 
the shard. This avoids an exception below.
+        // if the shard returned partial results but we don't need to 
unmarshal (a normal scoring query), then merge what we got.
+        if (thisResponseIsPartial && sortFieldValues.size() == 0 && 
needsUnmarshalling) {
+          continue;
+        }
+
+        // Checking needsUnmarshalling saves on iterating the SortFields in 
the SortSpec again.
+        NamedList<List<Object>> unmarshalledSortFieldValues = 
needsUnmarshalling ? unmarshalSortValues(ss, sortFieldValues, schema) : new 
NamedList<>();
 
         // go through every doc in this response, construct a ShardDoc, and
         // put it in the priority queue so it can be ordered.
diff --git a/solr/core/src/java/org/apache/solr/search/SortSpec.java 
b/solr/core/src/java/org/apache/solr/search/SortSpec.java
index b79ed0a..afbfb36 100644
--- a/solr/core/src/java/org/apache/solr/search/SortSpec.java
+++ b/solr/core/src/java/org/apache/solr/search/SortSpec.java
@@ -77,6 +77,29 @@ public class SortSpec
   }
 
   /**
+   * Returns whether SortFields for the Sort includes a
+   * field besides SortField.Type.SCORE or SortField.Type.DOC
+   * @return true if SortFields contains a field besides score or the Lucene 
doc
+   */
+  public boolean includesNonScoreOrDocField() {
+    return includesNonScoreOrDocField(sort);
+  }
+
+  /**
+   * Returns whether SortFields for the Sort includes a
+   * field besides SortField.Type.SCORE or SortField.Type.DOC
+   * @param sort org.apache.lucene.search.Sort
+   * @return true if SortFields contains a field besides score or the Lucene 
doc
+   */
+  public static boolean includesNonScoreOrDocField(Sort sort) {
+    if (sort==null) return false;
+    for (SortField sf : sort.getSort()) {
+      if (sf.getType() != SortField.Type.SCORE && sf.getType() != 
SortField.Type.DOC) return true;
+    }
+    return false;
+  }
+
+  /**
    * Gets the Lucene Sort object, or null for the default sort
    * by score descending.
    */
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java 
b/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
new file mode 100644
index 0000000..9ed7d50
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/MockResponseBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.StrField;
+import org.apache.solr.search.SortSpec;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MockResponseBuilder extends ResponseBuilder {
+
+    private MockResponseBuilder(SolrQueryRequest request, SolrQueryResponse 
response, List<SearchComponent> components) {
+        super(request, response, components);
+    }
+
+    public static MockResponseBuilder create() {
+
+        // the mocks
+        SolrQueryRequest request = Mockito.mock(SolrQueryRequest.class);
+        SolrQueryResponse response = Mockito.mock(SolrQueryResponse.class);
+        IndexSchema indexSchema = Mockito.mock(IndexSchema.class);
+        SolrParams params = Mockito.mock(SolrParams.class);
+
+        // SchemaField must be concrete due to field access
+        SchemaField uniqueIdField = new SchemaField("id", new StrField());
+
+        // we need this because QueryComponent adds a property to it.
+        NamedList<Object> responseHeader = new NamedList<>();
+
+        // the mock implementations
+        Mockito.when(request.getSchema()).thenReturn(indexSchema);
+        
Mockito.when(indexSchema.getUniqueKeyField()).thenReturn(uniqueIdField);
+        
Mockito.when(params.getBool(ShardParams.SHARDS_INFO)).thenReturn(false);
+        Mockito.when(request.getParams()).thenReturn(params);
+        Mockito.when(response.getResponseHeader()).thenReturn(responseHeader);
+
+        List<SearchComponent> components = new ArrayList<>();
+        return new MockResponseBuilder(request, response, components);
+
+    }
+
+    public MockResponseBuilder withSortSpec(SortSpec sortSpec) {
+        this.setSortSpec(sortSpec);
+        return this;
+    }
+
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java 
b/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
new file mode 100644
index 0000000..d33c4e2
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/component/MockShardRequest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.client.solrj.SolrResponse;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.NamedList;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+public class MockShardRequest extends ShardRequest {
+
+    public static MockShardRequest create() {
+        MockShardRequest mockShardRequest = new MockShardRequest();
+        mockShardRequest.responses = new ArrayList<>();
+        return mockShardRequest;
+    }
+
+    public MockShardRequest withShardResponse(NamedList<Object> 
responseHeader, SolrDocumentList solrDocuments) {
+        ShardResponse shardResponse = buildShardResponse(responseHeader, 
solrDocuments);
+        responses.add(shardResponse);
+        return this;
+    }
+
+    private ShardResponse buildShardResponse(NamedList<Object> responseHeader, 
SolrDocumentList solrDocuments) {
+        SolrResponse solrResponse = Mockito.mock(SolrResponse.class);
+        ShardResponse shardResponse = new ShardResponse();
+        NamedList<Object> response = new NamedList<>();
+        response.add("response", solrDocuments);
+        shardResponse.setSolrResponse(solrResponse);
+        response.add("responseHeader", responseHeader);
+        Mockito.when(solrResponse.getResponse()).thenReturn(response);
+
+        return shardResponse;
+    }
+
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java 
b/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
new file mode 100644
index 0000000..3c65080
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/MockSortSpecBuilder.java
@@ -0,0 +1,52 @@
+/*
+ * 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.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.search.SortSpec;
+import org.mockito.Mockito;
+
+public class MockSortSpecBuilder {
+    private final SortSpec sortSpec;
+
+    public MockSortSpecBuilder() {
+        this.sortSpec = Mockito.mock(SortSpec.class);
+        Mockito.when(sortSpec.getCount()).thenReturn(10);
+    }
+
+    public static MockSortSpecBuilder create() {
+        return new MockSortSpecBuilder();
+    }
+
+    public MockSortSpecBuilder withSortFields(SortField[] sortFields) {
+        Sort sort = Mockito.mock(Sort.class);
+        Mockito.when(sort.getSort()).thenReturn(sortFields);
+        Mockito.when(sortSpec.getSort()).thenReturn(sort);
+        return this;
+    }
+
+    public MockSortSpecBuilder withIncludesNonScoreOrDocSortField(boolean 
include) {
+        
Mockito.when(sortSpec.includesNonScoreOrDocField()).thenReturn(include);
+        return this;
+    }
+
+    public SortSpec build() {
+        return sortSpec;
+    }
+
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/QueryComponentPartialResultsTest.java
 
b/solr/core/src/test/org/apache/solr/handler/component/QueryComponentPartialResultsTest.java
new file mode 100644
index 0000000..952e720
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/QueryComponentPartialResultsTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.lucene.search.SortField;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.search.SortSpec;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class QueryComponentPartialResultsTest extends SolrTestCaseJ4 {
+    private static final String SORT_FIELD_NAME = "category";
+    private static final int shard1Size = 2;
+    private static final int shard2Size = 3;
+
+    private static int id = 0;
+    private static ShardRequest shardRequestWithPartialResults;
+
+    @BeforeClass
+    public static void setup() {
+        assumeWorkingMockito();
+        shardRequestWithPartialResults = 
createShardRequestWithPartialResults();
+    }
+
+    @Test
+    public void includesPartialShardResultWhenUsingImplicitScoreSort() {
+        SortSpec sortSpec = MockSortSpecBuilder.create()
+                .withIncludesNonScoreOrDocSortField(false)
+                .build();
+        testPartialResultsForSortSpec(sortSpec, true);
+    }
+
+    @Test
+    public void includesPartialShardResultWhenUsingExplicitScoreSort() {
+        SortSpec sortSpec = MockSortSpecBuilder.create()
+                .withSortFields(new SortField[]{SortField.FIELD_SCORE})
+                .withIncludesNonScoreOrDocSortField(false)
+                .build();
+        testPartialResultsForSortSpec(sortSpec, true);
+    }
+
+    @Test
+    public void includesPartialShardResultWhenUsingExplicitDocSort() {
+        SortSpec sortSpec = MockSortSpecBuilder.create()
+                .withSortFields(new SortField[]{SortField.FIELD_DOC})
+                .withIncludesNonScoreOrDocSortField(false)
+                .build();
+        testPartialResultsForSortSpec(sortSpec, true);
+    }
+
+    @Test
+    public void excludesPartialShardResultWhenUsingNonScoreOrDocSortField() {
+        SortField sortField = new SortField(SORT_FIELD_NAME, 
SortField.Type.INT);
+        SortSpec sortSpec = MockSortSpecBuilder.create()
+                .withSortFields(new SortField[]{sortField})
+                .withIncludesNonScoreOrDocSortField(true)
+                .build();
+        testPartialResultsForSortSpec(sortSpec, false);
+    }
+
+    private void testPartialResultsForSortSpec(SortSpec sortSpec, boolean 
shouldIncludePartialShardResult) {
+
+        MockResponseBuilder responseBuilder = 
MockResponseBuilder.create().withSortSpec(sortSpec);
+
+        QueryComponent queryComponent = new QueryComponent();
+        queryComponent.mergeIds(responseBuilder, 
shardRequestWithPartialResults);
+
+        // do we have the expected document count?
+        // if results are not merged for the partial results shard, then the 
total doc count will exclude them
+        if (shouldIncludePartialShardResult) {
+            assertEquals(shard1Size + shard2Size, 
responseBuilder.getResponseDocs().size());
+        } else {
+            assertEquals(shard2Size, responseBuilder.getResponseDocs().size());
+        }
+    }
+
+    private static ShardRequest createShardRequestWithPartialResults() {
+        final NamedList<Object> shard1ResponseHeader = new NamedList<>();
+        final NamedList<Object> shard2ResponseHeader = new NamedList<>();
+
+        // the results from shard1 are marked partial
+        
shard1ResponseHeader.add(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE);
+
+        return MockShardRequest.create()
+                .withShardResponse(shard1ResponseHeader, 
createSolrDocumentList(shard1Size))
+                .withShardResponse(shard2ResponseHeader, 
createSolrDocumentList(shard2Size));
+    }
+
+    private static SolrDocumentList createSolrDocumentList(int size) {
+        SolrDocumentList solrDocuments = new SolrDocumentList();
+        for(int i = 0; i < size; i++) {
+            SolrDocument solrDocument = new SolrDocument();
+            solrDocument.addField("id", id++);
+            solrDocument.addField("score", (float)id);
+            solrDocument.addField(SORT_FIELD_NAME, id);
+            solrDocuments.add(solrDocument);
+        }
+        return solrDocuments;
+    }
+
+}
diff --git a/solr/core/src/test/org/apache/solr/search/SortSpecParsingTest.java 
b/solr/core/src/test/org/apache/solr/search/SortSpecParsingTest.java
index a7eb5ed..25bcb7e 100644
--- a/solr/core/src/test/org/apache/solr/search/SortSpecParsingTest.java
+++ b/solr/core/src/test/org/apache/solr/search/SortSpecParsingTest.java
@@ -192,6 +192,35 @@ public class SortSpecParsingTest extends SolrTestCaseJ4 {
     assertNotNull(spec);
     assertNull(spec.getSort());
 
+    // test includesScore and includesNonScoreDocField methods
+    spec = doParseSortSpec("", req);
+    assertTrue(spec.includesScore());
+    assertFalse(spec.includesNonScoreOrDocField());
+
+    spec = doParseSortSpec("score desc", req);
+    assertTrue(spec.includesScore());
+    assertFalse(spec.includesNonScoreOrDocField());
+
+    spec = doParseSortSpec("score desc, _docid_ asc", req);
+    assertTrue(spec.includesScore());
+    assertFalse(spec.includesNonScoreOrDocField());
+
+    spec = doParseSortSpec("_docid_ desc", req);
+    assertFalse(spec.includesScore());
+    assertFalse(spec.includesNonScoreOrDocField());
+
+    spec = doParseSortSpec("weight desc", req);
+    assertFalse(spec.includesScore());
+    assertTrue(spec.includesNonScoreOrDocField());
+
+    spec = doParseSortSpec("weight desc, score desc", req);
+    assertTrue(spec.includesScore());
+    assertTrue(spec.includesNonScoreOrDocField());
+
+    spec = doParseSortSpec("weight desc, _docid_ desc", req);
+    assertFalse(spec.includesScore());
+    assertTrue(spec.includesNonScoreOrDocField());
+
     req.close();
   }
 

Reply via email to