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

hossman 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 59a5913  SOLR-11921: Move "cursorMark" logic from QueryComponent to 
SearchHandler so it can work with things like QueryElevationComponent that 
modify the SortSpec in prepare(), as well as possible custom "search" 
components other then QueryComponent
59a5913 is described below

commit 59a59138668e9513060824d85ee375a1723f4bfb
Author: Chris Hostetter <[email protected]>
AuthorDate: Mon Apr 5 09:57:24 2021 -0700

    SOLR-11921: Move "cursorMark" logic from QueryComponent to SearchHandler so 
it can work with things like QueryElevationComponent that modify the SortSpec 
in prepare(), as well as possible custom "search" components other then 
QueryComponent
---
 solr/CHANGES.txt                                   |   3 +
 .../solr/handler/component/QueryComponent.java     |  10 +-
 .../solr/handler/component/SearchHandler.java      |  14 ++
 .../java/org/apache/solr/search/CursorMark.java    |  17 +-
 .../collection1/conf/solrconfig-deeppaging.xml     |  15 ++
 .../src/test/org/apache/solr/CursorPagingTest.java | 180 +++++++++++++++--
 .../solr/TestCursorMarkWithoutUniqueKey.java       |   5 +-
 .../apache/solr/cloud/DistribCursorPagingTest.java | 220 ++++++++++++++++-----
 .../solr/handler/component/BadComponentTest.java   |   6 +
 .../component/QueryElevationComponentTest.java     | 101 ++++++++++
 .../org/apache/solr/search/CursorMarkTest.java     |  20 +-
 11 files changed, 490 insertions(+), 101 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 7ce63ff..7ad7907 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -321,6 +321,9 @@ Bug Fixes
 * SOLR-15273: Fix NullPointerException in StoredFieldsShardResponseProcessor 
that happened when a field name alias is
   used for the unique key field in searches with distributed result grouping. 
(limingnihao via Christine Poerschke)
 
+* SOLR-11921: Move "cursorMark" logic from QueryComponent to SearchHandler so 
it can work with things like QueryElevationComponent
+  that modify the SortSpec in prepare(), as well as possible custom "search" 
components other then QueryComponent.  (hossman)
+
 Other Changes
 ---------------------
 * SOLR-15118: Deprecate CollectionAdminRequest.getV2Request(). (Jason 
Gerlowski)
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 a08c41d..6622c30 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
@@ -189,14 +189,6 @@ public class QueryComponent extends SearchComponent
       rb.setSortSpec( parser.getSortSpec(true) );
       rb.setQparser(parser);
 
