3035 字
约 10 分钟
14
S13-A05 · 搜索架构取舍:自建、托管与混合检索的分层设计

搜索系统的技术选型很少输在"哪个引擎更快",多半输在默认了什么:默认索引同步是可靠的、默认相关性有人调、默认多租户过滤在正确的位置。等这些默认被现实推翻,改造成本已经比选型成本高一个数量级。

这篇不给压测数字,只给判断链:五个决策点 → 自建/托管 → 分层检索 → 融合 → 同步 → 调优组织 → 权限 → 容量 → 演进。


1. 五个决策点

决策点 需要回答的问题 一票否决项
索引由谁维护 写入路径谁控、映射谁改 需要频繁改 mapping 但无人管
查询在哪执行 应用侧拼 DSL 还是网关收口 客户端能直接连集群
相关性怎么调 谁定义"好"、怎么验证 没有标注集也没有点击数据
容量怎么扩 分片、副本、冷热 数据量增长无预估
成本谁承担 人力 vs 账单 团队无检索运维经验

这五个问题里,相关性调优的组织问题最容易被低估:技术上 RRF 与加权都能写,但没有"好"的定义,任何融合参数都只是猜。


2. 自建与托管

维度 自建(ES/OpenSearch) 托管服务
运维负担 高:分片、快照、升级、GC 调优 低:升级与备份由厂商担
版本控制 完全自主,可插插件 受限于厂商支持版本
弹性 需自建扩缩容流程 通常按规格档位伸缩
单价 硬件 + 人力,人力常被漏算 按厂商报价核对,含溢价
数据主权 数据在自有环境 取决于区域与合规条款
故障归因 自己查,能查到内核 黑盒,靠工单
# 自建的隐性成本项:这些不写进预算就会被低估
- 快照恢复演练(每季度,需独立集群验证)
- 版本升级(大版本通常要求滚动重启 + 重索引)
- 映射变更(reindex + 别名切换,需双倍磁盘)
- 容量水位(堆不超过物理内存一半的惯例)

决策规则很朴素:检索不是核心竞争力、团队没有专职检索运维,就不要自建。托管贵在账单,自建贵在人;如果人力不可替代(只有一个人懂),自建的真实成本是"这个人不能休假"。


3. 检索分层:每层只解决一类问题

层 解决 技术选择 失效场景
关键词 精确词、术语、编号 BM25(ES/Lucene) 同义词、错别字、跨语言
结构化过滤 权限、时间、状态 filter 上下文(不参与打分) 过滤字段未建索引
向量 语义相似、改写问法 HNSW/IVF,模型见下 精确编号、否定句
重排 前 N 条精排 cross-encoder 或规则 延迟预算不够
{"query": {"bool": {
  "must":   [{"match": {"title": {"query": "退款流程", "boost": 2}}}],
  "filter": [{"term": {"tenant_id": "t_42"}},
             {"range": {"updated_at": {"gte": "now-2y"}}}]
}}, "size": 50}

要点:过滤条件放 filter 上下文,既不参与打分也能被缓存;放 must 里会污染相关性分数,导致"过滤越严、排序越怪"。向量层与关键词层各自召回,交给融合层处理,不要让向量层承担精确匹配。


4. 融合策略

策略 做法 优点 失效场景
RRF 按名次取 1/(k+rank) 求和 无需分数可比、鲁棒 两路召回质量差太多时被拖累
加权 归一化后线性组合 可调、可解释 分数分布随查询漂移
级联 关键词召回 → 向量扩召回 → 重排 成本可控 首层漏召回则全漏
def rrf(rank_lists, k=60, weights=None):
    scores = {}
    for i, ranks in enumerate(rank_lists):
        w = (weights or [1] * len(rank_lists))[i]
        for r, doc in enumerate(ranks, start=1):
            scores[doc] = scores.get(doc, 0) + w / (k + r)
    return sorted(scores, key=scores.get, reverse=True)

RRF 的价值在于绕开分数不可比:BM25 分数与余弦相似度量纲不同,归一化又会随查询变化。它默认两路召回都"大致可信",所以当向量召回在短查询上明显更差时,RRF 会把噪声抬进结果——这时该做的是分流(短查询只走关键词),而不是调 k。


5. 索引同步的三种模式

模式 一致性 复杂度 适用
双写 最终一致,取决于重试 低,但易漏写 写少读多、可容忍秒级延迟
CDC(binlog/变更流) 最终一致,有顺序保证 中,需处理幂等与回放 主库是唯一事实源
定时全量 周期性一致 低,但窗口内陈旧 小数据量、可接受小时级延迟
双写的三个典型漏洞
1. 写库成功后进程崩溃 -> 索引缺失,且无人重试
2. 先写索引后写库 -> 库回滚,索引留下脏文档
3. 并发双写同一文档 -> 旧值覆盖新值(需版本号或时间戳兜底)

CDC 是多数中大型系统的默认答案,因为它把"同步"变成可重放的流:出错能重放,能补历史,能并行重建索引。代价是要有幂等消费(按主键 + 版本覆盖)和 lag 监控,否则你会得到"搜索结果里有一条三小时前的旧价格"。


