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

houston 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 aa81f20a439 SOLR-17915: Add shards.preference=replica.location:host 
(#3654)
aa81f20a439 is described below

commit aa81f20a43959ce799f5abefb980610c71e1f67b
Author: Houston Putman <[email protected]>
AuthorDate: Thu Sep 18 09:42:14 2025 -0700

    SOLR-17915: Add shards.preference=replica.location:host (#3654)
---
 solr/CHANGES.txt                                   |   2 +-
 .../org/apache/solr/handler/StreamHandler.java     |   1 +
 .../handler/component/HttpShardHandlerFactory.java |   1 +
 .../pages/solrcloud-distributed-requests.adoc      |  24 ++-
 .../solr/client/solrj/io/stream/StreamingTest.java |   2 +-
 .../routing/NodePreferenceRulesComparator.java     | 226 +++++++++++++--------
 .../solr/client/solrj/routing/PreferenceRule.java  |   5 +
 .../RequestReplicaListTransformerGenerator.java    |  60 ++++--
 .../java/org/apache/solr/common/cloud/Replica.java |   8 +-
 .../apache/solr/common/cloud/ZkCoreNodeProps.java  |   2 +-
 .../org/apache/solr/common/params/ShardParams.java |   3 +
 .../routing/NodePreferenceRulesComparatorTest.java | 158 +++++++++++++-
 12 files changed, 366 insertions(+), 126 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 837e05caa6c..6f7621fcad3 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -235,7 +235,7 @@ Other Changes
 ==================  9.10.0 ==================
 New Features
 ---------------------
-(No changes)
+* SOLR-17915: shards.preference=replica.location now supports the "host" 
option for routing to replicas on the same host. (Houston Putman)
 
 Improvements
 ---------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/StreamHandler.java 
b/solr/core/src/java/org/apache/solr/handler/StreamHandler.java
index 4f80731614d..90b0137a07d 100644
--- a/solr/core/src/java/org/apache/solr/handler/StreamHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/StreamHandler.java
@@ -213,6 +213,7 @@ public class StreamHandler extends RequestHandlerBase
                   .toString(),
               zkController.getNodeName(),
               zkController.getBaseUrl(),
+              zkController.getHostName(),
               zkController.getSysPropsCacher());
     } else {
       requestReplicaListTransformerGenerator = new 
RequestReplicaListTransformerGenerator();
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
 
b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
index 67b3e6e300b..abb5f31b398 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java
@@ -413,6 +413,7 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
               .toString(),
           zkController.getNodeName(),
           zkController.getBaseUrl(),
+          zkController.getHostName(),
           zkController.getSysPropsCacher());
     } else {
       return 
requestReplicaListTransformerGenerator.getReplicaListTransformer(params);
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/solrcloud-distributed-requests.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/solrcloud-distributed-requests.adoc
index 6d37b4dec80..f7b25937d9c 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/solrcloud-distributed-requests.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/solrcloud-distributed-requests.adoc
@@ -184,22 +184,26 @@ One or more replica types that are preferred.
 Any combination of `PULL`, `TLOG` and `NRT` is allowed.
 
 `replica.location`::
-One or more replica locations that are preferred.
-+
-A location starts with `http://hostname:port`.
-Matching is done for the given string as a prefix, so it's possible to e.g., 
leave out the port.
-+
-A special value `local` may be used to denote any local replica running on the 
same Solr instance as the one handling the query.
+Prefer replicas that match the given location. Available options are:
+- `local` - Replicas in the same Solr instance as the one handling the query.
 This is useful when a query requests many fields or large fields to be 
returned per document because it avoids moving large amounts of data over the 
network when it is available locally.
 In addition, this feature can be useful for minimizing the impact of a 
problematic replica with degraded performance, as it reduces the likelihood 
that the degraded replica will be hit by other healthy replicas.
 +
 The value of `replica.location:local` diminishes as the number of shards (that 
have no locally-available replicas) in a collection increases because the query 
controller will have to direct the query to non-local replicas for most of the 
shards.
 +
 In other words, this feature is mostly useful for optimizing queries directed 
towards collections with a small number of shards and many replicas.
+- `host` - Replicas in Solr instances running on the same host as the one 
handling the query.
+This can be useful in many of the same ways as `local`, but can help spare 
network costs when multiple Solr instances are running per host.
 +
-Also, this option should only be used if you are load balancing requests 
across all nodes that host replicas for the collection you are querying, as 
Solr's `CloudSolrClient` will do.
+As the number of shards grows, `replica.location:host` may be more useful than 
`replica.location:local`, since it is more likely that all shards can be found 
on the same host than all shards being found in the same Solr instance.
+- `_host-prefix_` - A replica location starts with `http://hostname:port`, 
i.e. `replica.location:http://hostname:port`.
+Matching is done for the given string as a prefix, so it's possible to e.g., 
leave out the port.
+
+IMPORTANT: The `local` and `host` options should only be used if you are load 
balancing requests across all nodes that host replicas for the collection you 
are querying, as Solr's `CloudSolrClient` will do.
 If not load-balancing, this feature can introduce a hotspot in the cluster 
since queries won't be evenly distributed across the cluster.
 
+NOTE: The `local` and `host` options are equivalent when only 1 Solr node is 
running on each host. This is also true in a setup like Kubernetes where each 
Solr node has a unique hostname.
+
 `replica.base`::
 Applied after sorting by inherent replica attributes, this property defines a 
fallback ordering among sets of preference-equivalent replicas; if specified, 
only one value may be specified for this property, and it must be specified 
last.
 +
@@ -253,10 +257,10 @@ shards.preference=replica.location:local
 [source,text]
 
shards.preference=replica.location:http://server1,replica.location:http://server2
 
-* Prefer PULL replicas if available, otherwise TLOG replicas, and local 
replicas among those:
+* Prefer PULL replicas if available, otherwise TLOG replicas, and among those, 
replicas on the same host:
 +
 [source,text]
-shards.preference=replica.type:PULL,replica.type:TLOG,replica.location:local
+shards.preference=replica.type:PULL,replica.type:TLOG,replica.location:host
 
 * Prefer local replicas, and among them PULL replicas when available, 
otherwise TLOG replicas:
 +
@@ -353,7 +357,7 @@ For example, the following line makes Solr use the 
`ExactStatsCache` implementat
 
 The query param distrib.statsCache defaults to `true`. If set to `false`, 
distributed calls to fetch global term stats is turned off for this query. This 
can reduce overhead for queries that do not utilize distributed IDF for score 
calculation.
 
-[source,xml]
+[source,plain]
 ----
 http://localhost:8987/solr/collection1/select?q=*%3A*&wt=json&fq={!terms 
f=id}id1,id2&distrib.statsCache=false
 ----