-      final String cursorStr = 
rb.req.getParams().get(CursorMarkParams.CURSOR_MARK_PARAM);
-      if (null != cursorStr) {
-        final CursorMark cursorMark = new CursorMark(rb.req.getSchema(),
-                                                     rb.getSortSpec());
-        cursorMark.parseSerializedTotem(cursorStr);
-        rb.setCursorMark(cursorMark);
-      }
-
       String[] fqs = req.getParams().getParams(CommonParams.FQ);
       if (fqs!=null && fqs.length!=0) {
         List<Query> filters = rb.getFilters();
@@ -240,7 +232,7 @@ public class QueryComponent extends SearchComponent
     SolrQueryRequest req = rb.req;
     SolrParams params = req.getParams();
 
-    if (null != rb.getCursorMark()) {
+    if (null != params.get(CursorMarkParams.CURSOR_MARK_PARAM)) {
       // It's hard to imagine, conceptually, what it would mean to combine
       // grouping with a cursor - so for now we just don't allow the 
combination at all
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can not 
use Grouping with " +
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java 
b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
index c0c536f..affbfd6 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
@@ -39,7 +39,9 @@ import org.apache.solr.pkg.PackageListeners;
 import org.apache.solr.pkg.PackageLoader;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.search.CursorMark;
 import org.apache.solr.search.SolrQueryTimeoutImpl;
+import org.apache.solr.search.SortSpec;
 import org.apache.solr.search.facet.FacetModule;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
@@ -342,6 +344,18 @@ public class SearchHandler extends RequestHandlerBase 
implements SolrCoreAware,
       subt.stop();
     }
 
+    { // Once all of our components have been prepared, check if this requset 
involves a SortSpec.
+      // If it does, and if our request includes a cursorMark param, then 
parse & init the CursorMark state
+      // (This must happen after the prepare() of all components, because any 
component may have modified the SortSpec)
+      final SortSpec spec = rb.getSortSpec();
+      final String cursorStr = 
rb.req.getParams().get(CursorMarkParams.CURSOR_MARK_PARAM);
+      if (null != spec && null != cursorStr) {
+        final CursorMark cursorMark = new CursorMark(rb.req.getSchema(), spec);
+        cursorMark.parseSerializedTotem(cursorStr);
+        rb.setCursorMark(cursorMark);
+      }
+    }
+    
     if (!rb.isDistrib) {
       // a normal non-distributed request
 
diff --git a/solr/core/src/java/org/apache/solr/search/CursorMark.java 
b/solr/core/src/java/org/apache/solr/search/CursorMark.java
index 8048471..eb33b40 100644
--- a/solr/core/src/java/org/apache/solr/search/CursorMark.java
+++ b/solr/core/src/java/org/apache/solr/search/CursorMark.java
@@ -101,10 +101,10 @@ public final class CursorMark {
     }
 
     if (sort.getSort().length != sortSpec.getSchemaFields().size()) {
-        throw new SolrException(ErrorCode.SERVER_ERROR,
-                                "Cursor SortSpec failure: sort length != 
SchemaFields: " 
-                                + sort.getSort().length + " != " + 
-                                sortSpec.getSchemaFields().size());
+      throw new SolrException(ErrorCode.SERVER_ERROR,
+                              "Cursor SortSpec failure: sort length != 
SchemaFields: " 
+                              + sort.getSort().length + " != " + 
+                              sortSpec.getSchemaFields().size());
     }
 
     this.sortSpec = sortSpec;
@@ -144,7 +144,12 @@ public final class CursorMark {
     if (null == input) {
       this.values = null;
     } else {
-      assert input.size() == sortSpec.getSort().getSort().length;
+      if (input.size() != sortSpec.getSort().getSort().length) {
+        throw new SolrException(ErrorCode.SERVER_ERROR,
+                                "Cursor SortSpec failure: sort values != sort 
length: "
+                                + input.size() + " != " + 
sortSpec.getSort().getSort().length);
+      }
+      
       // defensive copy
       this.values = new ArrayList<>(input);
     }
@@ -204,7 +209,7 @@ public final class CursorMark {
                               "'"+CURSOR_MARK_NEXT+"' returned by a previous 
search: "
                               + serialized, ex);
     }
-    assert null != pieces : "pieces wasn't parsed?";
+    assert null != pieces : "pieces wasn't parsed, nor exception thrown?";
 
     if (sortFields.length != pieces.size()) {
       throw new SolrException(ErrorCode.BAD_REQUEST,
diff --git 
a/solr/core/src/test-files/solr/collection1/conf/solrconfig-deeppaging.xml 
b/solr/core/src/test-files/solr/collection1/conf/solrconfig-deeppaging.xml
index 6825dab..d4b5017 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig-deeppaging.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-deeppaging.xml
@@ -57,5 +57,20 @@
   </query>
 
   <requestHandler name="/select" class="solr.SearchHandler" default="true" />
+
+  <!-- elevation should have no (adverse) impact on deep paging -->
+  <searchComponent name="elevate" 
class="org.apache.solr.handler.component.QueryElevationComponent" >
+    <!-- NOTE: we don't particularly care about the elevate.xml or 
queryFieldType used,
+         deep paging tests will only worry about using elevateIds.
+    -->
+    <str name="queryFieldType">str</str>
+    <str name="config-file">elevate.xml</str><!-- neccessary due to SOLR-14970 
-->
+  </searchComponent>
+  <requestHandler name="/elevate" class="solr.SearchHandler">
+    <arr name="last-components">
+      <str>elevate</str>
+    </arr>
+  </requestHandler>
+
 </config>
 
diff --git a/solr/core/src/test/org/apache/solr/CursorPagingTest.java 
b/solr/core/src/test/org/apache/solr/CursorPagingTest.java
index 169aaa7..3375b8e 100644
--- a/solr/core/src/test/org/apache/solr/CursorPagingTest.java
+++ b/solr/core/src/test/org/apache/solr/CursorPagingTest.java
@@ -25,6 +25,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
@@ -46,6 +47,9 @@ import org.apache.solr.util.LogLevel;
 import org.junit.After;
 import org.junit.BeforeClass;
 
+import org.apache.commons.lang3.StringUtils;
+
+import static org.apache.solr.common.params.SolrParams.wrapDefaults;
 import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_NEXT;
 import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_PARAM;
 import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_START;
@@ -88,7 +92,7 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
     } else {
       assertU(commit());
     }
-      assertU(commit());
+    assertU(commit());
 
     // empty, blank, or bogus cursor
     for (String c : new String[] { "", "   ", "all the docs please!"}) {
@@ -116,6 +120,30 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
                       GroupParams.GROUP_FIELD, "str",
                       CURSOR_MARK_PARAM, CURSOR_MARK_START),
                ErrorCode.BAD_REQUEST, "Grouping");
+
+    // if a user specifies a 'bogus' cursorMark param, this should error 
*only* if some other component
+    // cares about (and parses) a SortSpec in it's prepare() method.
+    // (the existence of a 'sort' param shouldn't make a diff ... unless it 
makes a diff to a component being used,
+    // which it doesn't for RTG)
+    assertU(adoc("id", "yyy", "str", "y", "float", "3", "int", "-3"));
+    if (random().nextBoolean()) {
+      assertU(commit());
+    }
+    for (SolrParams p : Arrays.asList(params(),
+                                      params(CURSOR_MARK_PARAM, "gibberish"),
+                                      params(CURSOR_MARK_PARAM, "gibberish",
+                                             "sort", "id asc"))) {
+      assertJQ(req(p,
+                   "qt","/get",
+                   "fl", "id",
+                   "id","yyy") 
+               , "=={'doc':{'id':'yyy'}}");
+      assertJQ(req(p,
+                   "qt","/get",
+                   "fl", "id",
+                   "id","xxx") // doesn't exist in our collection
+               , "=={'doc':null}");
+    }
   }
 
 
@@ -648,12 +676,24 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
       for (String order : new String[] {" asc", " desc"}) {
         String sort = f + order + ("id".equals(f) ? "" : ", id" + order);
         String rows = "" + TestUtil.nextInt(random(), 13, 50);
-        SentinelIntSet ids = assertFullWalkNoDups(totalDocs, 
-                                                  params("q", "*:*",
-                                                         "fl","id",
-                                                         "rows",rows,
-                                                         "sort",sort));
+        final SolrParams main = params("q", "*:*",
+                                       "fl","id",
+                                       "rows",rows,
+                                       "sort",sort);
+        final SentinelIntSet ids = assertFullWalkNoDups(totalDocs, main);
         assertEquals(initialDocs, ids.size());
+
+        // same query, now with QEC ... verify we get all the same docs, but 
the (expected) elevated docs are first...
+        final SentinelIntSet elevated = 
assertFullWalkNoDupsElevated(wrapDefaults(params("qt", "/elevate",
+                                                                               
          "fl","id,[elevated]",
+                                                                               
          "forceElevation","true",
+                                                                               
          "elevateIds", "50,20,80"),
+                                                                               
   main),
+                                                                     ids);
+        assertTrue(elevated.exists(50));
+        assertTrue(elevated.exists(20));
+        assertTrue(elevated.exists(80));
+        assertEquals(3, elevated.size());
       }
     }
 
