From 91f5267e59accfa9658f64bef9891ec4e7fc55fd Mon Sep 17 00:00:00 2001 From: endika-ravn Date: Wed, 4 Mar 2026 12:56:43 +0100 Subject: [PATCH 1/2] Update DistribJoinFromCollectionTest.java --- .../cloud/DistribJoinFromCollectionTest.java | 92 +++++++++++++------ 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java b/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java index 8a14d00ace82..5805b7ba5371 100644 --- a/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java @@ -25,9 +25,10 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.List; import org.apache.solr.client.solrj.RemoteSolrException; +import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; -import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.request.QueryRequest; import org.apache.solr.client.solrj.request.UpdateRequest; @@ -36,10 +37,8 @@ import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrInputDocument; -import org.apache.solr.common.cloud.ClusterState; -import org.apache.solr.common.cloud.Replica; -import org.apache.solr.common.cloud.Slice; -import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.core.NodeRoles; +import org.apache.solr.embedded.JettySolrRunner; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -56,40 +55,50 @@ public class DistribJoinFromCollectionTest extends SolrCloudTestCase { // resetExceptionIgnores(); private static String toColl = "to_2x2"; private static String fromColl = "from_1x4"; - + private static String configName = "solrCloudCollectionConfig"; private static String toDocId; + private static JettySolrRunner coordinatorJetty; + private static Set dataNodes; @BeforeClass public static void setupCluster() throws Exception { final Path configDir = TEST_COLL1_CONF(); - String configName = "solrCloudCollectionConfig"; - int nodeCount = 5; + // Create 4 data nodes + int nodeCount = 4; configureCluster(nodeCount).addConfig(configName, configDir).configure(); + // Start a coordinator-only node (no data role) + System.setProperty(NodeRoles.NODE_ROLES_PROP, "coordinator:on,data:off"); + try { + coordinatorJetty = cluster.startJettySolrRunner(); + } finally { + System.clearProperty(NodeRoles.NODE_ROLES_PROP); + } + Map collectionProperties = new HashMap<>(); collectionProperties.put("config", "solrconfig-tlog.xml"); collectionProperties.put("schema", "schema.xml"); - // create a collection holding data for the "to" side of the JOIN + // Collect data node names (exclude coordinator) + dataNodes = new HashSet<>(); + for (JettySolrRunner jetty : cluster.getJettySolrRunners()) { + if (jetty != coordinatorJetty) { + dataNodes.add(jetty.getNodeName()); + } + } + // create a collection holding data for the "to" side of the JOIN int shards = 2; int replicas = 2; CollectionAdminRequest.createCollection(toColl, configName, shards, replicas) + .setCreateNodeSet(String.join(",", dataNodes)) .setProperties(collectionProperties) .process(cluster.getSolrClient()); - // get the set of nodes where replicas for the "to" collection exist - Set nodeSet = new HashSet<>(); - ZkStateReader zkStateReader = cluster.getZkStateReader(); - ClusterState cs = zkStateReader.getClusterState(); - for (Slice slice : cs.getCollection(toColl).getActiveSlices()) - for (Replica replica : slice.getReplicas()) nodeSet.add(replica.getNodeName()); - assertTrue(nodeSet.size() > 0); - // deploy the "from" collection to all nodes where the "to" collection exists CollectionAdminRequest.createCollection(fromColl, configName, 1, 4) - .setCreateNodeSet(String.join(",", nodeSet)) + .setCreateNodeSet(String.join(",", dataNodes)) .setProperties(collectionProperties) .process(cluster.getSolrClient()); @@ -100,15 +109,39 @@ public static void setupCluster() throws Exception { } @Test - public void testScore() throws Exception { + public void testScoreCoordinator() throws Exception { // without score - testJoins(toColl, fromColl, toDocId, true); + testJoins(toColl, fromColl, toDocId, true, List.of(coordinatorJetty.getNodeName())); } @Test - public void testNoScore() throws Exception { + public void testNoScoreCoordinator() throws Exception { // with score - testJoins(toColl, fromColl, toDocId, false); + testJoins(toColl, fromColl, toDocId, false, List.of(coordinatorJetty.getNodeName())); + } + + @Test + public void testScoreDataNodes() throws Exception { + // without score + testJoins(toColl, fromColl, toDocId, true, dataNodes.stream().toList()); + } + + @Test + public void testNoScoreDataNodes() throws Exception { + // with score + testJoins(toColl, fromColl, toDocId, false, dataNodes.stream().toList()); + } + + @Test + public void testCoordinatorWithoutJoin() throws Exception { + // Verify that a simple query (no join) works through the coordinator node + SolrClient client = cluster.getSolrClient(); + QueryRequest qr = + new QueryRequest(params("collection", toColl, "q", "*:*", "fl", "id,get_s")); + qr.setPreferredNodes(List.of(coordinatorJetty.getNodeName())); + QueryResponse rsp = qr.process(client); + SolrDocumentList hits = rsp.getResults(); + assertEquals("Expected 1 doc", 1, hits.getNumFound()); } @AfterClass @@ -132,11 +165,11 @@ public static void shutdown() { log.info("DistribJoinFromCollectionTest succeeded ... shutting down now!"); } - private void testJoins(String toColl, String fromColl, String toDocId, boolean isScoresTest) + private void testJoins(String toColl, String fromColl, String toDocId, boolean isScoresTest, List targetNodes) throws SolrServerException, IOException { // verify the join with fromIndex works final String fromQ = "match_s:c^2"; - CloudSolrClient client = cluster.getSolrClient(); + SolrClient cloudClient = cluster.getSolrClient(); { final String joinQ = "{!join " @@ -147,7 +180,8 @@ private void testJoins(String toColl, String fromColl, String toDocId, boolean i + fromQ; QueryRequest qr = new QueryRequest(params("collection", toColl, "q", joinQ, "fl", "id,get_s,score")); - QueryResponse rsp = qr.process(client); + qr.setPreferredNodes(targetNodes); + QueryResponse rsp = qr.process(cloudClient); SolrDocumentList hits = rsp.getResults(); assertEquals("Expected 1 doc, got " + hits, 1, hits.getNumFound()); SolrDocument doc = hits.get(0); @@ -161,7 +195,7 @@ private void testJoins(String toColl, String fromColl, String toDocId, boolean i // create an alias for the fromIndex and then query through the alias String alias = fromColl + "Alias"; - CollectionAdminRequest.createAlias(alias, fromColl).process(client); + CollectionAdminRequest.createAlias(alias, fromColl).process(cloudClient); { final String joinQ = @@ -173,7 +207,8 @@ private void testJoins(String toColl, String fromColl, String toDocId, boolean i + fromQ; final QueryRequest qr = new QueryRequest(params("collection", toColl, "q", joinQ, "fl", "id,get_s,score")); - final QueryResponse rsp = qr.process(client); + qr.setPreferredNodes(targetNodes); + final QueryResponse rsp = qr.process(cloudClient); final SolrDocumentList hits = rsp.getResults(); assertEquals("Expected 1 doc", 1, hits.getNumFound()); SolrDocument doc = hits.get(0); @@ -195,7 +230,8 @@ private void testJoins(String toColl, String fromColl, String toDocId, boolean i + " to=join_s}match_s:d"; final QueryRequest qr = new QueryRequest(params("collection", toColl, "q", joinQ, "fl", "id,get_s,score")); - final QueryResponse rsp = qr.process(client); + qr.setPreferredNodes(targetNodes); + final QueryResponse rsp = qr.process(cloudClient); final SolrDocumentList hits = rsp.getResults(); assertEquals("Expected no hits", 0, hits.getNumFound()); } From eb8a483bf52a987ea98e10b78b6b90c4fed2f383 Mon Sep 17 00:00:00 2001 From: Mikhail Khludnev Date: Thu, 5 Mar 2026 13:15:00 +0300 Subject: [PATCH 2/2] dirty hacking synthetic core joining --- .../src/java/org/apache/solr/search/JoinQParserPlugin.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java index b403c0c92a28..ecf802058b9d 100644 --- a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java +++ b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java @@ -20,6 +20,7 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.Set; +import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.join.ScoreMode; import org.apache.solr.common.SolrException; @@ -27,6 +28,7 @@ import org.apache.solr.common.util.NamedList; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.SolrCore; +import org.apache.solr.core.SyntheticSolrCore; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrQueryRequestBase; import org.apache.solr.search.join.CrossCollectionJoinQParser; @@ -216,6 +218,9 @@ public QParser createParser( @Override public Query parse() throws SyntaxError { + if (req.getCore() instanceof SyntheticSolrCore) { + return new MatchNoDocsQuery("never search it anyway"); + } if (localParams != null && localParams.get(METHOD) != null) { // TODO Make sure 'method' is valid value here and give users a nice error final Method explicitMethod = Method.valueOf(localParams.get(METHOD));