4812 字
约 16 分钟
1
通用型聚合搜索设计文档

通用型聚合搜索设计文档

1. 文档定位

本文沉淀一套可复用的聚合搜索设计思路,适用于需要在一个搜索入口中接入多种业务数据、内部数据库、搜索引擎或外部 API 的系统。

它不限定具体业务。商品、课程、文章、用户、视频、图片、公众号文章、企业信息等,都可以按照这套思路接入。

本文解决的核心问题是:

用户只输入一次关键词,系统如何统一调度多个不同的数据源,并以稳定、可扩展的方式返回结果?

2. 什么是聚合搜索

聚合搜索是指:系统对外提供一个统一的搜索入口,内部根据搜索类型或业务规则,调用多个独立的数据源,最后将结果组合返回。

这里的“聚合”是业务能力聚合,不是 Elasticsearch 的 aggs 统计聚合。

聚合接口并不要求所有数据存放在同一个地方:

统一搜索接口
      ↓
多个数据源
  ├─ MySQL
  ├─ Elasticsearch
  ├─ 第三方 API
  ├─ 网页抓取
  └─ 其他内部服务

3. 适用场景

当系统出现下面任意需求时,可以考虑聚合搜索:

  • 一个搜索框需要同时搜索多个业务对象。
  • 不同业务对象的存储位置和查询方式不同。
  • 希望前端只调用一个接口。
  • 后续可能持续增加新的搜索类型。
  • 某些结果来自外部 API 或第三方平台。
  • 希望把数据源的具体实现细节隔离在后端内部。

不适合强行聚合的场景包括:

  • 业务只需要查询单张表,且没有扩展计划。
  • 所有结果必须进行严格的全局相关性排序,但不同来源无法提供统一评分。
  • 外部数据源不稳定,且系统没有容错、缓存和限流能力。

4. 设计目标

一套合格的聚合搜索设计,通常需要满足以下目标:

  1. 统一入口:前端不需要知道每个数据源的接口地址。
  2. 统一协议:不同数据源尽量使用一致的关键词、分页和结果封装方式。
  3. 解耦实现:新增一种数据源时,尽量不修改核心聚合逻辑。
  4. 独立演进:每个数据源可以独立更换数据库、搜索引擎或第三方服务。
  5. 可控失败:单个数据源异常时,可以根据业务要求选择整体失败或局部降级。
  6. 可观测:能够知道每个数据源耗时、命中数和失败原因。
  7. 可治理:明确数据来源、授权范围、更新频率和合规边界。

5. 推荐架构

推荐将系统拆成以下角色:

Controller
   ↓
Facade / Orchestrator
   ↓
DataSourceRegistry
   ↓
DataSource Adapter
   ↓
具体查询实现

各层职责如下:

角色 主要职责 不负责什么
Controller 接收请求、参数初步校验、返回统一响应 不直接操作数据库或第三方 API
Facade / Orchestrator 分发任务、并发执行、聚合结果、统一异常策略 不承载每个数据源的查询细节
Registry 根据类型找到对应数据源 不执行具体查询
DataSource Adapter 将统一请求转换为数据源专属请求 不决定全局业务流程
Repository / Client 访问 MySQL、ES、外部 API 等 不组装跨数据源结果
Result Assembler 统一结果格式、分类或排序 不隐藏关键错误

6. 统一数据源接口

最小可行接口可以设计成:

public interface DataSource<T> {

    Page<T> doSearch(String searchText, long pageNum, long pageSize);
}

实际项目中建议逐步扩展成统一请求对象:

public interface SearchDataSource<T> {

    SearchResult<T> search(SearchContext context);
}

统一上下文可以包含:

关键词
搜索类型
页码
分页大小
排序方式
过滤条件
当前用户
请求 traceId
超时时间

统一接口的价值不在于让所有数据源内部实现相同,而在于让聚合层调用方式稳定。

例如:

用户数据源:MySQL LIKE 或专门的用户索引
商品数据源:ES 全文检索
图片数据源:第三方图片 API
公众号数据源:授权的内容服务 API

它们内部完全不同,但上层都可以按照统一协议调用。

7. 两种聚合模式

7.1 指定类型搜索

用户明确选择一种搜索类型时,只调用对应数据源:

type = article
      ↓
ArticleDataSource
      ↓