@@ -671,16 +711,32 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
       final String fl = random().nextBoolean() ? "id" : "id,score";
       final boolean matchAll = random().nextBoolean();
       final String q = matchAll ? "*:*" : buildRandomQuery();
-
-      SentinelIntSet ids = assertFullWalkNoDups(totalDocs, 
-                                                params("q", q,
-                                                       "fl",fl,
-                                                       "rows",rows,
-                                                       "sort",sort));
+      final SolrParams main = params("q", q,
+                                     "fl",fl,
+                                     "rows",rows,
+                                     "sort",sort);
+      final SentinelIntSet ids = assertFullWalkNoDups(totalDocs, main);
       if (matchAll) {
         assertEquals(totalDocs, ids.size());
       }
 
+      // same query, now with QEC ... verify we get all the same docs, but the 
(expected) elevated docs are first...
+      // first we have to build a set of ids to elevate, from the set of ids 
known to match query...
+      final int[] expectedElevated = pickElevations(TestUtil.nextInt(random(), 
3, 33), ids);
+      final SentinelIntSet elevated = assertFullWalkNoDupsElevated
+        (wrapDefaults(params("qt", "/elevate",
+                             "fl", fl + ",[elevated]",
+                             // HACK: work around SOLR-15307... same results 
should match, just not same order
+                             "sort", (sort.startsWith("score asc") ? "score 
desc, " + sort : sort),
+                             "forceElevation","true",
+                             "elevateIds", 
StringUtils.join(expectedElevated,',')),
+                      main),
+         ids);
+      for (int expected : expectedElevated) {
+        assertTrue(expected + " wasn't elevated even though it should have 
been",
+                   elevated.exists(expected));
+      }
+      assertEquals(expectedElevated.length, elevated.size());
     }
   }
 
