从零构建现代搜索引擎(23):模型升级时怎样重建索引
embedding 模型会升级。Qwen3-Embedding-0.6B 今天够用,明天可能被更大或更准的版本替代。新模型产生的向量与旧模型不在同一空间——余弦相似度失去意义。 本篇解决一个工程问题:在不中断服务的前提下,用新模型重建整个向量索引,验证质量,原子切换,并在出问题时回滚。 为什么不能直接替换模型 向量空间不兼容 不同 embedding 模型(即使同一系列的不同版本)学到的向量空间不同。用模型 A 编码的文档向量和用模型 B 编码的查询向量做余弦相似度,结果无意义。 1234模型 A 空间: "搜索引擎" → [0.12, -0.34, 0.56, ...]模型 B 空间: "搜索引擎" → [0.78, 0.11, -0.22, ...]cosine(A_doc, B_query) = 无意义的数字 混用两个版本的向量等于在两套不同的坐标系之间算距离。 必须全量重建 局部替换不可行。如果只用新模型编码新文档,索引里就同时存在两个空间的向量。查询时无论用哪个模型编码查询,都只能正确匹配一半文档。 唯一正确的做法:用新模型对全...
从零构建现代搜索引擎(22):压缩向量与控制推理成本
第 18-19 篇用 float32 向量做 HNSW 搜索。512 维 float32 = 2KB/向量。1 万个 chunk 占 20 MB,尚可接受。但向量数增长到百万级时,内存开销达到 GB 级别——单机装不下,或者装得下但挤占了其他组件的内存预算。 本篇用量化减小向量体积,用缓存和批处理降低 embedding 推理的调用成本。 向量存储的成本账 内存占用 512 维 float32 向量的存储开销: 向量数 原始向量 含 HNSW 图 备注 10K 20 MB ~30 MB 教学语料 100K 200 MB ~300 MB 中等站点 1M 2 GB ~3 GB 大型站点 10M 20 GB ~30 GB 超出单机内存 HNSW 图的边(每节点 M 个邻居 × int32 node ID)增加约 50% 的额外开销。 推理成本 每次查询需要调用 embedding 服务编码查询文本。每次文档写入需要编码 chunk 文本。 操作 延迟(GPU) 延迟(CPU) 编码 1 条文本 ~5 ms ~50 ms 编码 100 ...
从零构建现代搜索引擎(21):让重排序模型检查候选
上一篇用 RRF 将 BM25 和向量检索的结果合并为一个排名。但 RRF 的融合依据只有排名位置——它不知道候选文档和查询的实际匹配程度。一篇排在 BM25 第 2 名的文档可能因为标题恰好包含查询词而得分高,但正文和查询主题完全无关。 本篇引入重排序模型(reranker),对 RRF 返回的 Top-N 候选逐一做精细评分,找出真正最相关的文档。 二阶段检索 为什么不用 reranker 做全量搜索 Cross-encoder reranker 对每个 (query, document) pair 做联合编码——query 和 document 的每个 token 之间做 full attention。这比 bi-encoder(第 18 篇)精确得多,但也慢得多。 方法 1 万文档耗时 10 万文档耗时 BM25 ~5 ms ~20 ms 向量 HNSW ~5 ms ~10 ms Cross-encoder ~5 s ~50 s Cross-encoder 对每个候选都要过一遍完整的 Transformer 前向传播。对 1 万文档逐个打分需...
从零构建现代搜索引擎(20):将关键词与向量结果合并
BM25 擅长精确标识符和稀有术语,向量检索擅长语义匹配和跨语言。单独使用任何一种都会在对方擅长的查询类型上表现不佳。本篇把两路召回的结果合并为一个排名——混合检索。 两路召回各有盲区 在第 16 篇的评测集上分别运行 BM25 和向量检索,按查询类型统计 nDCG@10: 查询类型 BM25 向量检索 差距 精确标识符 高 低 BM25 胜出,向量空间对标识符区分度差 概念查询 中 高 向量胜出,语义匹配覆盖同义表达 中英混合 中 中高 向量稍好,跨语言 embedding 有优势 同义改写 低 高 向量大幅胜出 多条件 中 中 各有优劣 无答案 好 差 BM25 正确返回零结果,向量总会返回"最近"的 没有一路在所有类型上都最优。混合检索的目标:在每种类型上至少达到两路中较好的那个。 Reciprocal Rank Fusion 为什么不直接加分数 BM25 分数通常在 0-30 之间,向量相似度在 0-1 之间。直接加权求和 α × BM25 + β × cosine 有三个问题: 量纲不同——需要归一化,但归...
从零构建现代搜索引擎(19):用 HNSW 加速近邻搜索
上一篇用精确扫描完成了向量检索的基线。精确扫描遍历所有向量计算点积,结果是精确的,但复杂度 O(N×D) 不可扩展。1 万个 chunk 只需 5 毫秒,10 万就要 50 毫秒,100 万要半秒——远超搜索服务的延迟预算。 本篇引入 HNSW(Hierarchical Navigable Small World),一种基于图的近似最近邻算法。Lucene 10.x 内置 HNSW 实现,可以直接通过 KnnFloatVectorField 使用。 为什么需要近似搜索 精确最近邻搜索保证找到真正最近的 K 个向量,但时间复杂度是线性的。搜索服务的延迟预算通常在 50 毫秒以内(整个搜索流程,不只是向量检索),线性扫描在大规模数据上无法满足。 近似最近邻(ANN)牺牲少量精度换取数量级的速度提升。具体来说:精确 Top-10 中的某些结果可能在 ANN 的 Top-10 中排到了第 11 或第 12——被遗漏了。但 ANN 返回的 Top-10 中 95% 以上是正确的,搜索质量几乎无损。 HNSW 的分层图结构 灵感来源:跳表 回忆第 11 篇的跳表:底层是完整链表,上层是稀疏...
从零构建现代搜索引擎(18):将文档编码成可检索向量
前面十七篇的检索全靠词汇匹配——查询和文档必须共享相同的 term 才能产生分数。搜索"如何提高程序运行速度",如果文档里写的是"性能优化",BM25 给出的分数是零。同义词表能覆盖一部分,但不可能穷举所有语义等价关系。 本篇引入向量检索:用 embedding 模型将文本编码为高维向量,在向量空间中用距离衡量语义相似度。先做精确扫描建立基线,下一篇再用 HNSW 加速。 从词汇匹配到语义匹配 BM25 的语义鸿沟 查询 相关文档包含 BM25 匹配 “如何提高程序运行速度” “性能优化最佳实践” 零分——没有共同 term “machine learning” “机器学习入门” 零分——跨语言 “数据库挂了怎么办” “MySQL 故障恢复指南” 低分——只有"数据库"模糊相关 同义词表(第 16 篇)缓解了一部分问题,但维护成本高,且无法覆盖所有隐含的语义关系。 向量空间的思路 将文本映射为 D 维向量(D 通常是 512 或 1024)。映射由 embedding 模型完成——模型在大量...
从零构建现代搜索引擎(17):查询理解与链接质量信号
用户在搜索框里输入的查询经常不是最理想的检索条件:拼错了字、只打了半个词、用了口语化的说法。前一篇优化了分词和字段权重,本篇在查询到达索引之前对它做预处理——拼写纠错、前缀补全、查询改写——再加入一个与查询无关的信号:链接结构。 前缀补全 问题 用户在搜索框中输入 “java 内” 时,如果等到用户按下回车才开始搜索,就错过了一个引导用户的机会。前缀补全在用户还在打字时就列出可能的完整查询,减少输入量,同时引导用户使用索引中实际存在的词汇。 Trie 实现 最基础的前缀补全用 Trie(前缀树): 123456789101112131415161718192021222324252627282930313233343536373839404142434445class TrieNode { final Map<Character, TrieNode> children = new HashMap<>(); boolean isTerminal; int frequency; String term;}class...
从零构建现代搜索引擎(16):在自己的查询集上优化相关性
第 15 篇把搜索内核切换到了 Lucene,同一组 fixture 在两个后端上对照通过。但 fixture 只有十来篇文档、几个查询——不足以暴露真正的相关性问题。 本篇把评测集从 30 个查询扩展到 100 个,分离开发集与冻结测试集,然后在开发集上逐步调整中文分词、字段权重和同义词,观察每个变量对搜索质量的实际影响。 为什么 30 个查询不够 第 03 篇建立的 30 个查询覆盖了基本查询类型,但存在统计问题: 单个查询的 nDCG 波动大——一个查询从 0.8 变到 0.6,30 个查询的平均值就移动了 0.007 某些查询类型只有 2-3 个样本,无法判断改善是否稳定 调参时容易"碰巧"在这 30 个查询上找到最优值,换一批查询就失效 100 个查询不算多,但足以让每个类型有 10+ 个样本,统计结论更可靠。 查询分类与扩展 六种查询类型 类型 数量 示例 特征 精确标识符 15 NullPointerException、BM25Similarity 必须精确匹配,不能被分词或同义词替换 概念查询 25 “倒排索引原理”、“...
从零构建现代搜索引擎(15):将自制内核接到 Lucene
前面十四篇从零实现了一个教学级搜索引擎:分词、倒排索引、BM25、段文件、崩溃恢复、摘要高亮、HTTP API。它能跑,能搜,能翻页。但它有一个根本问题——每一个组件都是教学级的,没有经过大规模生产验证。 Apache Lucene 是目前最广泛使用的开源搜索库。Elasticsearch、Solr、OpenSearch 底层都是 Lucene。它的分词器、索引格式、评分算法、段合并策略经过二十多年的工程打磨。与其继续在自制引擎上堆功能,不如把核心替换为 Lucene,把精力放在搜索质量和上层功能上。 本篇保持外部接口(搜索 API、文档导入格式)不变,把内部实现从自制内核切换到 Lucene 10.5.1。切换后用同一组 fixture 文档和查询对照两个后端,逐项记录差异。 迁移策略 保持契约不变 外部接口不变: 12输入:Document(title, url, content, language)输出:SearchResult(items[title, url, snippet, score], totalHits, page) 内部替换: 组件 自制 Lucen...
从零构建现代搜索引擎(14):搜索 API 与第一个网页界面
前面十三篇实现了一个完整的搜索引擎内核:倒排索引、BM25 评分、段文件、崩溃恢复、摘要和高亮。但它只能通过 Java main 方法调用——没有 API,没有界面,用户没法用。 本篇把搜索引擎包装成一个 HTTP 服务,加一个最简单的网页界面,让用户在浏览器中输入查询、看到结果、翻页。同时处理搜索服务必须面对的工程问题:参数校验、超时、错误反馈。 HTTP 搜索端点 JDK 内置 HTTP Server Java 25 自带 com.sun.net.httpserver.HttpServer,不需要引入 Spring Boot 或其他框架。教学场景下它足够: 12345HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);server.createContext("/api/search", this::handleSearch);server.createContext("/", this::handleStatic);server.setExecu...