返回文章结果

适合分类页、单独的搜索标签页和需要更丰富筛选条件的场景。

7.2 全类型搜索

用户没有指定类型时,同时调用多个数据源:

type = all / empty
      ↓
并发调用用户、文章、图片、视频等数据源
      ↓
分别整理结果
      ↓
返回分类聚合结果

全类型搜索有两种返回方式:

分类返回

{
  "users": [],
  "articles": [],
  "pictures": []
}

优点是结构清晰、实现简单,不同来源之间不需要比较分数。

统一列表返回

{
  "items": [
    { "type": "article", "score": 0.91, "data": {} },
    { "type": "user", "score": 0.75, "data": {} }
  ]
}

这种方式需要解决跨数据源评分、字段统一、结果去重和排序问题,复杂度明显更高。除非业务明确要求,否则建议优先采用分类返回。

8. 接口契约设计

8.1 请求模型

通用请求可以设计为:

{
  "searchText": "关键词",
  "type": "article",
  "current": 1,
  "pageSize": 10,
  "sortField": "createTime",
  "sortOrder": "desc"
}

建议约定:

字段 建议规则
searchText 去除首尾空格,限制最大长度,必要时做敏感词处理
type 使用稳定的枚举值,不直接使用中文展示名称
current 从 1 开始,禁止小于 1
pageSize 设置上限,防止一次返回过多数据
sortField 使用白名单,禁止直接拼接用户输入
sortOrder 只允许 ascdesc

8.2 响应模型

指定类型时可以统一返回:

{
  "code": 0,
  "data": {
    "type": "article",
    "records": [],
    "total": 0,
    "current": 1,
    "pageSize": 10,
    "tookMs": 32
  },
  "message": "ok"
}

全类型分类返回可以设计为:

{
  "code": 0,
  "data": {
    "users": {
      "records": [],
      "total": 0
    },
    "articles": {
      "records": [],
      "total": 0
    },
    "pictures": {
      "records": [],
      "total": null
    },
    "meta": {
      "partial": false,
      "failedSources": []
    }
  },
  "message": "ok"
}

外部 API 通常无法准确返回总数,可以允许 totalnull,不要伪造一个看似精确的数量。

9. 数据源注册与路由

不建议在 Facade 中堆积大量 if-else

if ("user".equals(type)) {
    // 搜用户
} else if ("article".equals(type)) {
    // 搜文章
} else if ("picture".equals(type)) {
    // 搜图片
}

可以使用注册器:

Map<String, SearchDataSource<?>> dataSources = Map.of(
        "user", userDataSource,
        "article", articleDataSource,
        "picture", pictureDataSource
);

路由流程:

请求 type
  ↓
校验 type 是否支持
  ↓
Registry 查找 DataSource
  ↓
调用统一 search 方法

新增数据源时,通常只需要:

  1. 定义结果对象。
  2. 编写数据源适配器。
  3. 接入具体 Repository 或第三方 Client。
  4. 注册类型。
  5. 增加接口和异常测试。

10. 并发聚合设计

全类型搜索通常适合并发调用:

CompletableFuture<Result<User>> userTask =
        CompletableFuture.supplyAsync(() -> userSource.search(context));

CompletableFuture<Result<Article>> articleTask =
        CompletableFuture.supplyAsync(() -> articleSource.search(context));

CompletableFuture.allOf(userTask, articleTask).join();

但并发不是越多越好,需要考虑:

  • 线程池大小是否受控。
  • 外部 API 是否有并发限制。
  • 单个数据源超时是否会拖慢整体接口。
  • 是否需要隔离不同数据源的线程池。
  • 是否需要取消超时任务。
  • 是否需要记录每个分支的耗时。

生产环境建议使用自定义线程池,而不是无条件使用公共线程池:

aggregate-search-pool
  ├─ 核心线程数
  ├─ 最大线程数
  ├─ 队列长度
  └─ 拒绝策略

11. 异常与降级策略

聚合接口需要先明确业务选择:

11.1 整体失败

任何一个关键数据源失败,整个请求返回错误。

适合:所有数据源都是核心结果,缺少任何一个都会导致业务不可用。

11.2 局部降级

某个数据源失败时,保留其他成功结果,并返回失败来源信息:

