关于 distributed-search
用于设计分布式全文搜索的技能,面向需要在单机无法容纳的数据集上构建排序搜索的工程师。内容涉及索引管道、相关性、自动补全、分片、副本及运维权衡。
主要功能
- 01批量与近实时索引
- 02布尔、BM25 与混合排序
- 03自动补全结构选择
- 04规划分片与副本
- 05分析搜索及索引故障
适用场景
- 01为大型文档集添加排序搜索
- 02选择文档更新后的可搜索时效
- 03设计输入时建议功能
- 04根据搜索流量规划分片与副本
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill distributed-search安装后在 Claude Code 中输入/distributed-search确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.md详细介绍
原文在翻译后有更新,译文可能不是最新。
分布式搜索
快速查找与自由文本查询最匹配的文档,并按相关性排序,处理量超过单台机器的承载能力。搜索出错,要么会导致运行缓慢的 LIKE '%term%' 扫描拖垮主数据库,要么搜索框返回错误结果、侵蚀用户信任——这两种问题在流量或语料规模暴露之前都不会显现。
适用场景
用户输入词语,并期待得到按相关性排序的匹配结果,而不是精确键值查询。语料以文本为主(文档、产品、日志、消息),查询是临时的(任意词语、任意组合),结果需要排序、高亮、分面或输入即搜索。当 WHERE col LIKE 或全表扫描已经成为读取瓶颈,或者需要 B-tree 索引无法支持的模糊/部分匹配时,就应考虑使用它。
不适用场景
访问模式是按已知键获取,或使用固定筛选条件——主数据库索引能以低得多的成本、更稳定地满足需求;应将其保留在 data-storage 中。
语料很小(数千行):进程内筛选或数据库内置的全文索引就足够了;单独搭建搜索集群只会带来纯粹的运维负担(YAGNI)。搜索是数据的派生副本,且最终一致;绝不能将其作为系统记录的权威来源。
先明确需求
- 语料规模和增长情况——文档数量、平均文档大小、索引总字节数?(→
back-of-the-envelope)这决定分片数量。 - 查询 QPS 和类型——是否以读取为主?词项查询、短语、模糊匹配、分面、自动补全?延迟目标(p99)是多少?
- 索引新鲜度——新建/编辑的文档必须在几秒内可搜索(近实时),还是允许延迟几分钟/几小时?
- 相关性要求——精确词项匹配是否足够,还是用户期待“最佳”结果(排序、同义词、错字容错)?
- 写入速率——每秒有多少文档发生变更?这决定索引流水线的规模。
方案
流水线(几乎总是存在):数据源发出文档变更 → 索引流水线对其进行转换/分析 → 倒排索引存储词项→文档的倒排列表 → 查询路径进行匹配和排序。对于基于爬取的系统(网络搜索),在此之前还要加入爬取 → 解析 → 去重;爬虫是一个独立子系统,为同一流水线提供数据。
索引构建模式
- **批量/全量重建索引:**定期重建整个索引。适用于语料变化缓慢,或可以接受数小时延迟的场景。
- **近实时(增量):**持续应用变更,让文档在几秒内即可搜索。适用于用户期望能找到自己刚写入内容的场景。
排序模型
- **布尔/仅筛选:**只进行匹配,不评分。适用于精确筛选(标签、分面),且排序无关紧要的场景。
- **TF-IDF / BM25(词法):**按词频和稀有程度评分。这是默认的全文相关性模型,成本低且易于解释。
- **混合(词法 + 信号):**将 BM25 与热度、新近度或业务加权结合。当“最佳”不只是词语重合时使用。
自动补全
- **内存中的前缀 Trie / FST:**根据前缀以亚毫秒级延迟提供输入即搜索。适用于输入时提供建议。
- **Edge-n-gram 索引:**在主索引中进行前缀匹配。适用于建议也必须遵循筛选条件/相关性要求的场景,但成本更高。
分布方式:将索引拆分为分片(每个分片都是一个针对部分文档构建的独立倒排索引)以满足容量需求,并为每个分片配置副本以提高读取吞吐量和容错能力。分片理论见 data-storage。
权衡
| 选项 | 解决的问题 | 带来的问题 | 何时调整 |
|---|---|---|---|
| 批量重新索引 | 简单、原子切换,无需处理实时写入的复杂性 | 下次构建前数据会过时;全量重建成本高 | 用户需要更新鲜的结果 → 切换为近实时 |
| 近实时 | 数秒内更新;无需全量重建 | 段频繁变动、合并负载增加,写入时刷新成本高 | 写入速率或合并成本压垮节点 → 切换为批量模式/延长刷新间隔 |
| 布尔/筛选 | 成本最低;结果确定 | 无法判断结果的「最佳」程度 | 用户开始评判结果质量 → 加入 BM25 |
| BM25 | 相关性好、易解释、成本低 | 忽略热门度、时效性和意图 | 仅靠词语重叠不够 → 加入混合信号 |
| 混合信号 | 匹配业务/用户意图 | 复杂、难以调试,需要调优数据 | 调优成本超过收益 → 回退到 BM25 |
| 前缀 Trie/FST | 类型提示速度最快 | 需要单独构建/刷新数据结构;忽略筛选条件 | 建议需要支持筛选/相关性排序 → 改用 edge-n-gram |
| 增加分片 | 提高并行度,适用于大型语料库 | 每次查询都要扇出并合并结果;过小的分片会浪费资源 | 扇出延迟占主导 → 减少分片并增大分片容量 |
| 增加副本 | 提高读取 QPS + 高可用性 | 占用更多 RAM/磁盘;写入时存在复制延迟 | 写入放大带来负担 → 减少副本 |
压力下的行为
搜索会通过扇出和派生数据延迟放大问题。
- **查询扇出的尾延迟:**每次查询都会访问所有分片;最慢的分片决定响应时间。一个负载过高或因 GC 暂停的分片会拖慢所有查询。 *缓解措施:*让分片大小均衡、增加副本、限制结果深度、使用超时 + 部分结果。
- **索引与查询争用:**写入/合并激增(批量导入、重新索引)会抢占查询所需的 CPU 和 I/O,导致延迟飙升。*缓解措施:*限制批量索引速率、在非高峰时段安排大型合并、隔离索引节点与查询节点的职责。
- **热点分片/数据倾斜:**分片键不均会导致文档或热门词集中在一个节点上。*缓解措施:*对文档进行哈希路由;重新路由或拆分热点分片。
- 深度分页:
from=100000会迫使每个分片对巨大的结果窗口排序。 *缓解措施:*使用游标/search_after,限制分页深度。 - **流水线积压:**如果数据源生成变更的速度超过索引器的处理速度,数据新鲜度延迟就会无限增长。*缓解措施:*实施背压并使用持久化缓冲区(→
messaging-streaming);监控延迟,而不只是吞吐量。 - **重启后的冷缓存:**文件系统/页缓存为空,因此延迟会飙升,直到缓存预热。*缓解措施:*预热关键查询;逐步提升流量。
**监控:**各分片的 p99 查询延迟、索引延迟(从数据源到可搜索)、段/合并数量、堆/GC、分片均衡情况,以及被拒绝/排队的请求。
如何应用
- 明确输入条件——确定语料库大小、查询 QPS 和查询形式、新鲜度目标及相关性标准(参见先明确这些问题)。如果数据库索引或内置全文搜索能满足访问模式,就到此为止——你不需要搜索集群。
- 参考权衡表进行选择——选择构建模式(新鲜度)、排序模型(相关性标准),并确定自动补全是否需要独立的数据结构。
- 设置关键参数——分片数量(根据索引字节数 ÷ 目标分片大小计算)、副本数量(根据读取 QPS + 高可用性确定)、分析器/分词器(语言、词干提取、同义词),以及刷新间隔(权衡新鲜度与写入成本)。
- 对方案进行压力测试——检查压力下的行为(扇出尾延迟、索引争用、热点分片、深度分页、流水线积压),并为流量特征可能触发的每种情况确认缓解措施。
- 用数字确定规模——确认分片能够容纳语料库,且每个分片的大小处于健康范围;副本能够满足峰值 QPS;索引吞吐量能够将延迟控制在新鲜度预算内(→ 重要指标)。
- 选择提供商——默认采用通用方案;只有在用户指定云服务时才打开提供商文件(参见选择提供商)。
应做与不应做
应做
- 将索引视为可派生、可重建的副本——将事实来源保留在
data-storage中,并确保能够据此完整重建索引。 - 在启动前确定分片大小(语料库 ÷ 目标分片大小);尽量让分片大小均匀,避免过小, 以控制扇出成本。
- 通过持久缓冲区为索引流水线提供数据,避免写入激增超过索引器的处理能力或导致更改丢失。
- 先使用 BM25 排名;只有在词语重合明显无法匹配意图时,才添加热度/新近度信号。
- 将索引延迟与查询延迟分开监控——新鲜度问题可能悄无声息地发生。
不应做
- 不要将搜索引擎用作主存储或用于事务性写入。
- 对于小型语料库,如果数据库全文索引或进程内过滤器就能满足需求,不要急于使用单独的集群(YAGNI)。
- 不要允许深度
from/offset分页;改用游标。 - 不要过度分片——大量微小分片会增加扇出和合并开销,却没有任何收益。
- 不要在服务实时查询的集群上,不限速地运行大规模批量重建索引任务。
重要指标
驱动设计的指标包括:索引总字节数(语料库大小 × 每文档开销,再 ÷ 目标分片大小,得出分片数量)、读 QPS × 扇出(每次查询都会触及所有分片),以及索引吞吐量与更改速率,用于将新鲜度延迟控制在预算范围内。单个分片在一定大小范围内表现良好;超过该范围后,就应拆分。延迟、QPS 和存储量的数据请使用 back-of-the-envelope,此处不再赘述。
接口示意
契约包括文档、查询和倒排列表。文档是带有经过分析的字段的类型化记录,例如 { "id": "p123", "title": "...", "body": "...", "tags": ["a","b"], "price": 9.99 }。查询指定字段、匹配类型、筛选条件、排序和分页游标,例如 q="wireless earbuds", fields=[title^2, body], filter={tag:audio}, sort=_score, search_after=<cursor>。
倒排索引将每个词项映射到倒排列表 (doc_id, term_freq, positions);排名会读取这些内容以计算 BM25。请预先确定分析器
(分词器、转小写、词干提取、同义词)——它属于契约的一部分,更改它需要重建索引。
选择提供方
默认采用上述通用方案(Elasticsearch/OpenSearch、Lucene、Solr、自托管)。如果用户指定了云服务,请阅读
references/providers/<provider>.md,了解托管服务的对应方案、
配额/限制以及提供方特有的权衡。如果该提供方没有对应文件,则采用通用方案。
图示
要展示数据源 → 索引流水线 → 倒排索引(分片 +
副本)→ 查询/排名路径,或爬取→解析→去重前端,请使用插件内的 architecture-diagram 技能——将索引路径和查询扇出显示为独立流程,并在每个分片后方展示副本。
相关构建模块
data-storage— 可替代数据库LIKE/全表扫描进行文本搜索; 搜索负责倒排索引,而分片/分区和复制理论归其所有——提及并链接即可,无需重复讲解。messaging-streaming— 依赖此模块构建索引更新流水线:持久缓冲区将文档更改传递给索引器,并吸收写入激增(投递、排序和背压由该模块负责)。caching— 与此模块配合缓存热门/重复查询,减少扇出负载(缓存击穿、热点键和淘汰处理由该模块负责)。back-of-the-envelope— 为此技能提供输入:语料库字节数、QPS 和新鲜度指标用于确定分片、副本和流水线的规模。system-design— 由编排器负责的概念:推理循环、权衡方法和十大故障模式。
参考资料
references/deep-dive.md— 倒排索引内部机制(倒排列表、段、合并)、爬取/索引/搜索流水线、BM25 评分、近实时刷新、自动补全(Trie/FST/n-gram)、分片路由和副本读取。详细设计搜索层时请阅读。references/providers/{generic,aws,azure,gcp}.md— 各环境的服务映射、限制和注意事项。