@@ -730,12 +786,74 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
    * and then continuously walks the results using {@link 
CursorMarkParams#CURSOR_MARK_START} as long
    * as a non-0 number of docs ar returned.  This method records the the set 
of all id's
    * (must be positive ints) encountered and throws an assertion failure if 
any id is
+   * encountered more than once, or if an id is encountered which is not 
expected, 
+   * or if an id is <code>[elevated]</code> and comes "after" any ids which 
were not <code>[elevated]</code>
+   * 
+   *
+   * @returns set of all elevated ids encountered in the walk
+   * @see #assertFullWalkNoDups(SolrParams,Consumer)
+   */
+  public SentinelIntSet assertFullWalkNoDupsElevated(final SolrParams params, 
final SentinelIntSet allExpected)
+    throws Exception {
+
+    final SentinelIntSet ids = new SentinelIntSet(allExpected.size(), -1);
+    final SentinelIntSet idsElevated = new SentinelIntSet(32, -1);
+
+    assertFullWalkNoDups(params, (doc) -> {
+        final int id = Integer.parseInt(doc.get("id").toString());
+        final boolean elevated = 
Boolean.parseBoolean(doc.getOrDefault("[elevated]","false").toString());
+        assertTrue(id + " is not expected to match query",
+                   allExpected.exists(id));
+        assertFalse("walk already seen: " + id,
+                    ids.exists(id));
+        if (elevated) {
+          assertEquals("id is elevated, but we've already seen non elevated 
ids: " + id,
+                       idsElevated.size(), ids.size());
+          idsElevated.put(id);
+        }
+        ids.put(id);
+      });
+    assertEquals("total number of ids seen did not match expected",
+                 allExpected.size(), ids.size());
+    
+    return idsElevated;
+  }
+
+  
+  /**
+   * Given a set of params, executes a cursor query using {@link 
CursorMarkParams#CURSOR_MARK_START}
+   * and then continuously walks the results using {@link 
CursorMarkParams#CURSOR_MARK_START} as long
+   * as a non-0 number of docs ar returned.  This method records the the set 
of all id's
+   * (must be positive ints) encountered and throws an assertion failure if 
any id is
    * encountered more than once, or if the set grows above maxSize
+   *
+   * @returns set of all ids encountered in the walk
+   * @see #assertFullWalkNoDups(SolrParams,Consumer)
    */
   public SentinelIntSet assertFullWalkNoDups(int maxSize, SolrParams params) 
     throws Exception {
-
-    SentinelIntSet ids = new SentinelIntSet(maxSize, -1);
+    final SentinelIntSet ids = new SentinelIntSet(maxSize, -1);
+    assertFullWalkNoDups(params, (doc) -> {
+        int id = Integer.parseInt(doc.get("id").toString());
+        assertFalse("walk already seen: " + id, ids.exists(id));
+        ids.put(id);
+        assertFalse("id set bigger then max allowed ("+maxSize+"): " + 
ids.size(),
+                    maxSize < ids.size());
+        
+      });
+    return ids;
+  }
+  
+  /**
+   * Given a set of params, executes a cursor query using {@link 
CursorMarkParams#CURSOR_MARK_START}
+   * and then continuously walks the results using {@link 
CursorMarkParams#CURSOR_MARK_START} as long
+   * as a non-0 number of docs ar returned.  This method does some basic 
validation of each response, and then 
+   * passes each doc encountered (in order returned) to the specified 
Consumer, which may throw an assertion if 
+   * there is a problem. 
+   */
+  public void assertFullWalkNoDups(SolrParams params, 
Consumer<Map<Object,Object>> consumer)
+    throws Exception {
+    
     String cursorMark = CURSOR_MARK_START;
     int docsOnThisPage = Integer.MAX_VALUE;
     while (0 < docsOnThisPage) {
@@ -760,15 +878,10 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
                      cursorMark, nextCursorMark);
       }
       for (Map<Object,Object> doc : docs) {
-        int id = Integer.parseInt(doc.get("id").toString());
-        assertFalse("walk already seen: " + id, ids.exists(id));
-        ids.put(id);
-        assertFalse("id set bigger then max allowed ("+maxSize+"): " + 
ids.size(),
-                    maxSize < ids.size());
+        consumer.accept(doc);
       }
       cursorMark = nextCursorMark;
     }
-    return ids;
   }
 
   /**
@@ -1055,4 +1168,31 @@ public class CursorPagingTest extends SolrTestCaseJ4 {
     return result.toString();
   }
 
+  /** 
+   * Given a set of id, picks some, semi-randomly, to use for elevation
+   */
+  public static int[] pickElevations(final int numToElevate, final 
SentinelIntSet ids) {
+    assert numToElevate < ids.size();
+    final int[] results = new int[numToElevate];
+    int slot = 0;
+    for (int key : ids.keys) {
+      if (key != ids.emptyVal) {
+        if (slot < results.length) {
+          // until results is full, take any value we can get in the 'next' 
slot...
+          results[slot] = key;
+        } else if (numToElevate * 2 < slot) {
+          // once we've done enough (extra) iters, break out with what we've 
got
+          break;
+        } else {
+          // otherwise, pick a random slot to overwrite .. maybe
+          if (random().nextBoolean()) {
+            results[random().nextInt(results.length)] = key;
+          }
+        }
+        slot++;
+      }
+    }
+    return results;
+  }
+  
 }