{
  "meta": {
    "partial": true,
    "failedSources": ["picture"]
  }
}

适合:图片、推荐、第三方内容等非核心数据源。

11.3 空结果降级

数据源超时后返回空列表,并记录监控告警。该方式必须在响应中标记来源不可用,否则前端和用户会误以为确实没有搜索结果。

建议为每个数据源定义:

配置项 说明
超时时间 超过时间后终止或忽略该分支
是否核心 决定失败时整体失败还是局部降级
重试次数 外部网络错误是否重试
是否缓存 是否允许返回短时间旧数据
失败提示 是否向前端展示

12. 分页、排序与去重

12.1 分类分页

每个数据源独立分页:

用户第 1 页
文章第 1 页
图片第 1 页

这是最简单、最稳定的方案。不同来源不需要共享总数和排序分数。

12.2 全局分页

如果要求所有类型混合成一条列表,就必须解决:

  • 不同数据源的评分如何归一化。
  • 哪类结果应该优先展示。
  • 不同来源的 total 如何合并。
  • 相同内容如何去重。
  • 下一页如何保持稳定。

通常需要引入统一的排序分数:

finalScore = sourceWeight × sourceScore

但这只是工程折中,不代表不同数据源的分数天然可比。

12.3 去重策略

跨数据源去重时,优先选择稳定业务标识:

source + sourceId

不要只用标题或 URL 判断重复,因为标题可能相同,URL 也可能发生变化。

13. Elasticsearch 与聚合搜索的关系

聚合搜索不等于“所有数据都放入 ES”。ES 只是其中一种数据源。

常见组合是:

聚合入口
  ├─ 用户 → MySQL
  ├─ 文章 → Elasticsearch + MySQL 回源
  ├─ 图片 → 第三方 API
  └─ 视频 → 视频平台 API

当某类内容需要全文搜索、分词、相关性排序和高性能分页时,可以为该类型建立独立 ES 索引。

如果 ES 只是索引副本,建议采用:

主数据库保存权威数据
ES 保存搜索字段和排序所需字段
搜索命中后按 ID 回源主数据库

这样可以避免把点赞数、库存、权限、审核状态等高频变化或高敏感字段完全交给搜索索引维护。

14. 数据同步设计

使用 ES 的数据源必须回答三个问题:

  1. ES 中的数据从哪里来?
  2. 数据什么时候更新?
  3. 更新失败后如何补偿?

常见同步方式:

方式 特点 适用场景
定时同步 实现简单,有一定延迟 学习项目、低实时场景
业务双写 延迟低,但一致性处理复杂 小型系统、可接受补偿
消息队列 解耦、可重试 中大型业务
Binlog / Canal 减少业务侵入,接近实时 生产级数据同步
Logstash 等管道 配置化、适合批量同步 数据管道场景

同步文档至少需要记录:

  • 全量初始化方法。
  • 增量同步触发方式。
  • 删除和逻辑删除如何处理。
  • 重试和死信处理。
  • 索引重建方法。
  • 同步延迟指标。
  • 数据校验和对账方式。

15. 外部数据源接入

对于第三方 API、网页抓取和内容合作接口,不能只记录“调用地址”,还需要记录业务边界:

15.1 接入前确认

  • 是否有公开 API 或正式授权。
  • 是否需要账号认证或商业资质。
  • 是否允许搜索、存储和展示返回内容。
  • 是否限制调用频率、并发数和 IP。
  • 是否要求展示来源和版权信息。
  • 数据是否允许缓存或建立自己的索引。

15.2 运行时保护

  • 连接超时和读取超时。
  • 重试次数和退避策略。
  • 限流和熔断。
  • 结果缓存。
  • 外部结构变化监控。
  • 敏感信息和密钥保护。
  • 来源字段和更新时间记录。

15.3 合规边界

网页可访问不代表可以任意抓取、保存和再分发。接入前应遵守目标平台的服务条款、robots 规则、版权要求和适用法律法规。

16. 安全设计

聚合搜索常见安全风险包括:

风险 典型问题 防护方式
SQL 注入 排序字段直接拼接 SQL 字段白名单
ES 查询注入 直接接受任意查询 DSL 服务端构造查询
SSRF 用户控制外部请求地址 固定域名白名单
爬虫滥用 pageSize 过大、频繁请求 限流、分页上限、验证码
密钥泄露 API Key 出现在前端或日志 后端保管、脱敏日志
内容风险 外部结果含恶意链接或脚本 输出编码、内容审核
权限绕过 搜索返回不应公开的数据 在数据源层执行权限过滤

