- Solr CCS (Cross Collection Search)与协调节点(Coordinator Node)的协同工作逻辑及自定义开发指南
Solr 跨集合查询(CCS)的核心是以协调节点(Coordinator Node)为核心引擎,将跨集合 / 分片的查询拆解、并行执行、汇总结果,CCS 是查询能力的抽象,协调节点是该能力的物理执行载体。以下从「协同运行逻辑」「协调节点汇总统计的底层机制」「自定义统计 / 排序逻辑开发」三个维度完整解答。
一、CCS 与协调节点的核心关系
- CCS:是 Solr 定义的「跨多个集合 / 核心查询并返回统一结果」的能力规范,定义了查询语法、参数规则、结果合并的语义。
- 协调节点 :是 SolrCloud 集群中承接客户端查询请求的节点(任意节点均可作为协调节点,默认由集群路由规则分配),是 CCS 逻辑的唯一执行主体------ 所有 CCS 的拆分、并行查询、结果汇总、排序都在协调节点上完成,其他节点仅作为「分片节点」执行子查询。
简单来说:CCS 是 "做什么"(跨集合查询),协调节点是 "谁来做"(执行查询 + 汇总) 。
二、CCS 与协调节点的协同运行逻辑
以你场景(查询 family,big 集合,统计字段)为例,完整运行流程如下(SolrCloud 模式):
步骤 1:请求接收与协调节点绑定
- 客户端(控制台 / API)发送 CCS 请求:
http://<solr-host>:8983/solr/family,big/select?q=*:*&stats=true&stats.field=age; - Solr 集群的负载均衡 / 路由层(或 ZooKeeper 路由规则)将请求分配给任意节点(比如 Node1),该节点即为「协调节点」;
- 协调节点初始化
ResponseBuilder(核心上下文对象),存储查询参数、目标集合、结果容器等信息。
步骤 2:CCS 集合解析与分片拓扑拉取
协调节点解析 CCS 核心参数(collection=family,big),完成「集合→分片」的映射(依赖 ZooKeeper):
- 解析
family集合:从 ZooKeeper 获取其分片拓扑(如family_shard1_replica1(Node2)、family_shard2_replica1(Node3)); - 解析
big集合:从 ZooKeeper 获取其分片拓扑(如big_shard1_replica1(Node4)); - 生成「全部分片列表」:
[family_shard1, family_shard2, big_shard1],并标记每个分片的地址、副本优先级(优先查主副本)。
步骤 3:分布式查询任务拆分(CCS → 分片级任务)
协调节点基于 SearchHandler 中的 DistributedSearchComponent(分布式查询核心组件),将 CCS 请求拆分为「分片级子查询任务」:
-
为每个目标分片生成
ShardRequest(分片请求对象),包含:- 子查询参数(复用原请求的
q、stats、stats.field等); - 分片地址(如
http://Node2:8983/solr/family_shard1); - 任务类型(如
STATS统计、QUERY基础查询);
- 子查询参数(复用原请求的
-
所有
ShardRequest被加入协调节点的「并行任务队列」,由线程池(默认solr.parallel.max.threads=16)调度执行。
步骤 4:并行子查询执行(协调节点→分片节点)
-
协调节点通过 HTTP 客户端(内部封装)并行向所有分片节点发送子查询;
-
分片节点(Node2/Node3/Node4)接收子查询后,独立执行:
family_shard1/2:查询本地索引,计算分片内的age统计值(sum/count/avg 等),返回ShardResponse;big_shard1:无数据,返回空统计结果(sum=0、count=0);
-
分片节点的响应会携带「分片级元数据」(如
numFound、统计结果、排序字段值),异步返回给协调节点。
步骤 5:协调节点汇总分片统计 / 排序结果(核心环节)
这是协调节点的核心工作,所有 CCS 的汇总逻辑都在此完成,核心依赖 ResponseBuilder 收集所有 ShardResponse 并聚合:
5.1 统计结果汇总(以 StatsComponent 为例)
Solr 内置的统计组件(StatsComponent)在协调节点上执行「分片统计值→全局统计值」的聚合,核心逻辑:
-
遍历所有分片的
ShardResponse,提取统计字段的原始值(如每个分片的sum_age、count_age、min_age、max_age); -
按统计类型执行聚合算法:
- 累加类(sum/count):
全局sum = shard1.sum + shard2.sum + ...; - 计算类(avg):
全局avg = 全局sum / 全局count; - 极值类(min/max):
全局min = min(shard1.min, shard2.min, ...);
- 累加类(sum/count):
-
将聚合后的全局统计值写入
ResponseBuilder的statsInfo容器。
5.2 排序结果汇总(若需全局排序)
若请求包含 sort 参数(如 sort=age desc),协调节点的排序汇总逻辑:
- 若分片已按同字段排序(默认分片会按
sort执行本地排序):协调节点采用「归并排序」(Merge Sort),将多个有序分片结果合并为全局有序结果(时间复杂度 O (N log K),K 为分片数); - 若分片未排序:协调节点收集所有分片结果后,在内存中执行全量排序;
- 按
start/rows截取最终分页结果,写入ResponseBuilder的docs容器。
步骤 6:响应组装与返回
- 协调节点将
ResponseBuilder中的全局统计结果、排序后的文档列表、元数据(numFound/QTime)封装为标准 Solr 响应格式; - 将响应返回给客户端,整个 CCS 流程结束(所有汇总结果仅存在于协调节点内存,不会写入任何集合)。
三、协调节点汇总 Shard 统计结果的底层机制
1. 核心依赖的组件 / 类
| 核心类 / 组件 | 作用 |
|---|---|
DistributedSearchComponent |
分布式查询的核心组件,负责拆分分片任务、收集分片响应、触发汇总逻辑 |
StatsComponent |
统计功能的核心组件,定义统计字段的聚合规则(可扩展) |
ResponseBuilder |
全局上下文对象,存储所有分片响应、最终结果、查询参数 |
ShardRequest/ShardResponse |
分片请求 / 响应对象,封装分片地址、参数、返回结果 |
SimpleFacets/StatsValues |
分面 / 统计值的存储结构,支持分片级值的累加 / 聚合 |
2. 汇总的核心流程(源码级简化逻辑)
java
// 协调节点汇总统计的核心逻辑(伪代码)
public void aggregateStats(ResponseBuilder rb) {
// 1. 初始化全局统计容器
StatsValues globalStats = new StatsValues();
// 2. 遍历所有分片响应
for (ShardResponse shardResp : rb.getShardResponses()) {
// 2.1 提取分片级统计结果
StatsValues shardStats = shardResp.getStatsValues("age");
// 2.2 聚合到全局(累加sum/count,计算min/max)
globalStats.addSum(shardStats.getSum());
globalStats.addCount(shardStats.getCount());
globalStats.updateMin(shardStats.getMin());
globalStats.updateMax(shardStats.getMax());
}
// 3. 计算派生指标(如avg)
globalStats.calculateAvg();
// 4. 写入最终响应
rb.getResponse().add("stats", globalStats);
}
3. 不同统计类型的汇总规则
| 统计类型 | 汇总算法 | 示例(family_shard1: sum=100, count=5;family_shard2: sum=200, count=10;big_shard1: sum=0, count=0) |
|---|---|---|
| Count/Sum | 分片值累加 | 全局 count=15,全局 sum=300 |
| Avg | 全局 sum / 全局 count | 全局 avg=20(300/15) |
| Min/Max | 所有分片的 Min/Max 极值 | 若 shard1.min=10,shard2.min=15 → 全局 min=10 |
| Facet(分面) | 分面项计数累加(如 age:20 的 count=shard1+shard2) | age:20 的全局 count=shard1.count (20) + shard2.count (20) |
四、自定义开发协调节点的统计 / 排序逻辑
Solr 支持通过「扩展 Component 组件」自定义协调节点的汇总逻辑,核心思路是:继承 / 重写 Solr 内置的 Component(如 StatsComponent、SearchComponent),在协调节点的汇总阶段插入自定义逻辑。
前提准备
- 环境:Solr 源码 / 自定义插件开发环境(JDK 11+,与 Solr 版本一致,如 Solr 9.x);
- 核心原则:自定义逻辑仅需在协调节点执行(分片节点仍用默认逻辑),需通过
rb.isDistributed()判断是否为分布式查询(CCS 属于分布式查询)。
场景 1:自定义统计汇总逻辑(示例:加权 Sum 统计)
需求:汇总时为不同集合的分片统计值加权重(如 family 分片权重 1,big 分片权重 0.5)。
步骤 1:开发自定义 StatsComponent
java
import org.apache.solr.handler.component.StatsComponent;
import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.common.util.NamedList;
public class CustomWeightedStatsComponent extends StatsComponent {
// 重写协调节点的统计汇总逻辑
@Override
public void finishStage(ResponseBuilder rb) {
// 仅在协调节点执行汇总(分布式查询阶段)
if (rb.isDistributed() && rb.stage == ResponseBuilder.STAGE_GET_FIELDS) {
// 1. 获取所有分片响应
List<ShardResponse> shardResponses = rb.getShardResponses();
// 2. 初始化加权全局统计容器
NamedList<Object> globalStats = new NamedList<>();
double weightedSum = 0.0;
long totalCount = 0;
// 3. 遍历分片响应,按集合加权汇总
for (ShardResponse resp : shardResponses) {
// 获取分片所属集合(从分片地址/元数据解析)
String collection = getCollectionFromShard(resp.getShardInfo().getShardName());
// 提取分片统计值
double shardSum = (Double) resp.getSolrResponse().getResponse().get("stats").get("sum_age");
long shardCount = (Long) resp.getSolrResponse().getResponse().get("stats").get("count_age");
// 4. 自定义加权规则
double weight = "family".equals(collection) ? 1.0 : 0.5; // big 分片权重 0.5
weightedSum += shardSum * weight;
totalCount += shardCount;
}
// 5. 写入自定义统计结果
globalStats.add("weighted_sum_age", weightedSum);
globalStats.add("weighted_avg_age", weightedSum / totalCount);
// 合并到原始统计结果中
rb.rsp.add("custom_stats", globalStats);
}
// 执行父类默认逻辑(保留原有统计)
super.finishStage(rb);
}
// 辅助方法:从分片名解析所属集合(如 family_shard1 → family)
private String getCollectionFromShard(String shardName) {
if (shardName.startsWith("family")) return "family";
if (shardName.startsWith("big")) return "big";
return "";
}
}
步骤 2:注册自定义 Component
- 在
solrconfig.xml中注册自定义组件(所有协调节点需配置):
xml
<searchComponent name="customStats" class="com.yourpackage.CustomWeightedStatsComponent">
<str name="stats.field">age</str>
</searchComponent>
<!-- 将自定义组件加入查询处理器 -->
<requestHandler name="/select" class="solr.SearchHandler">
<arr name="components">
<str>query</str>
<str>customStats</str> <!-- 自定义统计组件 -->
<str>stats</str> <!-- 保留原生统计组件(可选) -->
<str>facet</str>
<str>sort</str>
</arr>
</requestHandler>
步骤 3:打包部署
- 将自定义类打包为 JAR(依赖 Solr 核心包,如
solr-core-9.4.0.jar); - 将 JAR 放入 Solr 节点的
server/solr/lib目录(所有协调节点需部署); - 重启 Solr 集群,生效配置。
场景 2:自定义全局排序逻辑
需求:跨分片排序时,不仅按 age 排序,还按「集合优先级」排序(family 文档优先于 big 文档)。
步骤 1:开发自定义 SortComponent
java
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.common.SolrDocument;
import java.util.Comparator;
import java.util.List;
public class CustomCollectionSortComponent extends SearchComponent {
@Override
public void finishStage(ResponseBuilder rb) {
// 仅在协调节点执行全局排序
if (rb.isDistributed() && rb.stage == ResponseBuilder.STAGE_SORT) {
// 1. 获取所有分片返回的文档列表
List<SolrDocument> docs = rb.getResults().docs;
// 2. 自定义排序器:先按集合优先级,再按age降序
docs.sort(new Comparator<SolrDocument>() {
@Override
public int compare(SolrDocument d1, SolrDocument d2) {
// 2.1 解析文档所属集合(需分片返回时携带集合元数据)
String coll1 = (String) d1.get("_collection"); // 需在分片查询时添加该字段
String coll2 = (String) d2.get("_collection");
// 2.2 集合优先级:family > big
int collCompare = getCollectionPriority(coll2) - getCollectionPriority(coll1);
if (collCompare != 0) return collCompare;
// 2.3 按age降序
Integer age1 = (Integer) d1.get("age");
Integer age2 = (Integer) d2.get("age");
return age2.compareTo(age1);
}
});
// 3. 替换原始排序结果
rb.getResults().docs = docs;
}
}
// 定义集合优先级(数值越大优先级越高)
private int getCollectionPriority(String collection) {
return "family".equals(collection) ? 2 : 1; // big 优先级 1
}
@Override
public String getDescription() {
return "Custom collection-aware sort component";
}
}
步骤 2:配置与部署
- 在
solrconfig.xml中注册自定义排序组件(替换 / 追加到select处理器); - 确保分片查询时返回
_collection字段(在fl参数中添加_collection,如fl=*,_collection); - 打包 JAR 并部署到协调节点,重启生效。
自定义开发的关键注意事项
- 仅在协调节点执行 :通过
rb.isDistributed()判断分布式查询,避免分片节点执行自定义逻辑; - 性能控制 :协调节点汇总 / 排序依赖内存,自定义逻辑需避免全量数据加载(控制
shards.rows); - 元数据传递 :自定义逻辑需分片返回额外元数据(如
_collection)时,需在子查询中通过fl参数显式指定; - 版本兼容 :自定义组件需与 Solr 版本匹配(如 Solr 8.x 和 9.x 的
ResponseBuilder接口有差异)。
五、核心总结
- CCS 与协调节点的协同:CCS 定义跨集合查询的规则,协调节点负责将 CCS 请求拆分为分片任务、并行执行、汇总结果,所有逻辑均在协调节点内存完成;
- 统计汇总机制 :协调节点通过遍历分片响应,按统计类型(sum/count/avg)执行累加 / 极值 / 计算类聚合,核心依赖
StatsComponent; - 自定义开发 :通过扩展 Solr 的
SearchComponent(如StatsComponent/ 自定义 Sort 组件),在协调节点的finishStage阶段插入自定义汇总 / 排序逻辑,打包部署到协调节点即可生效。
若需简化开发,也可通过 Solr 的「自定义函数(Function Query)」实现轻量级统计 / 排序定制(无需开发插件),仅需在查询参数中使用自定义函数(如 sort=custom_weight(age, collection) desc)。