diff --git 
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
 
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
index 8be2fcd8059..a0b72d8d2a1 100644
--- 
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
+++ 
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamingTest.java
@@ -2821,7 +2821,7 @@ public class StreamingTest extends SolrCloudTestCase {
     streamContext.setSolrClientCache(new SolrClientCache());
     streamContext.setRequestReplicaListTransformerGenerator(
         new RequestReplicaListTransformerGenerator(
-            ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG", null, null, 
null));
+            ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":TLOG", null, null, 
null, null));
 
     streamContext.setRequestParams(
         params(ShardParams.SHARDS_PREFERENCE, 
ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE + ":nrt"));
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
index 527a8a30d5d..b6cd6df90c8 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparator.java
@@ -26,7 +26,6 @@ import org.apache.solr.common.cloud.NodesSysProps;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.StrUtils;
 
 /**
  * This comparator makes sure that the given replicas are sorted according to 
the given list of
@@ -38,13 +37,14 @@ import org.apache.solr.common.util.StrUtils;
  * "equivalent" replicas will be ordered (the base ordering). Defaults to 
"random"; may specify
  * "stable".
  */
-public class NodePreferenceRulesComparator implements Comparator<Object> {
+public class NodePreferenceRulesComparator {
 
   private final NodesSysProps sysProps;
   private final String nodeName;
   private final List<PreferenceRule> sortRules;
   private final List<PreferenceRule> preferenceRules;
-  private final String localHostAddress;
+  private final String baseUrl;
+  private final String hostName;
   private final ReplicaListTransformer baseReplicaListTransformer;
 
   public NodePreferenceRulesComparator(
@@ -52,21 +52,31 @@ public class NodePreferenceRulesComparator implements 
Comparator<Object> {
       final SolrParams requestParams,
       final ReplicaListTransformerFactory defaultRltFactory,
       final ReplicaListTransformerFactory stableRltFactory) {
-    this(preferenceRules, requestParams, null, null, null, defaultRltFactory, 
stableRltFactory);
+    this(
+        preferenceRules,
+        requestParams,
+        null,
+        null,
+        null,
+        null,
+        defaultRltFactory,
+        stableRltFactory);
   }
 
   public NodePreferenceRulesComparator(
       final List<PreferenceRule> preferenceRules,
       final SolrParams requestParams,
       final String nodeName,
-      final String localHostAddress,
+      final String baseUrl,
+      final String hostName,
       final NodesSysProps sysProps,
       final ReplicaListTransformerFactory defaultRltFactory,
       final ReplicaListTransformerFactory stableRltFactory) {
     this.sysProps = sysProps;
     this.preferenceRules = preferenceRules;
     this.nodeName = nodeName;
-    this.localHostAddress = localHostAddress;
+    this.baseUrl = baseUrl;
+    this.hostName = hostName;
     final int maxIdx = preferenceRules.size() - 1;
     final PreferenceRule lastRule = preferenceRules.get(maxIdx);
     if (!ShardParams.SHARDS_PREFERENCE_REPLICA_BASE.equals(lastRule.name)) {
@@ -110,42 +120,109 @@ public class NodePreferenceRulesComparator implements 
Comparator<Object> {
    * order.
    */
   NodePreferenceRulesComparator(
-      final List<PreferenceRule> sortRules, final SolrParams requestParams) {
-    this(sortRules, requestParams, NOOP_RLTF, null);
+      final List<PreferenceRule> preferenceRules, final SolrParams 
requestParams) {
+    this(preferenceRules, requestParams, NOOP_RLTF, null);
+  }
+
+  /**
+   * For compatibility with tests, which expect this constructor to have no 
effect on the *base*
+   * order.
+   */
+  NodePreferenceRulesComparator(
+      final List<PreferenceRule> preferenceRules,
+      final SolrParams requestParams,
+      final String nodeName,
+      final String baseUrl,
+      final String hostName) {
+    this(preferenceRules, requestParams, nodeName, baseUrl, hostName, null, 
NOOP_RLTF, null);
   }
 
   public ReplicaListTransformer getBaseReplicaListTransformer() {
     return baseReplicaListTransformer;
   }
 
-  @Override
-  public int compare(Object left, Object right) {
+  @SuppressWarnings({"unchecked"})
+  public <T> Comparator<T> getComparator(T example) {
+    if (example instanceof Replica) {
+      return (Comparator<T>) getReplicaComparator();
+    } else if (example instanceof String) {
+      return (Comparator<T>) getUrlComparator();
+    } else {
+      return null;
+    }
+  }
+
+  public Comparator<Replica> getReplicaComparator() {
+    Comparator<Replica> comparator = null;
+    if (this.sortRules != null) {
+      for (PreferenceRule preferenceRule : this.sortRules) {
+        Comparator<Replica> nextComparator = 
getPreferenceReplicaComparator(preferenceRule);
+        if (nextComparator != null) {
+          if (comparator != null) {
+            comparator = comparator.thenComparing(nextComparator);
+          } else {
+            comparator = nextComparator;
+          }
+        }
+      }
+    }
+    return comparator;
+  }
+
+  public Comparator<String> getUrlComparator() {
+    Comparator<String> comparator = null;
     if (this.sortRules != null) {
       for (PreferenceRule preferenceRule : this.sortRules) {
-        final boolean lhs;
-        final boolean rhs;
+        Comparator<String> nextComparator = 
getPreferenceUrlComparator(preferenceRule);
+        if (nextComparator != null) {
+          if (comparator != null) {
+            comparator = comparator.thenComparing(nextComparator);
+          } else {
+            comparator = nextComparator;
+          }
+        }
+      }
+    }
+    return comparator;
+  }
+
+  private Comparator<Replica> getPreferenceReplicaComparator(PreferenceRule 
preferenceRule) {
+    Comparator<Replica> comparator =
         switch (preferenceRule.name) {
           case ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE:
-            lhs = hasReplicaType(left, preferenceRule.value);
-            rhs = hasReplicaType(right, preferenceRule.value);
-            break;
+            yield Comparator.comparing(
+                r -> 
r.getType().toString().equalsIgnoreCase(preferenceRule.value));
           case ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION:
-            lhs = hasCoreUrlPrefix(left, preferenceRule.value);
-            rhs = hasCoreUrlPrefix(right, preferenceRule.value);
-            break;
+            yield switch (preferenceRule.value) {
+              case ShardParams.REPLICA_LOCAL -> {
+                if (baseUrl == null) {
+                  // For SolrJ clients, which do not have a baseUrl, this 
preference won't be used
+                  yield null;
+                }
+                yield Comparator.comparing(r -> 
r.getBaseUrl().equals(baseUrl));
+              }
+              case ShardParams.REPLICA_HOST -> {
+                if (hostName == null) {
+                  // For SolrJ clients, which do not have a hostName, this 
preference won't be used
+                  yield null;
+                }
+                final String hostNameWithColon = hostName + ":";
+                yield Comparator.comparing(r -> 
r.getNodeName().startsWith(hostNameWithColon));
+              }
+              default -> Comparator.comparing(r -> 
r.getCoreUrl().startsWith(preferenceRule.value));
+            };
           case ShardParams.SHARDS_PREFERENCE_REPLICA_LEADER:
-            lhs = hasLeaderStatus(left, preferenceRule.value);
-            rhs = hasLeaderStatus(right, preferenceRule.value);
-            break;
+            final boolean preferredIsLeader = 
Boolean.parseBoolean(preferenceRule.value);
+            yield Comparator.comparing(r -> r.isLeader() == preferredIsLeader);
           case ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP:
             if (sysProps == null) {
-              throw new IllegalArgumentException(
-                  "Unable to get the NodesSysPropsCacher on sorting replicas 
by preference:"
-                      + preferenceRule.value);
+              // For SolrJ clients, which do not have Solr sysProps, this 
preference won't be used
+              yield null;
             }
-            lhs = hasSameMetric(left, preferenceRule.value);
-            rhs = hasSameMetric(right, preferenceRule.value);
-            break;
+            Collection<String> tags = 
Collections.singletonList(preferenceRule.value);
+            Map<String, Object> currentNodeMetric = 
sysProps.getSysProps(nodeName, tags);
+            yield Comparator.comparing(
+                r -> 
currentNodeMetric.equals(sysProps.getSysProps(r.getNodeName(), tags)));
           case ShardParams.SHARDS_PREFERENCE_REPLICA_BASE:
             throw new IllegalArgumentException(
                 "only one base replica order may be specified in "
@@ -154,61 +231,50 @@ public class NodePreferenceRulesComparator implements 
Comparator<Object> {
           default:
             throw new IllegalArgumentException(
                 "Invalid " + ShardParams.SHARDS_PREFERENCE + " type: " + 
preferenceRule.name);
-        }
-        if (lhs != rhs) {
-          return lhs ? -1 : +1;
-        }
-      }
-    }
-    return 0;
+        };
+    // Boolean comparators are 'false' first by default, so we need to reverse
+    return comparator != null ? comparator.reversed() : null;
   }
 
-  private boolean hasSameMetric(Object o, String metricTag) {
-    if (!(o instanceof Replica)) {
-      return false;
-    }
-
-    Collection<String> tags = Collections.singletonList(metricTag);
-    String otherNodeName = ((Replica) o).getNodeName();
-    Map<String, Object> currentNodeMetric = sysProps.getSysProps(nodeName, 
tags);
-    Map<String, Object> otherNodeMetric = sysProps.getSysProps(otherNodeName, 
tags);
-    return currentNodeMetric.equals(otherNodeMetric);
-  }
-
-  private boolean hasCoreUrlPrefix(Object o, String prefix) {
-    final String s;
-    if (o instanceof String) {
-      s = (String) o;
-    } else if (o instanceof Replica) {
-      s = ((Replica) o).getCoreUrl();
-    } else {
-      return false;
-    }
-    if (prefix.equals(ShardParams.REPLICA_LOCAL)) {
-      return StrUtils.isNotNullOrEmpty(localHostAddress) && 
s.startsWith(localHostAddress);
-    } else {
-      return s.startsWith(prefix);
-    }
-  }
-
-  private static boolean hasReplicaType(Object o, String preferred) {
-    if (!(o instanceof Replica)) {
-      return false;
-    }
-    final String s = ((Replica) o).getType().toString();
-    return s.equalsIgnoreCase(preferred);
-  }
-
-  private static boolean hasLeaderStatus(Object o, String status) {
-    if (!(o instanceof Replica)) {
-      return false;
-    }
-    final boolean leaderStatus = ((Replica) o).isLeader();
-    return leaderStatus == Boolean.parseBoolean(status);
-  }
-
-  public List<PreferenceRule> getSortRules() {
-    return sortRules;
+  private Comparator<String> getPreferenceUrlComparator(PreferenceRule 
preferenceRule) {
+    Comparator<String> comparator =
+        switch (preferenceRule.name) {
+            // These preferences are not supported for URLs
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE:
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_LEADER:
+          case ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP:
+            yield null;
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION:
+            yield switch (preferenceRule.value) {
+              case ShardParams.REPLICA_LOCAL -> {
+                if (baseUrl == null) {
+                  // For SolrJ clients, which do not have a baseUrl, this 
preference won't be used
+                  yield null;
+                }
+                yield Comparator.comparing(url -> url.startsWith(baseUrl));
+              }
+              case ShardParams.REPLICA_HOST -> {
+                if (hostName == null) {
+                  // For SolrJ clients, which do not have a hostName, this 
preference won't be used
+                  yield null;
+                }
+                String scheme = baseUrl.startsWith("https") ? "https" : "http";
+                final String baseUrlHostPrefix = scheme + "://" + hostName + 
":";
+                yield Comparator.comparing(url -> 
url.startsWith(baseUrlHostPrefix));
+              }
+              default -> Comparator.comparing(url -> 
url.startsWith(preferenceRule.value));
+            };
+          case ShardParams.SHARDS_PREFERENCE_REPLICA_BASE:
+            throw new IllegalArgumentException(
+                "only one base replica order may be specified in "
+                    + ShardParams.SHARDS_PREFERENCE
+                    + ", and it must be specified last");
+          default:
+            throw new IllegalArgumentException(
+                "Invalid " + ShardParams.SHARDS_PREFERENCE + " type: " + 
preferenceRule.name);
+        };
+    // Boolean comparators are 'false' first by default, so we need to reverse
+    return comparator != null ? comparator.reversed() : null;
   }
 
   public List<PreferenceRule> getPreferenceRules() {
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java
index 3bc18c630a2..8c12cac967e 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/PreferenceRule.java
@@ -45,4 +45,9 @@ public class PreferenceRule {
         });
     return preferenceRules;
   }
+
+  @Override
+  public String toString() {
+    return name + ":" + value;
+  }
 }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
index 28e2072e77f..4071dd7527f 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/routing/RequestReplicaListTransformerGenerator.java
@@ -17,11 +17,12 @@
 package org.apache.solr.client.solrj.routing;
 
 import java.lang.invoke.MethodHandles;
-import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
+import java.util.stream.Collectors;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.NodesSysProps;
@@ -44,7 +45,8 @@ public class RequestReplicaListTransformerGenerator {
   private final ReplicaListTransformerFactory defaultRltFactory;
   private final String defaultShardPreferences;
   private final String nodeName;
-  private final String localHostAddress;
+  private final String baseUrl;
+  private final String hostName;
   private final NodesSysProps sysProps;
 
   public RequestReplicaListTransformerGenerator() {
@@ -58,15 +60,16 @@ public class RequestReplicaListTransformerGenerator {
   public RequestReplicaListTransformerGenerator(
       ReplicaListTransformerFactory defaultRltFactory,
       ReplicaListTransformerFactory stableRltFactory) {
-    this(defaultRltFactory, stableRltFactory, null, null, null, null);
+    this(defaultRltFactory, stableRltFactory, null, null, null, null, null);
   }
 
   public RequestReplicaListTransformerGenerator(
       String defaultShardPreferences,
       String nodeName,
-      String localHostAddress,
+      String baseUrl,
+      String hostName,
       NodesSysProps sysProps) {
-    this(null, null, defaultShardPreferences, nodeName, localHostAddress, 
sysProps);
+    this(null, null, defaultShardPreferences, nodeName, baseUrl, hostName, 
sysProps);
   }
 
   public RequestReplicaListTransformerGenerator(
@@ -74,14 +77,16 @@ public class RequestReplicaListTransformerGenerator {
       ReplicaListTransformerFactory stableRltFactory,
       String defaultShardPreferences,
       String nodeName,
-      String localHostAddress,
+      String baseUrl,
+      String hostName,
       NodesSysProps sysProps) {
     this.defaultRltFactory = Objects.requireNonNullElse(defaultRltFactory, 
RANDOM_RLTF);
     this.stableRltFactory =
         Objects.requireNonNullElseGet(stableRltFactory, 
AffinityReplicaListTransformerFactory::new);
     this.defaultShardPreferences = 
Objects.requireNonNullElse(defaultShardPreferences, "");
     this.nodeName = nodeName;
-    this.localHostAddress = localHostAddress;
+    this.baseUrl = baseUrl;
+    this.hostName = hostName;
     this.sysProps = sysProps;
   }
 
@@ -91,14 +96,16 @@ public class RequestReplicaListTransformerGenerator {
 
   public ReplicaListTransformer getReplicaListTransformer(
       final SolrParams requestParams, String defaultShardPreferences) {
-    return getReplicaListTransformer(requestParams, defaultShardPreferences, 
null, null, null);
+    return getReplicaListTransformer(
+        requestParams, defaultShardPreferences, null, null, null, null);
   }
 
   public ReplicaListTransformer getReplicaListTransformer(
       final SolrParams requestParams,
       String defaultShardPreferences,
       String nodeName,
-      String localHostAddress,
+      String baseUrl,
+      String hostName,
       NodesSysProps sysProps) {
     defaultShardPreferences =
         Objects.requireNonNullElse(defaultShardPreferences, 
this.defaultShardPreferences);
@@ -112,15 +119,14 @@ public class RequestReplicaListTransformerGenerator {
               preferenceRules,
               requestParams,
               nodeName != null ? nodeName : this.nodeName, // could be still 
null
-              localHostAddress != null
-                  ? localHostAddress
-                  : this.localHostAddress, // could still be null
+              baseUrl != null ? baseUrl : this.baseUrl, // could still be null
+              hostName != null ? hostName : this.hostName, // could still be 
null
               sysProps != null ? sysProps : this.sysProps, // could still be 
null
               defaultRltFactory,
               stableRltFactory);
       ReplicaListTransformer baseReplicaListTransformer =
           replicaComp.getBaseReplicaListTransformer();
-      if (replicaComp.getSortRules() == null) {
+      if (replicaComp.getPreferenceRules() == null || 
replicaComp.getPreferenceRules().isEmpty()) {
         // only applying base transformation
         return baseReplicaListTransformer;
       } else {
@@ -154,26 +160,40 @@ public class RequestReplicaListTransformerGenerator {
         if (log.isDebugEnabled()) {
           log.debug(
               "Applying the following sorting preferences to replicas: {}",
-              Arrays.toString(replicaComp.getPreferenceRules().toArray()));
+              replicaComp.getPreferenceRules().stream()
+                  .map(PreferenceRule::toString)
+                  .collect(Collectors.joining(",", "[", "]")));
         }
 
+        Comparator<T> comparator;
+        try {
+          comparator = replicaComp.getComparator(choices.get(0));
+        } catch (IllegalArgumentException iae) {
+          throw new SolrException(ErrorCode.BAD_REQUEST, iae.getMessage());
+        }
+        if (comparator == null) {
+          // A null comparator means that the choices cannot be sorted by the 
given rules.
+          // Just sort by the base transformer and return.
+          baseReplicaListTransformer.transform(choices);
+          return;
+        }
         // First, sort according to comparator rules.
         try {
-          choices.sort(replicaComp);
+          choices.sort(comparator);
         } catch (IllegalArgumentException iae) {
           throw new SolrException(ErrorCode.BAD_REQUEST, iae.getMessage());
         }
 
         // Next determine all boundaries between replicas ranked as 
"equivalent" by the comparator
-        Iterator<?> iter = choices.iterator();
-        Object prev = iter.next();
-        Object current;
+        Iterator<T> iter = choices.iterator();
+        T prev = iter.next();
+        T current;
         int idx = 1;
         int boundaryCount = 0;
         int[] boundaries = new int[choices.size()];
         do {
           current = iter.next();
-          if (replicaComp.compare(prev, current) != 0) {
+          if (comparator.compare(prev, current) != 0) {
             boundaries[boundaryCount++] = idx;
           }
           prev = current;
@@ -196,7 +216,7 @@ public class RequestReplicaListTransformerGenerator {
         if (log.isDebugEnabled()) {
           log.debug(
               "Applied sorting preferences to replica list: {}",
-              Arrays.toString(choices.toArray()));
+              
choices.stream().map(T::toString).collect(Collectors.joining(",", "[", "]")));
         }
       }
     }
diff --git a/solr/solrj/src/java/org/apache/solr/common/cloud/Replica.java 
b/solr/solrj/src/java/org/apache/solr/common/cloud/Replica.java
index d50dc1a1da8..e5102b0d3f1 100644
--- a/solr/solrj/src/java/org/apache/solr/common/cloud/Replica.java
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/Replica.java
@@ -168,6 +168,7 @@ public class Replica extends ZkNodeProps implements 
MapWriter {
   public final String core;
   public final Type type;
   public final String shard, collection;
+  private String baseUrl, coreUrl; // Derived values
   private AtomicReference<PerReplicaStates> perReplicaStatesRef;
 
   // mutable
@@ -254,8 +255,9 @@ public class Replica extends ZkNodeProps implements 
MapWriter {
     }
     Objects.requireNonNull(this.node, "'node' must not be null");
 
-    String baseUrl = (String) propMap.get(ReplicaStateProps.BASE_URL);
+    baseUrl = (String) propMap.get(ReplicaStateProps.BASE_URL);
     Objects.requireNonNull(baseUrl, "'base_url' must not be null");
+    coreUrl = ZkCoreNodeProps.getCoreUrl(baseUrl, core);
 
     // make sure all declared props are in the propMap
     propMap.put(ReplicaStateProps.NODE_NAME, node);
@@ -299,11 +301,11 @@ public class Replica extends ZkNodeProps implements 
MapWriter {
   }
 
   public String getCoreUrl() {
-    return ZkCoreNodeProps.getCoreUrl(getBaseUrl(), core);
+    return coreUrl;
   }
 
   public String getBaseUrl() {
-    return getStr(ReplicaStateProps.BASE_URL);
+    return baseUrl;
   }
 
   /** SolrCore name. */
diff --git 
a/solr/solrj/src/java/org/apache/solr/common/cloud/ZkCoreNodeProps.java 
b/solr/solrj/src/java/org/apache/solr/common/cloud/ZkCoreNodeProps.java
index 9c060511445..34da8d239a0 100644
--- a/solr/solrj/src/java/org/apache/solr/common/cloud/ZkCoreNodeProps.java
+++ b/solr/solrj/src/java/org/apache/solr/common/cloud/ZkCoreNodeProps.java
@@ -66,7 +66,7 @@ public class ZkCoreNodeProps {
 
   public static String getCoreUrl(String baseUrl, String coreName) {
     Objects.requireNonNull(baseUrl, "baseUrl must not be null");
-    StringBuilder sb = new StringBuilder();
+    StringBuilder sb = new StringBuilder(baseUrl.length() + coreName.length() 
+ 2);
     sb.append(baseUrl);
     if (!baseUrl.endsWith("/")) sb.append("/");
     sb.append(coreName != null ? coreName : "");
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java 
b/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java
index a96a5d4943d..a05e0e4c4ca 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java
@@ -84,6 +84,9 @@ public interface ShardParams {
   /** Value denoting local replicas */
   String REPLICA_LOCAL = "local";
 
+  /** Value denoting local replicas */
+  String REPLICA_HOST = "host";
+
   /** Value denoting randomized replica sort */
   String REPLICA_RANDOM = "random";
 
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
 
b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
index 17d918a233a..f74006a245f 100644
--- 
a/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
+++ 
b/solr/solrj/src/test/org/apache/solr/client/solrj/routing/NodePreferenceRulesComparatorTest.java
@@ -18,8 +18,10 @@
 package org.apache.solr.client.solrj.routing;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.ZkStateReader;
@@ -33,18 +35,118 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
   @Test
   public void replicaLocationTest() {
     List<Replica> replicas = getBasicReplicaList();
+    Collections.shuffle(replicas, random());
+    List<String> urls = 
replicas.stream().map(Replica::getCoreUrl).collect(Collectors.toList());
 
     // replicaLocation rule
     List<PreferenceRule> rules =
         PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + 
":http://node2:8983";);
     NodePreferenceRulesComparator comparator = new 
NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node2", getHost(replicas.get(0).getNodeName()));
-    assertEquals("node1", getHost(replicas.get(1).getNodeName()));
+    urls.sort(comparator.getUrlComparator());
+    assertEquals(replicas.get(0).getCoreUrl(), urls.get(0));
+  }
+
+  @Test
+  public void replicaLocationLocalTest() {
+    List<Replica> replicas = getMultiPortReplicaList();
+    Collections.shuffle(replicas, random());
+    List<String> urls = 
replicas.stream().map(Replica::getCoreUrl).collect(Collectors.toList());
+
+    // replicaLocation rule
+    List<PreferenceRule> rules =
+        PreferenceRule.from(
+            ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":" + 
ShardParams.REPLICA_LOCAL);
+    NodePreferenceRulesComparator comparator =
+        new NodePreferenceRulesComparator(
+            rules, null, "node1:8983_solr", "http://node1:8983/solr";, "node1");
+    replicas.sort(comparator.getReplicaComparator());
+    assertEquals("node1:8983_solr", replicas.get(0).getNodeName());
+    urls.sort(comparator.getUrlComparator());
+    assertEquals(replicas.get(0).getCoreUrl(), urls.get(0));
+  }
+
+  @Test
+  public void clientComparatorTest() {
+    NodePreferenceRulesComparator clientComparator;
+    clientComparator =
+        new NodePreferenceRulesComparator(
+            PreferenceRule.from(
+                ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":" + 
ShardParams.REPLICA_LOCAL),
+            null);
+    assertNull(clientComparator.getReplicaComparator());
+    assertNull(clientComparator.getUrlComparator());
+    clientComparator =
+        new NodePreferenceRulesComparator(
+            PreferenceRule.from(
+                ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION + ":" + 
ShardParams.REPLICA_HOST),
+            null);
+    assertNull(clientComparator.getReplicaComparator());
+    assertNull(clientComparator.getUrlComparator());
+    clientComparator =
+        new NodePreferenceRulesComparator(
+            PreferenceRule.from(
+                ShardParams.SHARDS_PREFERENCE_NODE_WITH_SAME_SYSPROP + 
":solr.sysProp"),
+            null);
+    assertNull(clientComparator.getReplicaComparator());
+    assertNull(clientComparator.getUrlComparator());
+
+    clientComparator =
+        new NodePreferenceRulesComparator(
+            PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION 
+ ":http://node1";),
+            null);
+    assertNotNull(clientComparator.getReplicaComparator());
+    assertNotNull(clientComparator.getUrlComparator());
+
+    // Even if one preference is not supported, the others should be used
+    clientComparator =
+        new NodePreferenceRulesComparator(
+            PreferenceRule.from(
+                ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION
+                    + ":"
+                    + ShardParams.REPLICA_LOCAL
+                    + ","
+                    + ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION
+                    + ":http://node1";
+                    + ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION
+                    + ":"
+                    + ShardParams.REPLICA_HOST
+                    + ","),
+            null);
+    assertNotNull(clientComparator.getReplicaComparator());
+    assertNotNull(clientComparator.getUrlComparator());
+  }
+
+  @Test
+  public void replicaLocationHostTest() {
+    List<Replica> replicas = getMultiPortReplicaList();
+    Collections.shuffle(replicas, random());
+    List<String> urls = 
replicas.stream().map(Replica::getCoreUrl).collect(Collectors.toList());
+
+    // replicaLocation rule
+    List<PreferenceRule> rules =
+        PreferenceRule.from(
+            ShardParams.SHARDS_PREFERENCE_REPLICA_LOCATION
+                + ":"
+                + ShardParams.REPLICA_HOST
+                + ","
+                + ShardParams.SHARDS_PREFERENCE_REPLICA_TYPE
+                + ":nrt");
+    NodePreferenceRulesComparator comparator =
+        new NodePreferenceRulesComparator(
+            rules, null, "node2:8983_solr", "http://node2:8983/solr";, "node2");
+    replicas.sort(comparator.getReplicaComparator());
+    assertEquals("node2:8984_solr", replicas.get(0).getNodeName());
+    assertEquals("node2:8983_solr", replicas.get(1).getNodeName());
+    urls.sort(comparator.getUrlComparator());
+    assertEquals("node2:8984_solr", replicas.get(0).getNodeName());
+    assertEquals("node2:8983_solr", replicas.get(1).getNodeName());
   }
 
   public void replicaTypeTest() {
     List<Replica> replicas = getBasicReplicaList();
+    Collections.shuffle(replicas, random());
 
     List<PreferenceRule> rules =
         PreferenceRule.from(
@@ -54,7 +156,7 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 + ":TLOG");
     NodePreferenceRulesComparator comparator = new 
NodePreferenceRulesComparator(rules, null);
 
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node1", getHost(replicas.get(0).getNodeName()));
     assertEquals("node2", getHost(replicas.get(1).getNodeName()));
 
@@ -67,9 +169,10 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 + ":NRT");
     comparator = new NodePreferenceRulesComparator(rules, null);
 
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node2", getHost(replicas.get(0).getNodeName()));
     assertEquals("node1", getHost(replicas.get(1).getNodeName()));
+    assertNull(comparator.getUrlComparator());
   }
 
   @Test
@@ -86,6 +189,8 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 ZkStateReader.REPLICA_TYPE, "TLOG"),
             "collection1",
             "shard1"));
+    Collections.shuffle(replicas, random());
+    List<String> urls = 
replicas.stream().map(Replica::getCoreUrl).collect(Collectors.toList());
 
     List<PreferenceRule> rules =
         PreferenceRule.from(
@@ -97,11 +202,15 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 + ":http://node4";);
     NodePreferenceRulesComparator comparator = new 
NodePreferenceRulesComparator(rules, null);
 
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node1", getHost(replicas.get(0).getNodeName()));
     assertEquals("node4", getHost(replicas.get(1).getNodeName()));
     assertEquals("node2", getHost(replicas.get(2).getNodeName()));
     assertEquals("node3", getHost(replicas.get(3).getNodeName()));
+    // The URL comparator does not support replica type, so the replica 
location will be the only
+    // sort criteria
+    urls.sort(comparator.getUrlComparator());
+    assertEquals(replicas.get(1).getCoreUrl(), urls.get(0));
   }
 
   @Test
@@ -123,11 +232,13 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 "NRT"),
             "collection1",
             "shard1"));
+    Collections.shuffle(replicas, random());
+
     // Prefer non-leader only, therefore node1 has the lowest priority
     List<PreferenceRule> rules =
         PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_LEADER + 
":false");
     NodePreferenceRulesComparator comparator = new 
NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node1:8983_solr", replicas.get(3).getNodeName());
 
     // Prefer NRT replica, followed by non-leader
@@ -138,7 +249,7 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 + ShardParams.SHARDS_PREFERENCE_REPLICA_LEADER
                 + ":false");
     comparator = new NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node4", getHost(replicas.get(0).getNodeName()));
     assertEquals("node1", getHost(replicas.get(1).getNodeName()));
 
@@ -153,7 +264,7 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
                 + ":http://host2";);
     comparator = new NodePreferenceRulesComparator(rules, null);
 
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node3", getHost(replicas.get(0).getNodeName()));
     assertEquals("node4", getHost(replicas.get(1).getNodeName()));
     assertEquals("node2", getHost(replicas.get(2).getNodeName()));
@@ -174,8 +285,9 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
             "shard1"));
     rules = PreferenceRule.from(ShardParams.SHARDS_PREFERENCE_REPLICA_LEADER + 
":false");
     comparator = new NodePreferenceRulesComparator(rules, null);
-    replicas.sort(comparator);
+    replicas.sort(comparator.getReplicaComparator());
     assertEquals("node1:8983_solr", onlyLeader.get(0).getNodeName());
+    assertNull(comparator.getUrlComparator());
   }
 
   @Test(expected = IllegalArgumentException.class)
@@ -195,7 +307,7 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
     List<Replica> replicas = getBasicReplicaList();
     List<PreferenceRule> rules = PreferenceRule.from("badRule:test");
     try {
-      replicas.sort(new NodePreferenceRulesComparator(rules, null));
+      replicas.sort(new NodePreferenceRulesComparator(rules, 
null).getReplicaComparator());
     } catch (IllegalArgumentException e) {
       assertEquals("Invalid shards.preference type: badRule", e.getMessage());
       throw e;
@@ -238,6 +350,32 @@ public class NodePreferenceRulesComparatorTest extends 
SolrTestCaseJ4 {
     return replicas;
   }
 
+  private static List<Replica> getMultiPortReplicaList() {
+    List<Replica> replicas = getBasicReplicaList();
+    replicas.add(
+        new Replica(
+            "node1",
+            Map.of(
+                ZkStateReader.NODE_NAME_PROP, "node1:8984_solr",
+                ZkStateReader.BASE_URL_PROP, 
Utils.getBaseUrlForNodeName("node1:8984_solr", "http"),
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "TLOG",
+                ZkStateReader.LEADER_PROP, "true"),
+            "collection1",
+            "shard1"));
+    replicas.add(
+        new Replica(
+            "node2",
+            Map.of(
+                ZkStateReader.NODE_NAME_PROP, "node2:8984_solr",
+                ZkStateReader.BASE_URL_PROP, 
Utils.getBaseUrlForNodeName("node2:8984_solr", "http"),
+                ZkStateReader.CORE_NAME_PROP, "collection1",
+                ZkStateReader.REPLICA_TYPE, "NRT"),
+            "collection1",
+            "shard1"));
+    return replicas;
+  }
+
   private String getHost(final String nodeName) {
     final int colonAt = nodeName.indexOf(':');
     return colonAt != -1


Reply via email to