搜索只是入口,不应绕过业务权限。每个数据源都要明确“哪些结果允许当前用户看到”。

17. 可观测性

建议为一次聚合请求生成 traceId,并记录:

traceId
searchType
searchText(必要时脱敏)
sourceName
sourceStatus
sourceLatency
resultCount
cacheHit
errorCode

推荐监控指标:

  • 聚合接口总耗时。
  • 各数据源平均耗时和 P95/P99。
  • 各数据源错误率。
  • 超时次数。
  • 空结果比例。
  • 缓存命中率。
  • ES 查询耗时和命中数。
  • 外部 API 配额消耗。

18. 测试清单

18.1 单元测试

  • 每个数据源能根据统一请求生成正确的内部查询。
  • Registry 能正确路由类型。
  • Facade 能正确组装分类结果。
  • 指定类型和全类型分支行为正确。
  • 分页、排序和过滤参数校验正确。

18.2 异常测试

  • 某个数据源超时。
  • 某个数据源返回空结果。
  • 某个数据源返回格式错误。
  • ES 不可用。
  • MySQL 回源失败。
  • 外部 API 达到限流阈值。
  • 所有数据源同时失败。

18.3 一致性测试

  • 主数据库新增数据后是否最终可搜索。
  • 修改标题后旧标题是否会消失。
  • 删除或逻辑删除后是否仍会返回。
  • ES 有脏数据但主数据库不存在时是否能清理。
  • 同一请求中的结果顺序是否稳定。

18.4 性能测试

  • 单类型搜索吞吐量。
  • 全类型并发搜索耗时。
  • 最慢数据源对整体响应的影响。
  • 外部 API 慢响应时的线程池占用。
  • 大关键词、高并发和大分页请求。

19. 推荐落地步骤

可以按照下面的顺序设计一套新的聚合搜索:

  1. 明确搜索对象和用户期望的结果形态。
  2. 列出每个对象的真实数据来源。
  3. 判断每个来源是否需要全文检索、缓存或同步。
  4. 确定分类返回还是统一列表返回。
  5. 设计统一请求和响应模型。
  6. 定义数据源接口。
  7. 为每个来源编写独立适配器。
  8. 使用 Registry 完成类型路由。
  9. 在 Facade 中实现指定类型和全类型流程。
  10. 明确超时、重试、降级和权限策略。
  11. 增加日志、指标和链路追踪。
  12. 用测试验证正常、异常和一致性场景。

20. 可复用文档模板

以后设计类似聚合能力时,可以直接复制下面的结构:

# XXX 聚合搜索业务文档

## 1. 业务目标
- 用户为什么需要统一搜索?
- 搜索结果解决什么问题?

## 2. 搜索范围
| 类型 | 标识 | 数据来源 | 状态 |
| --- | --- | --- | --- |

## 3. 接口契约
### 请求
### 响应
### 分页和排序

## 4. 整体架构
- Controller
- Facade
- Registry
- DataSource
- Repository / Third-party Client

## 5. 数据源说明
### 5.1 数据源 A
### 5.2 数据源 B

## 6. 指定类型搜索流程

## 7. 全类型搜索流程

## 8. 并发、超时与降级

## 9. 数据同步与一致性

## 10. 权限、安全与合规

## 11. 缓存与性能

## 12. 监控与日志

## 13. 测试和验收标准

## 14. 后续扩展点

## 15. 源码或服务定位

21. 最终原则

设计聚合搜索时,可以记住四句话:

  1. 一个入口,不等于一个存储。
  2. 统一调用,不等于内部实现相同。
  3. 并发查询,不等于可以忽略超时和降级。
  4. 能搜到数据,不等于拥有随意抓取和展示数据的权利。

真正可复用的聚合设计,应该让新增数据源变得容易,让异常边界变得清楚,让前端调用保持稳定,也让后续的性能、一致性和合规治理有明确落点。

通用型聚合搜索设计文档
http://www.clxhxhhr.top/posts/437/
作者
clxstart
发布于
2026-09-04
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。