diff --git a/changelog/unreleased/SOLR-18335-splitshard-router-field-point-field.yml b/changelog/unreleased/SOLR-18335-splitshard-router-field-point-field.yml
new file mode 100644
index 00000000000..78cb96be26b
--- /dev/null
+++ b/changelog/unreleased/SOLR-18335-splitshard-router-field-point-field.yml
@@ -0,0 +1,8 @@
+title: >
+ SPLITSHARD fails to migrate documents when using a numeric PointField as router.field
+type: fixed
+authors:
+ - name: Olivier Boudet
+links:
+ - name: SOLR-18335
+ url: https://issues.apache.org/jira/browse/SOLR-18335
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java b/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
index 9312b831e75..d9f404eced0 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexSplitter.java
@@ -28,14 +28,18 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.lucene.document.Document;
import org.apache.lucene.index.CodecReader;
+import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.FilterCodecReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NoMergePolicy;
+import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.SlowCodecReaderWrapper;
import org.apache.lucene.index.Terms;
@@ -70,6 +74,7 @@
import org.apache.solr.handler.IndexFetcher;
import org.apache.solr.handler.SnapShooter;
import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.NumberType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.BitsFilteredPostingsEnum;
import org.apache.solr.search.SolrIndexSearcher;
@@ -677,6 +682,20 @@ static FixedBitSet[] split(
}
}
+ if (field.getType().isPointField()) {
+ return splitPointField(
+ reader,
+ numPieces,
+ field,
+ rangesArr,
+ splitKey,
+ hashRouter,
+ delete,
+ docSets,
+ liveDocs,
+ currentPartition);
+ }
+
Terms terms = reader.terms(field.getName());
TermsEnum termsEnum = terms == null ? null : terms.iterator();
if (termsEnum == null) return docSets;
@@ -746,38 +765,145 @@ static FixedBitSet[] split(
}
if (docsMatchingRanges != null) {
- for (int ii = 0; ii < docsMatchingRanges.length; ii++) {
- if (0 == docsMatchingRanges[ii]) continue;
- switch (ii) {
- case 0:
- // document loss
- log.error(
- "Splitting {}: {} documents belong to no shards and will be dropped",
- reader,
- docsMatchingRanges[ii]);
- break;
- case 1:
- // normal case, each document moves to one of the sub-shards
- log.info(
- "Splitting {}: {} documents will move into a sub-shard",
- reader,
- docsMatchingRanges[ii]);
- break;
- default:
- // document duplication
- log.error(
- "Splitting {}: {} documents will be moved to multiple ({}) sub-shards",
- reader,
- docsMatchingRanges[ii],
- ii);
- break;
+ logDocsMatchingRanges(reader, docsMatchingRanges);
+ }
+
+ return docSets;
+ }
+
+ private static FixedBitSet[] splitPointField(
+ LeafReader reader,
+ int numPieces,
+ SchemaField field,
+ DocRouter.Range[] rangesArr,
+ String splitKey,
+ HashBasedRouter hashRouter,
+ boolean delete,
+ FixedBitSet[] docSets,
+ Bits liveDocs,
+ AtomicInteger currentPartition)
+ throws IOException {
+ NumericDocValues numericDocValues =
+ field.hasDocValues() ? DocValues.getNumeric(reader, field.getName()) : null;
+
+ int[] docsMatchingRanges = null;
+ if (rangesArr != null) {
+ docsMatchingRanges = new int[rangesArr.length + 1];
+ }
+
+ for (int doc = 0; doc < reader.maxDoc(); doc++) {
+ if (liveDocs != null && !liveDocs.get(doc)) {
+ continue;
+ }
+
+ String routeValue = getRouteFieldValue(reader, doc, field, numericDocValues);
+ if (splitKey != null) {
+ String part1 = ((CompositeIdRouter) hashRouter).getRouteKeyNoSuffix(routeValue);
+ if (part1 == null || !splitKey.equals(part1)) {
+ continue;
}
}
+
+ if (rangesArr == null) {
+ if (delete) {
+ docSets[currentPartition.get()].clear(doc);
+ } else {
+ docSets[currentPartition.get()].set(doc);
+ }
+ currentPartition.set((currentPartition.get() + 1) % numPieces);
+ } else {
+ int hash = hashRouter.sliceHash(routeValue, null, null, null);
+ int matchingRangesCount = 0;
+ for (int i = 0; i < rangesArr.length; i++) {
+ if (rangesArr[i].includes(hash)) {
+ if (delete) {
+ docSets[i].clear(doc);
+ } else {
+ docSets[i].set(doc);
+ }
+ ++matchingRangesCount;
+ }
+ }
+ docsMatchingRanges[matchingRangesCount]++;
+ }
}
+ if (docsMatchingRanges != null) {
+ logDocsMatchingRanges(reader, docsMatchingRanges);
+ }
return docSets;
}
+ private static String getRouteFieldValue(
+ LeafReader reader, int doc, SchemaField field, NumericDocValues numericDocValues)
+ throws IOException {
+ if (numericDocValues != null && numericDocValues.advanceExact(doc)) {
+ return numericRouteValueToString(field, numericDocValues.longValue());
+ }
+
+ if (field.stored()) {
+ Document storedDocument = reader.storedFields().document(doc);
+ IndexableField storedField = storedDocument.getField(field.getName());
+ if (storedField != null) {
+ Object routeValue = field.getType().toObject(storedField);
+ if (routeValue != null) {
+ return routeValue.toString();
+ }
+ }
+ }
+
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Unable to read route field '"
+ + field.getName()
+ + "' for shard splitting. Point-based route fields must expose docValues or be stored.");
+ }
+
+ private static String numericRouteValueToString(SchemaField field, long value) {
+ NumberType numberType = field.getType().getNumberType();
+ if (numberType == null) {
+ return Long.toString(value);
+ }
+
+ return switch (numberType) {
+ case INTEGER -> Integer.toString((int) value);
+ case LONG -> Long.toString(value);
+ case FLOAT -> Float.toString(Float.intBitsToFloat((int) value));
+ case DOUBLE -> Double.toString(Double.longBitsToDouble(value));
+ case DATE -> Long.toString(value);
+ };
+ }
+
+ private static void logDocsMatchingRanges(LeafReader reader, int[] docsMatchingRanges) {
+ for (int ii = 0; ii < docsMatchingRanges.length; ii++) {
+ if (0 == docsMatchingRanges[ii]) continue;
+ switch (ii) {
+ case 0:
+ // document loss
+ log.error(
+ "Splitting {}: {} documents belong to no shards and will be dropped",
+ reader,
+ docsMatchingRanges[ii]);
+ break;
+ case 1:
+ // normal case, each document moves to one of the sub-shards
+ log.info(
+ "Splitting {}: {} documents will move into a sub-shard",
+ reader,
+ docsMatchingRanges[ii]);
+ break;
+ default:
+ // document duplication
+ log.error(
+ "Splitting {}: {} documents will be moved to multiple ({}) sub-shards",
+ reader,
+ docsMatchingRanges[ii],
+ ii);
+ break;
+ }
+ }
+ }
+
private static void checkRouterSupportsSplitKey(HashBasedRouter hashRouter, String splitKey) {
if (splitKey != null && !(hashRouter instanceof CompositeIdRouter)) {
throw new IllegalStateException(
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema15.xml b/solr/core/src/test-files/solr/collection1/conf/schema15.xml
index 87fdad981d6..590ea78aab0 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema15.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema15.xml
@@ -35,6 +35,7 @@
+
@@ -595,6 +596,7 @@
+
diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java
index c55843dab9d..661de75923c 100644
--- a/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java
@@ -107,6 +107,7 @@ public void test() throws Exception {
incompleteOrOverlappingCustomRangeTest();
splitByUniqueKeyTest();
splitByRouteFieldTest();
+ splitByNumericRouteFieldTest();
splitByRouteKeyTest();
// todo can't call waitForThingsToLevelOut because it looks for jettys of all shards
@@ -1009,6 +1010,75 @@ public void splitByRouteFieldTest() throws Exception {
.query(new SolrQuery("*:*").setParam("shards", "shard1_1"))
.getResults()
.getNumFound());
+ assertEquals(101, collectionClient.query(new SolrQuery("*:*")).getResults().getNumFound());
+ }
+ }
+
+ public void splitByNumericRouteFieldTest() throws Exception {
+ log.info("Starting splitByNumericRouteFieldTest");
+ String collectionName = "numericRouteFieldColl";
+ int numShards = 4;
+ int replicationFactor = 2;
+
+ HashMap> collectionInfos = new HashMap<>();
+ String shardField = "shard_pl";
+ try (CloudSolrClient client = createCloudClient(null)) {
+ Map props =
+ Map.of(
+ REPLICATION_FACTOR,
+ replicationFactor,
+ CollectionHandlingUtils.NUM_SLICES,
+ numShards,
+ "router.field",
+ shardField);
+
+ createCollection(collectionInfos, collectionName, props, client);
+ }
+
+ List list = collectionInfos.get(collectionName);
+ checkForCollection(collectionName, list);
+
+ waitForRecoveriesToFinish(false);
+
+ getCommonCloudSolrClient();
+ String baseUrl = getBaseUrlFromZk(cloudClient.getClusterState(), collectionName);
+
+ try (SolrClient collectionClient = getHttpSolrClient(baseUrl, collectionName)) {
+ ClusterState clusterState = cloudClient.getClusterState();
+ final DocRouter router = clusterState.getCollection(collectionName).getRouter();
+ Slice shard1 = clusterState.getCollection(collectionName).getSlice(SHARD1);
+ DocRouter.Range shard1Range =
+ shard1.getRange() != null ? shard1.getRange() : router.fullRange();
+ final List ranges = router.partitionRange(2, shard1Range);
+ final int[] docCounts = new int[ranges.size()];
+
+ for (int i = 100; i <= 200; i++) {
+ collectionClient.add(getDoc(id, i, "n_ti", i, shardField, i));
+ int idx = getHashRangeIdx(router, ranges, Integer.toString(i));
+ if (idx != -1) {
+ docCounts[idx]++;
+ }
+ }
+
+ collectionClient.commit();
+
+ trySplit(collectionName, null, SHARD1, 3);
+
+ waitForRecoveriesToFinish(collectionName, false);
+
+ assertEquals(
+ docCounts[0],
+ collectionClient
+ .query(new SolrQuery("*:*").setParam("shards", "shard1_0"))
+ .getResults()
+ .getNumFound());
+ assertEquals(
+ docCounts[1],
+ collectionClient
+ .query(new SolrQuery("*:*").setParam("shards", "shard1_1"))
+ .getResults()
+ .getNumFound());
+ assertEquals(101, collectionClient.query(new SolrQuery("*:*")).getResults().getNumFound());
}
}
diff --git a/solr/core/src/test/org/apache/solr/update/SolrIndexSplitterTest.java b/solr/core/src/test/org/apache/solr/update/SolrIndexSplitterTest.java
index d7f7fa4f2e1..967dd8853e0 100644
--- a/solr/core/src/test/org/apache/solr/update/SolrIndexSplitterTest.java
+++ b/solr/core/src/test/org/apache/solr/update/SolrIndexSplitterTest.java
@@ -413,6 +413,16 @@ public void testSplitByRouteKeyLink() throws Exception {
doTestSplitByRouteKey(SolrIndexSplitter.SplitMethod.LINK);
}
+ @Test
+ public void testSplitByNumericRouteField() throws Exception {
+ doTestSplitByNumericRouteField(SolrIndexSplitter.SplitMethod.REWRITE);
+ }
+
+ @Test
+ public void testSplitByNumericRouteFieldLink() throws Exception {
+ doTestSplitByNumericRouteField(SolrIndexSplitter.SplitMethod.LINK);
+ }
+
private void doTestSplitByRouteKey(SolrIndexSplitter.SplitMethod splitMethod) throws Exception {
Path indexDir = createTempDir();
@@ -480,6 +490,83 @@ private void doTestSplitByRouteKey(SolrIndexSplitter.SplitMethod splitMethod) th
}
}
+ private void doTestSplitByNumericRouteField(SolrIndexSplitter.SplitMethod splitMethod)
+ throws Exception {
+ CompositeIdRouter router = new CompositeIdRouter();
+ List ranges = router.partitionRange(2, router.fullRange());
+ int[] expectedDocCounts = new int[ranges.size()];
+
+ for (int i = 100; i < 140; i++) {
+ String routeValue = Integer.toString(i);
+ assertU(adoc("id", "doc-" + i, "route_pl", routeValue));
+
+ int hash = router.sliceHash(routeValue, null, null, null);
+ for (int rangeIndex = 0; rangeIndex < ranges.size(); rangeIndex++) {
+ if (ranges.get(rangeIndex).includes(hash)) {
+ expectedDocCounts[rangeIndex]++;
+ break;
+ }
+ }
+ }
+
+ assertU(commit());
+ assertJQ(req("q", "*:*"), "/response/numFound==40");
+
+ SolrQueryRequestBase request = null;
+ Directory directory = null;
+ try {
+ request = lrf.makeRequest("q", "dummy");
+ SolrQueryResponse rsp = new SolrQueryResponse();
+ SplitIndexCommand command =
+ new SplitIndexCommand(
+ request,
+ rsp,
+ List.of(indexDir1.toString(), indexDir2.toString()),
+ null,
+ ranges,
+ router,
+ "route_pl",
+ null,
+ splitMethod);
+ doSplit(command);
+
+ directory =
+ h.getCore()
+ .getDirectoryFactory()
+ .get(
+ indexDir1.toString(),
+ DirectoryFactory.DirContext.DEFAULT,
+ h.getCore().getSolrConfig().indexConfig.lockType);
+ DirectoryReader reader = DirectoryReader.open(directory);
+ assertEquals(
+ "split index1 has wrong number of documents", expectedDocCounts[0], reader.numDocs());
+ reader.close();
+ h.getCore().getDirectoryFactory().release(directory);
+ directory = null;
+
+ directory =
+ h.getCore()
+ .getDirectoryFactory()
+ .get(
+ indexDir2.toString(),
+ DirectoryFactory.DirContext.DEFAULT,
+ h.getCore().getSolrConfig().indexConfig.lockType);
+ reader = DirectoryReader.open(directory);
+ assertEquals(
+ "split index2 has wrong number of documents", expectedDocCounts[1], reader.numDocs());
+ reader.close();
+ h.getCore().getDirectoryFactory().release(directory);
+ directory = null;
+ } finally {
+ if (request != null) {
+ request.close();
+ }
+ if (directory != null) {
+ h.getCore().getDirectoryFactory().release(directory);
+ }
+ }
+ }
+
@Test
public void testSplitWithChildDocs() throws Exception {
doTestSplitWithChildDocs(SolrIndexSplitter.SplitMethod.REWRITE);