diff --git 
a/solr/core/src/test/org/apache/solr/TestCursorMarkWithoutUniqueKey.java 
b/solr/core/src/test/org/apache/solr/TestCursorMarkWithoutUniqueKey.java
index cedc9c8..a98d44f 100644
--- a/solr/core/src/test/org/apache/solr/TestCursorMarkWithoutUniqueKey.java
+++ b/solr/core/src/test/org/apache/solr/TestCursorMarkWithoutUniqueKey.java
@@ -27,14 +27,13 @@ import org.junit.After;
  */
 public class TestCursorMarkWithoutUniqueKey extends SolrTestCaseJ4 {
 
-  /** solrconfig.xml file name, shared with other cursor related tests */
-  public final static String TEST_SOLRCONFIG_NAME = 
CursorPagingTest.TEST_SOLRCONFIG_NAME;
-
+  public final static String TEST_SOLRCONFIG_NAME = "solrconfig-minimal.xml";
   public final static String TEST_SCHEMAXML_NAME = "schema-minimal.xml";
 
   @Before
   public void beforeSetupCore() throws Exception {
     System.setProperty("solr.test.useFilterForSortedQuery", 
Boolean.toString(random().nextBoolean()));
+    System.setProperty("solr.test.useFilterForSortedQuery", 
Boolean.toString(random().nextBoolean()));
     initCore(TEST_SOLRCONFIG_NAME, TEST_SCHEMAXML_NAME);
     SchemaField uniqueKeyField = 
h.getCore().getLatestSchema().getUniqueKeyField();
     assertNull("This test requires that the schema not have a uniquekey field 
-- someone violated that in " + TEST_SCHEMAXML_NAME, uniqueKeyField);
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/DistribCursorPagingTest.java 
b/solr/core/src/test/org/apache/solr/cloud/DistribCursorPagingTest.java
index b006145..89b4971 100644
--- a/solr/core/src/test/org/apache/solr/cloud/DistribCursorPagingTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/DistribCursorPagingTest.java
@@ -24,6 +24,7 @@ import org.apache.solr.CursorPagingTest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.LukeRequest;
 import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.cloud.ZkTestServer;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.SolrException;
@@ -36,6 +37,9 @@ import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.search.CursorMark;
 
+import org.apache.commons.lang3.StringUtils;
+
+import static org.apache.solr.common.params.SolrParams.wrapDefaults;
 import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_PARAM;
 import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_NEXT;
 import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_START;
@@ -50,6 +54,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * Distributed tests of deep paging using {@link CursorMark} and {@link 
CursorMarkParams#CURSOR_MARK_PARAM}.
@@ -75,8 +80,17 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
     return configString;
   }
 
+  /** 
+   * A really obnoxious hack needed to get our elevate.xml into zk ... 
+   * But simpler for now then re-writing the whole test case using 
SolrCloudTestCase.
+   */
+  @Override
+  public void distribSetUp() throws Exception {
+    super.distribSetUp();
+    ZkTestServer.putConfig("conf1", zkServer.getZkClient(), 
ZkTestServer.SOLRHOME, "elevate.xml");
+  }
+  
   @Test
-  // commented out on: 24-Dec-2018   
@BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028";) // added 
23-Aug-2018
   public void test() throws Exception {
     boolean testFinished = false;
     try {
@@ -139,6 +153,7 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
                       GroupParams.GROUP_FIELD, "str",
                       CURSOR_MARK_PARAM, CURSOR_MARK_START),
                ErrorCode.BAD_REQUEST, "Grouping");
+
   }
 
   private void doSimpleTest() throws Exception {
@@ -538,36 +553,25 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
       for (String order : new String[] {" asc", " desc"}) {
         String sort = f + order + ("id".equals(f) ? "" : ", id" + order);
         String rows = "" + TestUtil.nextInt(random(), 13, 50);
-        SentinelIntSet ids = assertFullWalkNoDups(numInitialDocs,
-                                                  params("q", "*:*",
-                                                         "fl","id,"+f,
-                                                         "rows",rows,
-                                                         "sort",sort));
-        if (numInitialDocs != ids.size()) {
-          StringBuilder message = new StringBuilder
-              ("Expected " + numInitialDocs + " docs but got " + ids.size() + 
". ");
-          message.append("sort=");
-          message.append(sort);
-          message.append(". ");
-          if (ids.size() < numInitialDocs) {
-            message.append("Missing doc(s): ");
-            for (SolrInputDocument doc : initialDocs) {
-              int id = Integer.parseInt(doc.getFieldValue("id").toString());
-              if ( ! ids.exists(id)) {
-                QueryResponse rsp = cloudClient.query(params("q", "id:" + id,
-                                                             "rows", "1"));
-                if (0 == rsp.getResults().size()) {
-                  message.append("<NOT RETRIEVABLE>:");
-                  message.append(doc.values());
-                } else {
-                  
message.append(rsp.getResults().get(0).getFieldValueMap().toString());
-                }
-                message.append("; ");
-              }
-            }
-          }
-          fail(message.toString());
-        }
+        final SolrParams main = params("q", "*:*",
+                                       "fl","id,"+f,
+                                       "rows",rows,
+                                       "sort",sort);
+        final SentinelIntSet ids = assertFullWalkNoDups(numInitialDocs, main);
+        assertEquals(numInitialDocs, ids.size());
+
+        // same query, now with QEC ... verify we get all the same docs, but 
the (expected) elevated docs are first...
+        final SentinelIntSet elevated = 
assertFullWalkNoDupsElevated(wrapDefaults(params("qt", "/elevate",
+                                                                               
          "fl","id,[elevated]",
+                                                                               
          "forceElevation","true",
+                                                                               
          "elevateIds", "50,20,80"),
+                                                                               
   main),
+                                                                     ids);
+        assertTrue(elevated.exists(50));
+        assertTrue(elevated.exists(20));
+        assertTrue(elevated.exists(80));
+        assertEquals(3, elevated.size());
+
       }
     }
 
@@ -585,15 +589,36 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
       final String fl = random().nextBoolean() ? "id" : "id,score";
       final boolean matchAll = random().nextBoolean();
       final String q = matchAll ? "*:*" : CursorPagingTest.buildRandomQuery();
-
-      SentinelIntSet ids = assertFullWalkNoDups(totalDocs,
-                                                params("q", q,
-                                                       "fl",fl,
-                                                       "rows",rows,
-                                                       "sort",sort));
+      final SolrParams main = params("q", q,
+                                     "fl",fl,
+                                     "rows",rows,
+                                     "sort",sort);
+      final SentinelIntSet ids = assertFullWalkNoDups(totalDocs,
+                                                      params("q", q,
+                                                             "fl",fl,
+                                                             "rows",rows,
+                                                             "sort",sort));
       if (matchAll) {
         assertEquals(totalDocs, ids.size());
       }
+      
+      // same query, now with QEC ... verify we get all the same docs, but the 
(expected) elevated docs are first...
+      // first we have to build a set of ids to elevate, from the set of ids 
known to match query...
+      final int[] expectedElevated = 
CursorPagingTest.pickElevations(TestUtil.nextInt(random(), 3, 33), ids);
+      final SentinelIntSet elevated = assertFullWalkNoDupsElevated
+        (wrapDefaults(params("qt", "/elevate",
+                             "fl", fl + ",[elevated]",
+                             // HACK: work around SOLR-15307... same results 
should match, just not same order
+                             "sort", (sort.startsWith("score asc") ? "score 
desc, " + sort : sort),
+                             "forceElevation","true",
+                             "elevateIds", 
StringUtils.join(expectedElevated,',')),
+                      main),
+         ids);
+      for (int expected : expectedElevated) {
+        assertTrue(expected + " wasn't elevated even though it should have 
been",
+                   elevated.exists(expected));
+      }
+      assertEquals(expectedElevated.length, elevated.size());
 
     }
 
@@ -689,6 +714,60 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
   }
 
   /**
+   * Given a set of params, executes a cursor query using {@link 
CursorMarkParams#CURSOR_MARK_START}
+   * and then continuously walks the results using {@link 
CursorMarkParams#CURSOR_MARK_START} as long
+   * as a non-0 number of docs ar returned.  This method records the the set 
of all id's
+   * (must be positive ints) encountered and throws an assertion failure if 
any id is
+   * encountered more than once, or if an id is encountered which is not 
expected, 
+   * or if an id is <code>[elevated]</code> and comes "after" any ids which 
were not <code>[elevated]</code>
+   * 
+   *
+   * @returns set of all elevated ids encountered in the walk
+   * @see #assertFullWalkNoDups(SolrParams,Consumer)
+   */
+  public SentinelIntSet assertFullWalkNoDupsElevated(final SolrParams params, 
final SentinelIntSet allExpected)
+    throws Exception {
+
+    final SentinelIntSet ids = new SentinelIntSet(allExpected.size(), -1);
+    final SentinelIntSet idsElevated = new SentinelIntSet(32, -1);
+
+    assertFullWalkNoDups(params, (doc) -> {
+        final int id = Integer.parseInt(doc.get("id").toString());
+        final boolean elevated = 
Boolean.parseBoolean(doc.getOrDefault("[elevated]","false").toString());
+        assertTrue(id + " is not expected to match query",
+                   allExpected.exists(id));
+        
+        if (ids.exists(id)) {
+          String msg = "walk already seen: " + id;
+          try {
+            try {
+              queryAndCompareShards(params("distrib","false",
+                                           "q","id:"+id));
+            } catch (AssertionError ae) {
+              throw new AssertionError(msg + ", found shard inconsistency that 
would explain it...", ae);
+            }
+            final QueryResponse rsp = cloudClient.query(params("q","id:"+id));
+            throw new AssertionError(msg + ", don't know why; q=id:"+id+" 
gives: " + rsp.toString());
+          } catch (Exception e) {
+            throw new AssertionError(msg + ", exception trying to fiture out 
why...", e);
+          }
+        }
+        if (elevated) {
+          assertEquals("id is elevated, but we've already seen non elevated 
ids: " + id,
+                       idsElevated.size(), ids.size());
+          idsElevated.put(id);
+        }
+        ids.put(id);
+      });
+
+    assertEquals("total number of ids seen did not match expected",
+                 allExpected.size(), ids.size());
+    
+    return idsElevated;
+  }
+
+  
+  /**
    * <p>
    * Given a set of params, executes a cursor query using {@link 
CursorMarkParams#CURSOR_MARK_START} 
    * and then continuously walks the results using {@link 
CursorMarkParams#CURSOR_MARK_START} as long 
@@ -705,9 +784,57 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
    * to the control index -- which can affect the sorting in some cases and 
cause false 
    * negatives in the response comparisons (even if we don't include "score" 
in the "fl")
    * </p>
+   *
+   * @returns set of all ids encountered in the walk
+   * @see #assertFullWalkNoDups(SolrParams,Consumer)
    */
   public SentinelIntSet assertFullWalkNoDups(int maxSize, SolrParams params) 
throws Exception {
-    SentinelIntSet ids = new SentinelIntSet(maxSize, -1);
+    final SentinelIntSet ids = new SentinelIntSet(maxSize, -1);
+    assertFullWalkNoDups(params, (doc) -> {
+        int id = Integer.parseInt(doc.getFieldValue("id").toString());
+        if (ids.exists(id)) {
+          String msg = "walk already seen: " + id;
+          try {
+            try {
+              queryAndCompareShards(params("distrib","false",
+                                           "q","id:"+id));
+            } catch (AssertionError ae) {
+              throw new AssertionError(msg + ", found shard inconsistency that 
would explain it...", ae);
+            }
+            final QueryResponse rsp = cloudClient.query(params("q","id:"+id));
+            throw new AssertionError(msg + ", don't know why; q=id:"+id+" 
gives: " + rsp.toString());
+          } catch (Exception e) {
+            throw new AssertionError(msg + ", exception trying to fiture out 
why...", e);
+          }
+        }
+        ids.put(id);
+        assertFalse("id set bigger then max allowed ("+maxSize+"): " + 
ids.size(),
+                    maxSize < ids.size());
+      });
+    return ids;
+  }
+
+  
+  /**
+   * <p>
+   * Given a set of params, executes a cursor query using {@link 
CursorMarkParams#CURSOR_MARK_START} 
+   * and then continuously walks the results using {@link 
CursorMarkParams#CURSOR_MARK_START} as long 
+   * as a non-0 number of docs ar returned.  This method does some basic 
validation of each response, and then 
+   * passes each doc encountered (in order returned) to the specified 
Consumer, which may throw an assertion if 
+   * there is a problem. 
+   * </p>
+   *
+   * <p>
+   * Note that this method explicitly uses the "cloudClient" for executing the 
queries, 
+   * instead of relying on the test infrastructure to execute the queries 
redundently
+   * against both the cloud client as well as a control client.  This is 
because term stat 
+   * differences in a sharded setup can result in different scores for 
documents compared 
+   * to the control index -- which can affect the sorting in some cases and 
cause false 
+   * negatives in the response comparisons (even if we don't include "score" 
in the "fl")
+   * </p>
+   */
+  public void assertFullWalkNoDups(SolrParams params, Consumer<SolrDocument> 
consumer) throws Exception {
+  
     String cursorMark = CURSOR_MARK_START;
     int docsOnThisPage = Integer.MAX_VALUE;
     while (0 < docsOnThisPage) {
@@ -727,25 +854,10 @@ public class DistribCursorPagingTest extends 
AbstractFullDistribZkTestBase {
       }
 
       for (SolrDocument doc : docs) {
-        int id = Integer.parseInt(doc.getFieldValue("id").toString());
-        if (ids.exists(id)) {
-          String msg = "(" + p + ") walk already seen: " + id;
-          try {
-            queryAndCompareShards(params("distrib","false",
-                                         "q","id:"+id));
-          } catch (AssertionError ae) {
-            throw new AssertionError(msg + ", found shard inconsistency that 
would explain it...", ae);
-          }
-          rsp = cloudClient.query(params("q","id:"+id));
-          throw new AssertionError(msg + ", don't know why; q=id:"+id+" gives: 
" + rsp.toString());
-        }
-        ids.put(id);
-        assertFalse("id set bigger then max allowed ("+maxSize+"): " + 
ids.size(),
-                    maxSize < ids.size());
+        consumer.accept(doc);
       }
       cursorMark = nextCursorMark;
     }
-    return ids;
   }
 
   private SolrParams p(SolrParams params, String... other) {
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/BadComponentTest.java 
b/solr/core/src/test/org/apache/solr/handler/component/BadComponentTest.java
index 6de1dd6..c09b5bb 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/BadComponentTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/BadComponentTest.java
@@ -18,6 +18,7 @@ package org.apache.solr.handler.component;
 
 
 import org.apache.solr.SolrTestCaseJ4;
+import org.junit.After;
 import org.junit.Test;
 
 /**
@@ -38,4 +39,9 @@ public class BadComponentTest extends SolrTestCaseJ4{
       resetExceptionIgnores();
     }
   }
+
+  @After
+  public void deleteCoreThatFailedToInit() throws Exception {
+    deleteCore();
+  }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/QueryElevationComponentTest.java
 
b/solr/core/src/test/org/apache/solr/handler/component/QueryElevationComponentTest.java
index d236883..7d158aa 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/component/QueryElevationComponentTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/QueryElevationComponentTest.java
@@ -24,6 +24,7 @@ import java.lang.invoke.MethodHandles;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.Arrays;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -32,6 +33,7 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.util.BytesRef;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.CursorMarkParams;
 import org.apache.solr.common.params.GroupParams;
 import org.apache.solr.common.params.QueryElevationParams;
 import org.apache.solr.common.params.SolrParams;
@@ -49,6 +51,11 @@ import org.junit.rules.TestRule;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_NEXT;
+import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_PARAM;
+import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_START;
+import static org.apache.solr.common.util.Utils.fromJSONString;
+
 public class QueryElevationComponentTest extends SolrTestCaseJ4 {
 
   @Rule
@@ -1031,8 +1038,102 @@ public class QueryElevationComponentTest extends 
SolrTestCaseJ4 {
       delete();
     }
   }
+  @Test
+  public void testCursor() throws Exception {
+    try {
+      init("schema12.xml");
+      
+      assertU(adoc("id", "a", "title", "ipod trash trash", "str_s1", 
"group1"));
+      assertU(adoc("id", "b", "title", "ipod ipod  trash", "str_s1", 
"group2"));
+      assertU(adoc("id", "c", "title", "ipod ipod  ipod ", "str_s1", 
"group2"));
 
+      assertU(adoc("id", "x", "title", "boosted",                 "str_s1", 
"group1"));
+      assertU(adoc("id", "y", "title", "boosted boosted",         "str_s1", 
"group2"));
+      assertU(adoc("id", "z", "title", "boosted boosted boosted", "str_s1", 
"group2"));
+      assertU(commit());
+
+      final SolrParams baseParams = params("qt", "/elevate",
+                                           "q", "title:ipod",
+                                           "sort", "score desc, id asc",
+                                           "fl", "id",
+                                           "elevateIds", "x,y,z",
+                                           "excludeIds", "b");
+
+      // sanity check everything returned w/these elevation options...
+      assertJQ(req(baseParams)
+               , "/response/numFound==5"
+               , "/response/start==0"
+               , 
"/response/docs==[{'id':'x'},{'id':'y'},{'id':'z'},{'id':'c'},{'id':'a'}]"
+               );
+      // same query using CURSOR_MARK_START should produce a 'next' cursor...
+      assertCursorJQ(req(baseParams, CURSOR_MARK_PARAM, CURSOR_MARK_START)
+                     , "/response/numFound==5"
+                     , "/response/start==0"
+                     , 
"/response/docs==[{'id':'x'},{'id':'y'},{'id':'z'},{'id':'c'},{'id':'a'}]"
+                     );
+
+      // use a cursor w/rows < 5, then fetch next cursor...
+      String nextCursor = null;
+      nextCursor = assertCursorJQ(req(baseParams
+                                      , CURSOR_MARK_PARAM, CURSOR_MARK_START
+                                      , "rows", "2"
+                                      )
+                                  , "/response/numFound==5"
+                                  , "/response/start==0"
+                                  , "/response/docs==[{'id':'x'},{'id':'y'}]"
+                                  );
+      nextCursor = assertCursorJQ(req(baseParams
+                                      , CURSOR_MARK_PARAM, nextCursor
+                                      , "rows", "2"
+                                      )
+                                  , "/response/numFound==5"
+                                  , "/response/start==0"
+                                  , "/response/docs==[{'id':'z'},{'id':'c'}]"
+                                  );
+      nextCursor = assertCursorJQ(req(baseParams
+                                      , CURSOR_MARK_PARAM, nextCursor
+                                      , "rows", "2"
+                                      )
+                                  , "/response/numFound==5"
+                                  , "/response/start==0"
+                                  , "/response/docs==[{'id':'a'}]"
+                                  );
+      final String lastCursor = nextCursor;
+      nextCursor = assertCursorJQ(req(baseParams
+                                      , CURSOR_MARK_PARAM, nextCursor
+                                      , "rows", "2"
+                                      )
+                                  , "/response/numFound==5"
+                                  , "/response/start==0"
+                                  , "/response/docs==[]"
+                                  );
+      assertEquals(lastCursor, nextCursor);
+
+    } finally {
+      delete();
+    }
+
+  }
   private static Set<BytesRef> toIdSet(String... ids) {
     return Arrays.stream(ids).map(BytesRef::new).collect(Collectors.toSet());
   }
+
+  
+  /**
+   * Asserts that the query matches the specified JSON patterns and then 
returns the
+   * {@link CursorMarkParams#CURSOR_MARK_NEXT} value from the response
+   *
+   * @see #assertJQ
+   */
+  private static String assertCursorJQ(SolrQueryRequest req, String... tests) 
throws Exception {
+    String json = assertJQ(req, tests);
+    @SuppressWarnings({"rawtypes"})
+    Map rsp = (Map) fromJSONString(json);
+    assertTrue("response doesn't contain "+CURSOR_MARK_NEXT + ": " + json,
+               rsp.containsKey(CURSOR_MARK_NEXT));
+    String next = (String)rsp.get(CURSOR_MARK_NEXT);
+    assertNotNull(CURSOR_MARK_NEXT + " is null", next);
+    return next;
+  }
+
 }
diff --git a/solr/core/src/test/org/apache/solr/search/CursorMarkTest.java 
b/solr/core/src/test/org/apache/solr/search/CursorMarkTest.java
index 19722be..9ae3fe4 100644
--- a/solr/core/src/test/org/apache/solr/search/CursorMarkTest.java
+++ b/solr/core/src/test/org/apache/solr/search/CursorMarkTest.java
@@ -39,6 +39,7 @@ import java.util.Collections;
 import java.util.UUID;
 
 import org.junit.BeforeClass;
+import static org.hamcrest.core.StringContains.containsString;
 
 /**
  * Primarily a test of parsing and serialization of the CursorMark values.
@@ -71,15 +72,16 @@ public class CursorMarkTest extends SolrTestCaseJ4 {
     assertEquals("next values not correct", nextValues, next.getSortValues());
     assertEquals("next SortSpec not correct", ss, next.getSortSpec());
 
-    try {
-      // append to our random sort string so we know it has wrong num clauses
-      final SortSpec otherSort = 
SortSpecParsing.parseSortSpec(randomSortString+",id asc", req);
-      CursorMark trash = previous.createNext(Arrays.<Object>asList
-                                             
(buildRandomSortObjects(otherSort)));
-      fail("didn't fail on next with incorrect num of sortvalues");
-    } catch (AssertionError e) {
-      // NOOP: we're happy
-    }
+    SolrException e = expectThrows(SolrException.class,
+                                   "didn't fail on next with incorrect num of 
sortvalues",
+                                   () -> {
+        // append to our random sort string so we know it has wrong num clauses
+        final SortSpec otherSort = 
SortSpecParsing.parseSortSpec(randomSortString+",id asc", req);
+        CursorMark trash = previous.createNext(Arrays.<Object>asList
+                                               
(buildRandomSortObjects(otherSort)));
+                                   });
+    assertEquals(500, e.code());
+    assertThat(e.getMessage(), containsString("sort values != sort length"));
   }
 
   public void testInvalidUsage() {

Reply via email to