6. 相关性调优的组织问题

问题 现实 应对
谁定义"好" 没人,或产品一个人拍 建 rubric:相关性分级 0–3
标注成本 每条分钟级,量级决定预算 分层抽样,只标争议与头部查询
离线指标 NDCG/MRR 可算 与线上点击率常有落差
线上指标 点击、零结果率、改写率 受 UI 与位置偏置污染
# 离线评估:先有分级标注,再谈调参
def ndcg(ranked, labels, k=10):
    dcg = sum((2 ** labels.get(d, 0) - 1) / math.log2(i + 2)
              for i, d in enumerate(ranked[:k]))
    ideal = sorted(labels.values(), reverse=True)[:k]
    idcg = sum((2 ** l - 1) / math.log2(i + 2) for i, l in enumerate(ideal))
    return dcg / idcg if idcg else 0.0

离线涨、线上不涨的三个常见原因:标注集只覆盖头部查询;线上有位置偏置(第一条被点是因为它在第一条);UI 改了但评估没跟着改。所以相关性调优至少要有一个离线集 + 一个线上对照实验,缺一个就会自欺。


7. 多租户与权限过滤的位置

位置 做法 风险
索引层(每租户独立索引) 物理隔离 索引数爆炸、集群状态压力
索引层(共享索引 + 路由字段) routing 到固定分片 路由值设计错则热点
查询层(filter) 强制注入 tenant_id 漏注入即泄漏,需框架兜底
应用层(结果后过滤) 查完再筛 分页数量失真、深分页放大
{"query": {"bool": {
  "filter": [
    {"terms": {"acl_group": ["g_sales", "g_all"]}},
    {"term": {"tenant_id": "t_42"}}
  ]
}}}

铁律:过滤条件必须由框架注入,不能由业务代码手写。手写的下场是某个新接口忘了加,泄漏一次就够上新闻。后过滤(查完再删)还会让"第 2 页只有 3 条"这种诡异现象出现,因为每页被裁掉的数量不可预知。


8. 容量与成本

项 影响 典型量级判断
分片数 并行度与元数据开销 单分片数十 GB 量级较稳,按实际压测定
副本数 可用性与写入放大 至少 1 副本;写入重的场景副本很贵
冷热分层 存储成本 历史数据降副本、缩分片、转只读
向量维度 存储与内存 768/1024 维常见;维度翻倍内存近似翻倍
重排模型 延迟 只对前 N(数十条)做,不要全量
向量存储粗算(量级示意,按实际精度与量化方式核对)
N 条 × D 维 × 4 字节(float32) × (1 + 副本) × 索引开销系数
1,000,000 × 768 × 4B ≈ 3.0 GB 原始向量,HNSW 图结构另有可观开销

向量层的成本不在磁盘,在内存:HNSW 要常驻才能低延迟。要做量化(int8/PQ)就得接受召回率损失,这个取舍必须用评估集验证,不能凭感觉。


9. 演进路径

阶段 形态 触发下一步的信号
一 单集群,单索引 查询延迟随数据量上升
二 读写分离(独立协调/查询节点) 写入影响查询
三 关键词 + 向量混合 语义召回不足,用户改写多
四 加重排与语义层 前 10 条质量成为瓶颈

每个阶段的退出条件要写清楚,否则会为了"架构先进"提前上混合检索,最后维护两套召回却没有评估集,调参全靠直觉。


10. 排错

现象 原因 处理
结果里有已删除文档 双写漏删 / CDC lag 加删除事件、监控 lag
分页越翻越慢 深分页 from+size 改 search_after
加了 filter 排序变怪 条件写在 must 里参与打分 移到 filter
集群 yellow 不恢复 副本无法分配、磁盘水位 查 allocation explain
同义词生效但相关性下降 同义词扩展污染打分 用 search analyzer 单独控
向量召回全是近似重复 文档切片过碎 调 chunk 粒度与去重

11. 边界

场景 更合适 理由
数据量小(万级)、查询简单 数据库 LIKE/全文索引 一套系统省掉同步链路
只需按字段精确过滤 数据库索引 搜索引擎的优势用不上
强事务一致要求 主库查询 + 缓存 索引天然最终一致
查询 QPS 很低 直接查库 + 缓存 集群空闲成本高
只需排序不要相关性 数据库 ORDER BY 无需维护索引

搜索集群的门槛不是数据量,而是是否需要相关性排序 + 多字段组合 + 高并发检索三者同时成立。只满足一项时,数据库通常更划算:少一条同步链路,就少一整类一致性 bug。


12. 自测

  1. 五个决策点里,哪一项缺失会让后面所有调优都失去依据?
  2. 自建与托管的真实成本差异主要体现在哪一项上?
  3. 过滤条件为什么必须放 filter 上下文?
  4. RRF 解决的是什么问题?它在什么情况下会把噪声抬进结果?
  5. 双写的三个典型漏洞分别是什么?CDC 为什么能缓解?
  6. 权限过滤为什么必须由框架注入,而不是业务代码手写?
S13-A05 · 搜索架构取舍:自建、托管与混合检索的分层设计
https://www.clxhxhhr.top/posts/6040/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。