尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Qdrant的语义搜索引擎搭建:从关键词到向量检索

基于Qdrant的语义搜索引擎搭建:从关键词到向量检索 如果你曾经在公司知识库里输入“线上服务变慢怎么办”返回的结果全是“如何调整日志级别”你大概率会怀疑搜索功能是不是坏了。这不是产品经理的锅而是传统搜索的技术范式决定了它只能做字面匹配。它知道你输入了什么词但不知道你在问什么。过去十年这个矛盾一直被“用户学关键词”掩盖直到语义向量成为工程基础设施才真正有了不同的解法。最近一段时间各类“新型搜索引擎”接连出现不少读者会来问它们到底新在哪里是换了个交互界面还是底层真的变了我的判断是大多数所谓的新型搜索底层都做了一件相同的事把文本从“字符序列”变成“语义坐标”再用向量距离替代关键词匹配。这件事不是大厂专属现在用 Qdrant 加一个开源 Embedding 模型完全可以在自己机器上搭出一套能跑的语义搜索引擎。这篇文章会从传统搜索瓶颈讲起解释向量检索为什么能解决“语义召回”问题然后给出一套基于 Qdrant、Python 和开源中文 Embedding 模型的完整实现。包含环境准备、代码、运行验证、常见问题排查和工程建议。读完你可以直接把这套流程迁移到自己的文档搜索、知识库问答或 RAG 应用里。1. 传统搜索引擎的瓶颈为什么关键词匹配不够用了要理解“新型搜索引擎”先要知道传统搜索引擎做了什么。市面上主流的开源搜索方案比如 Elasticsearch、OpenSearch、Solr核心数据结构都是倒排索引。你可以把它理解成一本书末尾的“关键词索引页”每个词后面跟着一组文档 ID搜索时先找到包含这个词的文档列表再用 BM25 这类相关性算法排序。BM25 的思路大体是一个词在文档里出现得越多相关性越高但如果在所有文档里都出现区分度反而低。这套算法在学术和工程上都被验证过很多年直到今天也完全不过时。真正的问题是它评估的是“字面相关性”不是“语义相关性”。举一个开发场景里的例子。开发者搜索“接口超时”可能真正想找的是“OpenFeign 调用报 Read timed out 如何处理”。如果文档里写的是“远程调用请求未能按时返回”关键词匹配就找不到了。反过来搜索“数据库连接池满了”文档里写的是“HikariCP 活跃连接数达到最大”也匹配不上。同义词、口语化表达、中英文混写都会让词法匹配失效。这不是 BM25 的缺陷而是倒排索引的边界。它擅长处理精确词、专有名词、代码片段、路径和类名但遇到“语义上相似、字面上不同”的表达表现得非常脆弱。所以依赖关键词搜索的知识库经常出现两种结果要么召回很少要么召回很多但全不相关。用户被迫反复换关键词本质上是在替搜索引擎做语义映射。传统搜索的另一个问题是它对“用户意图”没有建模能力。搜索“面试题”和搜索“如何准备面试”在倒排索引看来是两组完全不同的词但用户的意图非常接近。想要通过字面匹配去建立这种关联需要人工维护同义词表、近义词扩展和规则维护成本极高而且永远追不上自然语言的变化。小结论很清晰传统搜索在精确匹配场景依然高效但在语义召回、意图理解、多语言表达上存在结构性短板。新型搜索引擎解决的核心问题不是“搜索速度慢”而是“搜索找不到”。2. 新型搜索引擎的核心向量检索与语义匹配所谓新型搜索本质是用 Embedding 模型把文本变成一个固定长度的数字列表也就是“向量”。这个向量的意义在于语义相近的句子即使字面完全不同它们在多维空间里的距离也相对接近。搜索引擎不再比较“词是否出现过”而是比较“语义方向是否一致”。这个过程可以这样理解。把“卡顿”“掉帧”“FPS 低”“运行不流畅”这四句话放进同一个语义空间它们的向量分布会很接近因为模型在大量语料中学会了这些表达描述的是相近的现象。如果改用字符匹配这四句话几乎没有任何共同词。有了向量之后检索就变成了数学运算最常见的是余弦相似度。两个向量之间的夹角越小说明语义越接近。搜索时把用户问题向量化在已经存好的文档向量集合里找距离最近的 Top K 条这就是“向量召回”。但这里要提醒一个很容易产生的误解向量检索不是要替代倒排索引而是补上“语义召回”这一层能力。实际工程里向量检索对精确词、代码、型号、人名这种实体并不稳定反而是 BM25 的强项。真正可落地的方案是混合检索用 BM25 处理词面匹配用向量处理语义匹配再把两路结果融合排序。下面用表格对比两种检索方式的核心差异维度倒排索引 BM25向量检索匹配粒度词项/短语语义向量同义词能力差需要人工扩展强模型自动学习精确词匹配强适合代码、ID、专名一般依赖模型质量索引构建分词、倒排、统计权重模型推理 向量索引主要瓶颈字面匹配模型效果与计算成本典型应用日志、商品、代码搜索知识库、RAG、问答还有一个核心组件是向量数据库。它会负责向量的存储、索引和相似度检索。市面上常见的有 Qdrant、Milvus、Weaviate、Chroma以及 PostgreSQL 的 pgvector 扩展。对于个人项目和中小团队Qdrant 是比较折中的选择单机部署简单Python SDK 完善支持 payload 过滤和混合检索社区也活跃。可以说新型搜索引擎的出现是整个 AI 基础设施下沉的结果。以前语义检索是研究机构才玩得起的事情现在开源模型和向量数据库把门槛降到了“一个 Docker 容器加一个 pip install”。这也是我建议开发者真正动手跑一遍的原因它不再是一个遥不可及的概念而是一个可以当天部署、当天验证的工程能力。3. 系统架构与整体设计为了让示例贴近真实项目我选的场景是“开发者文档语义搜索”把一堆技术文档分块、向量化、写入向量库然后通过查询接口做语义召回。这个场景可以平滑迁移到企业知识库、产品手册、内部 Wiki 和 RAG 应用。整个系统由四个部分组成内容处理层对文档做清洗、分块控制在模型可接受的输入长度。Embedding 服务用预训练模型把文本块转成向量。向量存储层用 Qdrant 保存向量和原始内容。查询服务层接收用户输入向量化后召回 Top N 结果。数据流向是离线阶段把文档切块后逐块生成向量写入集合在线阶段把用户查询转成向量在集合里做相似度搜索返回对应的原始文本和元数据。为什么选择 Qdrant 而不是直接全用 Elasticsearch主要考虑三点。第一Qdrant 的向量索引 HNSW 实现成熟单机百万级向量也能跑得动。第二Qdrant 支持在向量检索时附带结构化过滤比如按分类、标签过滤这对知识库搜索非常重要。第三它支持稀疏向量可以和 BM25 类似的算法做混合检索不需要额外再搭一套倒排系统。Embedding 模型选用开源中文模型 BAAI/bge-large-zh-v1.5。这个模型在中文语义匹配上表现稳定输出向量维度是 1024能兼顾效果和资源开销。如果使用场景以英文为主可以选择 bge-large-en如果追求多语言和更强语义能力可以尝试 bge-m3。模型的输出维度不同向量索引的配置也要跟着改这一点在接入时最容易出错。架构上还有一个安全边界需要提前强调Qdrant 默认没有鉴权向量库里保存的可能是企业敏感文档。生产环境一定要把 Qdrant 端口绑定到内网或者开启 API Key。不要在公网裸奔。4. 环境准备与前置条件开始之前需要确认本机环境满足以下条件操作系统Windows、macOS、Linux 均可示例以命令行方式执行。Python 3.9 或更高版本。Docker用于启动 Qdrant 服务。网络连接用于下载模型文件和 Python 依赖。需要说明的是下面的依赖和命令以当前主流版本为准。版本迭代较快如果你的环境有兼容问题优先检查官方文档而不是死套这里的版本号。4.1 启动 Qdrant 容器执行下面的命令把 Qdrant 跑在本地端口 6333 上docker run -d \ --name qdrant \ -p 6333:6333 \ -p 6334:6334 \ qdrant/qdrant6333 是 HTTP 端口6334 是 gRPC 端口。如果端口被占用可以先换一组映射docker run -d --name qdrant -p 6333:6333 qdrant/qdrant启动后打开http://localhost:6333/dashboard能看到 Qdrant 的 Web 控制台说明服务正常。4.2 安装 Python 依赖创建项目目录并安装依赖mkdir semantic-search-demo cd semantic-search-demo pip install qdrant-client sentence-transformers fastapi uvicorn如果你的机器上同时存在多个 Python 环境建议先创建虚拟环境python3 -m venv venv source venv/bin/activate安装完成之后可以用下面的命令确认关键包已经可用python -c from qdrant_client import QdrantClient; print(qdrant ok) python -c from sentence_transformers import SentenceTransformer; print(sentence ok)首次加载 Embedding 模型时程序会从模型仓库下载权重文件文件体积较大需要等待一段时间。如果下载时网络不稳定可以考虑使用国内模型平台 ModelScope 下载对应模型权重再拷贝到本地缓存目录。5. 完整示例构建一个语义文档搜索系统下面开始写完整代码。我会把示例拆成几个文件每个文件职责单一方便二次修改requirements.txt依赖清单models.pyEmbedding 模型封装init_collection.py初始化集合与写入文档数据search.py命令行检索验证app.pyFastAPI 查询服务5.1 依赖清单qdrant-client1.9 sentence-transformers2.5 fastapi0.100 uvicorn0.295.2 Embedding 模型封装新建models.py统一管理模型加载和向量生成# 文件路径models.py from sentence_transformers import SentenceTransformer _model None def get_model(): global _model if _model is None: # 中英文混合场景可改成 BAAI/bge-m3 _model SentenceTransformer(BAAI/bge-large-zh-v1.5) return _model def embed_texts(texts): model get_model() # normalize_embeddingsTrue 会做 L2 归一化 # 配合余弦距离使用时更稳定。 return model.encode(texts, normalize_embeddingsTrue).tolist() def embed_query(text): model get_model() # 部分 bge 模型建议给 query 增加指令前缀 # 具体以模型卡片说明为准。 return model.encode([text], normalize_embeddingsTrue).tolist()[0]这里把模型加载写成单例避免每次请求都重新加载。模型加载一次约消耗 1 到 2 GB 内存反复加载会明显拖慢服务启动甚至触发内存不足。5.3 初始化集合并写入数据新建init_collection.py清空集合、创建向量索引并写入示例文档# 文件路径init_collection.py from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct from models import embed_texts COLLECTION_NAME doc_search client QdrantClient(hostlocalhost, port6333) # 示例文档来源于日常技术问答场景 documents [ { id: 1, title: 接口调用超时排查, text: 当远程接口调用出现超时需要先确认超时时间配置、网络延迟以及服务端线程池是否被打满。 }, { id: 2, title: 数据库连接池满, text: 数据库连接池满通常表现为获取连接超时需要检查慢查询、连接泄漏和最大连接数配置。 }, { id: 3, title: 如何定位内存泄漏, text: Java 服务内存持续上涨时可以使用堆转储和 MAT 分析大对象引用定位到无法回收的根对象。 }, { id: 4, title: JVM 调优参数, text: 常见的 JVM 调优参数包括堆大小设置、垃圾回收器选择和日志输出配置需要结合业务场景验证。 }, { id: 5, title: 服务启动变慢, text: 服务启动变慢可能由类加载扫描、配置中心连接超时、大量 Bean 初始化等原因引起。 }, ] def main(): # 清空并重建集合保证演示环境干净 client.recreate_collection( collection_nameCOLLECTION_NAME, vectors_configVectorParams( size1024, distanceDistance.COSINE ), ) texts [doc[text] for doc in documents] vectors embed_texts(texts) points [ PointStruct( iddoc[id], vectorvectors[idx], payload{ title: doc[title], text: doc[text], }, ) for idx, doc in enumerate(documents) ] client.upsert( collection_nameCOLLECTION_NAME, pointspoints, ) print(f写入 {len(points)} 条文档到集合 {COLLECTION_NAME}) if __name__ __main__: main()这段代码里有一个容易出错的地方向量维度必须和模型输出维度一致。示例用的是bge-large-zh-v1.5维度是 1024。如果换成其他模型记得同时改VectorParams里的size否则写入会报维度不匹配错误。5.4 命令行检索验证新建search.py用一条查询测试语义召回# 文件路径search.py from qdrant_client import QdrantClient from models import embed_query COLLECTION_NAME doc_search client QdrantClient(hostlocalhost, port6333) def search(query, limit3): vector embed_query(query) results client.search( collection_nameCOLLECTION_NAME, query_vectorvector, limitlimit, ) return results if __name__ __main__: # 注意这句查询没有出现“数据库连接池”等关键词 query 服务突然拿不到数据库连接 print(f查询{query}) print( * 50) results search(query) for hit in results: title hit.payload.get(title) text hit.payload.get(text) score hit.score print(f相似度{score:.4f}) print(f标题{title}) print(f内容{text}) print(- * 50)这个查询故意不包含文档里的原词就是为了验证语义检索的效果。如果系统正常排在第一位的结果应该是“数据库连接池满”。5.5 封装 FastAPI 查询服务最后用一个 FastAPI 接口把检索能力暴露出来方便前端或者其他服务调用# 文件路径app.py from fastapi import FastAPI, Query from pydantic import BaseModel from typing import List, Optional from qdrant_client import QdrantClient from models import embed_query COLLECTION_NAME doc_search client QdrantClient(hostlocalhost, port6333) app FastAPI(titleSemantic Search API) class SearchResult(BaseModel): title: str text: str score: float class SearchResponse(BaseModel): query: str results: List[SearchResult] app.get(/search, response_modelSearchResponse) def search( q: str Query(..., description查询内容), limit: int Query(3, ge1, le10), ): vector embed_query(q) hits client.search( collection_nameCOLLECTION_NAME, query_vectorvector, limitlimit, ) results [ SearchResult( titlehit.payload.get(title, ), texthit.payload.get(text, ), scorehit.score, ) for hit in hits ] return SearchResponse(queryq, resultsresults)启动服务uvicorn app:app --host 0.0.0.0 --port 8000访问地址http://localhost:8000/search?q服务突然拿不到数据库连接返回的 JSON 结构里会包含查询词和按相似度排序的文档列表。6. 运行结果与效果验证先执行初始化脚本写入数据python init_collection.py预期输出写入 5 条文档到集合 doc_search然后执行检索python search.py如果一切正常输出应当类似查询服务突然拿不到数据库连接 相似度0.8743 标题数据库连接池满 内容数据库连接池满通常表现为获取连接超时需要检查慢查询、连接泄漏和最大连接数配置。 -------------------------------------------------- 相似度0.8120 标题接口调用超时排查 内容当远程接口调用出现超时需要先确认超时时间配置、网络延迟以及服务端线程池是否被打满。 -------------------------------------------------- 相似度0.7342 标题服务启动变慢 内容服务启动变慢可能由类加载扫描、配置中心连接超时、大量 Bean 初始化等原因引起。 --------------------------------------------------注意看查询里没有出现“数据库连接池”“连接池满”这些词但模型依然把相关文档排到了最前面。这就是语义召回和关键词匹配的最大区别。判断系统是否成功可以从三个角度验证结果是否“语义相关”排在第一位的文档读起来是不是最贴合查询意图。结果是否“稳定”同一个查询运行多次返回顺序不能忽上忽下。结果是否“有区分度”查询“接口超时”时应该优先返回网络相关文档而不是内存泄漏的文档。如果结果不理想先不要急着换模型。优先检查三件事文档分块粒度是否合适、查询文本是否明显偏离文档主题、集合里的数据量是否太少缺乏区分度。示例里只有 5 条数据效果只能说明“流程跑通了”要评估真实效果还需要更完整的文档集和评测集。7. 常见问题与排查思路问题现象可能原因排查方式解决方案连接 Qdrant 报 Connection refused容器未启动或端口映射错误执行docker ps查看容器状态检查端口占用重新启动容器或改用映射后的端口写入时报向量维度不匹配模型输出维度与集合配置不符打印模型向量长度对比VectorParams的size修改集合配置按实际维度重新创建首次加载模型非常慢正在下载模型权重观察日志确认下载进度等待完成如果网络受限改用 ModelScope 下载检索结果完全不相关模型选型不合适或文本太短检查文档分块长度尝试更换模型换用更大或领域更匹配的模型服务启动后请求超时模型在推理阶段计算较慢查看 CPU/内存使用率观察日志耗时使用 GPU或把模型部署为独立服务并缓存向量化结果补充一个常见但不是一眼能看到的坑如果使用recreate_collection重建集合会清空已有数据。在演示环境没问题但在生产环境千万不要用这个方法做日常更新。生产环境应该使用create_collection配合别名切换或者通过upsert增量写入避免误删数据。另外如果sentence-transformers在下载模型时频繁中断可以检查网络状态。模型下载完成后后续启动会走本地缓存不会再出现长时间等待。8. 最佳实践与工程建议到这里示例流程已经跑通了。但要把一个 demo 变成真正可用的搜索服务下面几点经验值得提前考虑。8.1 文档分块策略直接影响召回效果Embedding 模型一般有输入长度上限长文档必须切块。切块太短语义信息不完整切块太长向量会被平均化检索精度下降。实践里常见做法是按固定长度切分并设置一定比例的 overlap比如每块 500 到 800 字overlap 100 字左右。也可以按 Markdown 标题、段落、代码块先做结构化切分再对过长段落二次切分。8.2 不要只靠向量检索混合检索更可靠向量检索擅长语义匹配但精确词匹配能力不稳定。比如用户搜索“JVM 参数 -Xmx”纯向量检索很可能召回一些无关的 JVM 调优文章。更好的做法是同时跑 BM25 召回和向量召回然后用 RRF 算法融合两路结果。Qdrant 的新版本支持稀疏向量可以直接在一个库里做混合检索省去额外维护一套倒排索引的成本。8.3 向量数据库生产环境必须考虑安全和备份Qdrant 默认没有鉴权端口映射到公网等于把内部文档暴露出去。生产环境至少要做两件事一是绑定内网地址或者启用 API Key二是定期备份集合数据。向量本身也是业务数据备份和回滚策略不能省。8.4 模型服务化与批量向量化分离如果索引的文档量很大不要在应用进程里直接调模型来生成向量否则应用启动慢、内存占用高流量一大还会触发超时。建议把 Embedding 拆成独立服务离线批量生成向量后写入在线查询只走向量检索。模型推理过程也要做缓存同一个问题重复查询时可以直接复用向量。8.5 建立评测集用数据判断模型好不好“效果好”不能靠感觉。建议准备 50 到 100 条真实查询人工标注每条查询期望命中的文档 ID然后定期跑一遍统计召回率、MRR 等指标。换模型、调分块策略时用同一套评测集对比避免越调越差而不自知。8.6 向量检索之外的元数据过滤在企业知识库场景文档往往有分类、标签、作者、权限等属性。Qdrant 支持在检索时附加结构化过滤条件比如“只搜索 Java 分类下的文档”“只搜索最近一个月更新的文档”。这能把检索范围缩小提升精度。示例代码里没有加过滤条件但实际项目强烈建议保留一层业务过滤能力。9. 总结与下一步实践方向这篇文章讲清楚了一件事所谓新型搜索引擎核心变化不是交互方式而是把语义相似作为一级检索能力。传统倒排索引负责精确匹配向量检索负责语义召回两者在当前工程实践中是互补关系而不是替代关系。通过 Qdrant 加开源 Embedding 模型你已经能搭起一个完整的语义检索链路文档分块、向量化、写入向量库、查询召回、HTTP 接口暴露。这套链路可以直接用来做企业内部知识库搜索也可以作为 RAG 应用中的检索模块。下一步建议从两个方向继续深入。一个方向是混合检索把 BM25 和向量召回融合起来观察不同召回策略在不同查询类型上的表现。另一个方向是接入大模型生成答案由语义检索先找到依据文档再让大模型基于这些文档生成回答这就是当前知识库问答和 RAG 的标准架构。动手实践时有一个提醒先在你自己的业务文档上跑一遍用真实查询验证效果再考虑扩大数据规模。搜索引擎的难点从来不在“能搜到”而在“在你有几十万份文档、用户表达千奇百怪的情况下依然能搜到”。从今天的最小闭环开始你会比那些只看概念的人更早理解这个边界在哪里。
返回列表