尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用向量数据库打造语义搜索:从Milvus部署到日记检索实战
1. 先说清楚一次失败的日记搜索如何让我转投向量数据库1.1 传统方案在模糊的回忆面前的无力我之前一直用普通文本文件记日记记了大概三年攒下几十万字。平时写的时候很爽但真正要检索的时候就傻眼了。有一天我想找一段记忆——去年夏天某个周二的晚上我一个人在阳台上发呆楼下有人在弹吉他弹的是《晴天》当时觉得自己特别平静。我想把那段话翻出来看看但完全记不清那天的日记里写了什么关键词。我试着用吉他晴天阳台这几个词去搜结果搜出来一堆不相关的内容有一篇写的是去琴行看吉他有一篇写的是朋友推荐周杰伦的歌还有一篇压根只是提到了阳台两个字。问题出在哪传统的关键词搜索本质上是字符串匹配。它不管你这句话什么意思只看字面长什么样。体现在技术上就是两类方案数据库里的 LIKE 模糊查询以及 Elasticsearch 那种倒排索引。后者比前者快很多但核心逻辑是一样的——文档被拆成一个个词条查询时把输入也拆成词条然后做词条匹配。但人对语言的记忆根本不是按词条来的。我记得的是场景、情绪、画面、声音而不是当年的措辞。这就导致一个很尴尬的现实凡是认真写过的日记回头看反而找不回来。因为认真写的句子往往用了很多修辞真正跟记忆相关的信息被藏在了语义里而不是字面上。我当时想我要的是意思上相近的搜索而不是字面上相同的搜索。这个诉求用传统方案基本无解除非我手动给每篇日记打标签。打了三年日记的我告诉你这事儿坚持不了一个月。1.2 语义搜索的解题思路把文字变成高维坐标语义搜索能做到按意思找靠的是把文字转化成一串数字也就是向量。你可能听过 embedding、向量化、向量表示这些词听起来高大上其实原理并不复杂。拿我身边非技术的朋友举例我会说每个句子可以被理解成高维空间里的一个点意思相近的句子它们的点离得就近意思无关的句子点就离得远。搜索的时候把你输入的查询也变成一个新的点然后在所有已经存好的点里找出离它最近的几个。这就是语义搜索的底层逻辑。那这个点怎么来靠的是预训练语言模型比如 BERT 系的模型、OpenAI 的 embedding 接口、或者现在常见的 bge 系列模型。这些模型在大规模语料上学习过语言的规律能够把一个句子映射到一个固定维度的向量空间里。比如一个 768 维的向量你可以理解为它在 768 个维度上分别取值这些值共同编码了这句话的语义信息。这里有个容易误解的地方向量不是标签也不是某个词对应的编号而是整句话在高维空间里的坐标。正因如此它天然支持我记不清原话只记得大概意思这种场景。比如我写一个人在阳台上发呆楼下有人弹吉他跟原文那晚的风很轻楼下传来吉他声我靠着栏杆什么也没想在向量空间里就非常接近。因为模型学到过这些表达之间的语义关联。思路清楚了接下来就要选工具。我需要一个能存向量、能算相似度、还能快速返回结果的数据库。市面上的选择不少但我最终选了 Milvus。1.3 从 FAISS 到 Milvus为什么最终选了它在确定 Milvus 之前我其实先试过几个方案也和很多做 RAG检索增强生成的朋友聊过这里分享一下我的选型思路。最开始我试的是 FAISSMeta 开源的一个向量检索库。说实话如果你只是想在一万个向量里做最近邻搜索FAISS 完全够用性能特别好。但用着用着会发现一个问题FAISS 只是一个库不是一个数据库。它不管数据的增删改查不管你什么时候写入、什么时候删除、怎么按条件过滤这些都得自己在外面写逻辑。而我需要的场景是——每天写日记时往里面追加内容搜索时除了语义相关还要按日期范围过滤这种需求用 FAISS 就得自己造轮子。然后我试了 Chroma部署很轻接口也简单适合本地小项目。但它在数据量上来之后性能和稳定性会打折扣而且我后期想把搜索能力开放给一个 Web 小应用Chroma 在多并发下的表现不太让人放心。也考虑过 PostgreSQL pgvector。如果你现有的数据本来就在 PostgreSQL 里用 pgvector 确实是最省事的方案。但对于我这种从零开始搭一个语义搜索应用的需求它在索引类型、过滤能力、可观测性上还是偏弱一些。尤其是 pgvector 的索引参数调节空间不如专门的向量数据库灵活。最终选的 Milvus核心原因有这么几个维度FAISSChromapgvectorMilvus定位检索库轻量向量数据库关系型数据库的扩展专用向量数据库部署成本低很低中中Docker 一键数据量扩展需自己管理一般中等很强过滤能力无弱强强标量向量混合过滤索引类型丰富有限有限HNSW/IVF/SCANN 等运维友好度需要写很多胶水代码适合原型适合已有 PG 生态独立服务SDK 完整Milvus 把向量检索和数据库能力做在了一起。它有完整的数据模型Collection/Schema/Field、支持标量字段过滤、支持多种索引类型、有官方 Python SDK还支持用 Docker Compose 一键起单机版。对于我这种想专注在语义搜索业务本身不想重复造轮子的人来说是非常合适的选择。另外提一句如果你看过 Milvus 的架构就会发现它底层把存储和消息队列拆给了 MinIO 和 etcd单机模式下。这个设计在初期会觉得有点重但好处是后续想从单机平滑扩展到集群模式架构不用推倒重来。这也是我没选那些all-in-one轻量方案的原因之一。2. Docker部署Milvus单机版完整记录与踩坑补充2.1 部署前的准备确认虚拟化与Docker环境Milvus 单机版推荐用 Docker 部署所以第一步是准备 Docker 环境。如果你还没装这一节可以当个速查手册看。Windows 用户要特别注意一点Milvus 官方镜像基于 Linux在 Windows 上必须靠 Docker Desktop 里的 WSL 2 后端来跑。装 Docker Desktop 之前先确认你的机器已经开启虚拟化。最简单的验证方式是打开任务管理器切到性能标签页看右下角虚拟化是否是已启用。如果显示未启用需要进 BIOS 开启 Intel VT-x 或 AMD-V。我身边真有同事卡在这一步卡了一下午界面报错一直是Docker Desktop failed to start because virtualisation support wasnt detected——其实就是虚拟化没开。macOS 用户相对省心直接装 Docker DesktopApple Silicon 芯片记得下载对应 arm64 版本Intel 芯片就下 x86_64 版本别下反了。Linux 用户最省事一条命令装好 Docker Engine 和 Docker Compose 插件即可。这一步假设你已经装好了 Docker我直接讲怎么验证环境没问题。docker --version docker compose version两条命令都正常输出版本号说明基础环境没问题。我的环境是 Windows 11 WSL 2 后端 Docker Desktop 4.x后面所有的操作都基于这套环境Linux/macOS 上的命令完全一致。还有一个容易被忽略的点检查端口占用。Milvus 单机版默认会用到 19530Milvus 服务端口、2379etcd 端口、9000MinIO 端口。如果你本机已经有其他服务占了这些端口后面启动会报错。排查命令netstat -ano | findstr :19530 # Windows lsof -i :19530 # Linux/macOS有输出就代表端口被占用要么关掉占用程序要么后面改映射端口。2.2 用Docker Compose拉起Milvus三件套Milvus 单机版其实是由三个组件组成的milvus-standalone主服务、etcd元数据存储、minio数据持久化存储。Docker Compose 的方式就是一次性把三个服务都拉起来。官方提供一个脚本来生成配置文件但我的建议是直接手写 docker-compose.yml这样你能清楚地知道每个组件在干什么、怎么改。以下是我在项目中实际使用的配置version: 3.5 services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODErevision - ETCD_AUTO_COMPACTION_RETENTION1000 - ETCD_QUOTA_BACKEND_BYTES4294967296 - ETCD_SNAPSHOT_COUNT50000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: container_name: milvus-minio image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin ports: - 9000:9000 - 9001:9001 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data --console-address :9001 milvus: container_name: milvus-standalone image: milvusdb/milvus:v2.3.4 command: [milvus, run, standalone] security_opt: - seccomp:unconfined environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus ports: - 19530:19530 - 9091:9091 depends_on: - etcd - minio配置文件里重点看三处。第一etcd 的ETCD_QUOTA_BACKEND_BYTES我设成了 4GB这是元数据存储的内存上限默认值太小会导致写入量大的时候 etcd 报错。第二MinIO 的默认账号密码都是minioadmin如果只是本地用没问题如果要暴露到局域网务必改掉。第三Milvus 镜像我用了v2.3.4这是一个长期稳定版本不建议追最新版至少在正式使用前先等等社区反馈。配置写好后在同目录下执行docker compose up -d第一次启动会自动拉取三个镜像大概需要几分钟看网络情况。拉取完成后通过docker compose ps可以看到三个容器都是 Up 状态。2.3 启动验证与两个高频启动问题容器起来了不代表万事大吉必须验证 Milvus 真的能响应请求。我习惯用 Python 快速测一下pip install pymilvusfrom pymilvus import connections connections.connect(host127.0.0.1, port19530) print(连接成功)能输出连接成功说明 Milvus 服务已经在正常监听请求了。启动过程中有两个问题特别高频我在这里一并说清楚。第一个是 Windows 下 Docker Desktop 启动报错错误信息里带virtualisation support wasnt detected。原因我在前面提到过就是 BIOS 里的虚拟化没开或者 WSL 2 没有正确启用。处理方式分两步先去 BIOS 打开虚拟化然后在 PowerShell管理员里执行wsl --install装好 WSL 2 并设置默认版本为 2最后再启动 Docker Desktop。注意 Docker Desktop 的 Settings - General 里有个 Use the WSL 2 based engine 的选项必须勾选。第二个是端口冲突导致某个容器反复重启。尤其是 2379 端口很多本机装了其他分布式组件比如 Redis 集群、Consul的机器上容易撞车。解决方式很简单如果你不需要从宿主机直接访问 etcd 和 MinIO可以将这两个服务的端口映射去掉只要保证 Milvus 容器内部能通过etcd:2379、minio:9000访问即可。只保留19530:19530给客户端连接用这样可以最大程度避免冲突。还有一个容易被忽略的资源问题。Milvus 三件套跑起来之后空闲状态大约占用 1.5GB ~ 2GB 内存如果 Docker Desktop 的配置内存给得太小容器会在负载上来时被 OOM 杀掉。建议在 Docker Desktop 的 Settings - Resources 里把内存上限设成 4GB 以上否则后面做索引构建时很容易出幺蛾子。3. 数据入库设计Collection、选择Embedding模型、批量写入3.1 Schema设计给日记建立结构Milvus 里的核心概念跟传统数据库有一些对应关系先把名词理清Milvus 术语类比传统数据库说明Collection表同一类数据的集合Schema表结构定义有哪些字段Field列单个字段Entity行一条数据Partition分区按某个字段分区的物理存储我设计的日记 Collection 包含以下几个字段from pymilvus import CollectionSchema, FieldSchema, DataType fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idFalse), FieldSchema(namecontent, dtypeDataType.VARCHAR, max_length65535), FieldSchema(namedate, dtypeDataType.VARCHAR, max_length10), FieldSchema(nametag, dtypeDataType.VARCHAR, max_length50), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim768) ] schema CollectionSchema(fieldsfields, descriptionAI 日记语义搜索) from pymilvus import Collection collection Collection(namediary_entries, schemaschema)这里有几个设计上的考量说给你参考。id字段我用的 INT64 作为主键取自日记文本的哈希值。为什么不直接自增因为我的流程里存在重复导入同一篇日记的可能用内容哈希做幂等控制重复写入同一篇日记也不会产生重复数据非常实用。embedding字段的维度写成 768这是根据我选的 embedding 模型决定的后面会细说。维度必须和模型输出严格一致这一步错了后面写入必报错。content字段的max_length我给了 65535覆盖所有单日日记长度。VARCHAR 字段在 Milvus 里长度是有限制的如果你某天写了超长文本会在写入阶段报错所以留足冗余。我自己的日记最长的一天写了大概八千字65535 完全够用。还有一个需要提前考虑的问题Collection 创建之后 schema 是不能修改的。我踩过这个坑最开始没有加tag字段后面想给日记加上工作/生活/灵感的分类过滤结果发现加不了只能新建一个 Collection 重新导入。所以第一次设计时宁可多留一两个可能用到的字段也不要后面再回头改。3.2 Embedding模型选型本地模型与API方案的取舍embedding 是整个语义搜索的核心。如果模型选不好后面所有检索效果都会受影响。我重点比较过两条路线方案一本地模型sentence-transformers bgepip install sentence-transformersfrom sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-base-zh-v1.5) text 一个人在阳台上发呆楼下传来吉他声 embedding model.encode(text) print(len(embedding)) # 768本地模型的优势很明显数据不出本机隐私性拉满没有 API 调用费用请求延迟极低通常在几毫秒到几十毫秒。对于日记这种极其私密的数据我一开始就倾向本地跑。我用的模型是bge-base-zh-v1.5中文语义理解能力强输出 768 维向量在各种中文语义相似度评测里表现都很靠前。缺点是模型文件有好几百 MB首次加载有点慢但在可接受范围内。方案二云端 API如 OpenAI text-embedding-3-smallfrom openai import OpenAI client OpenAI(api_keyyour-key) resp client.embeddings.create( modeltext-embedding-3-small, # 输出 1536 维 inputtext ) embedding resp.data[0].embeddingAPI 方案的好处是效果稳定、不用管模型文件、对机器配置没有要求。但坏处也很明显私密数据要出本机每百万 token 的调用费用虽不高但日记如果长期积累还会有持续费用而且每次搜索都要等网络往返延迟比本地高一个数量级。还有一个非常关键的技术细节同一个项目里query 和入库文本必须用同一个模型做向量化。因为不同模型的向量空间是不同的A 模型把句子映射到空间里的坐标 (1, 2, 3)B 模型可能映射到 (4, 5, 6)两者之间的相似度计算完全没有意义。一旦中途换了模型之前已经入库的所有向量全部作废需要重新做一遍全量向量化。我最终选了本地模型bge-base-zh-v1.5综合考量了隐私、成本和效果三个维度。如果你手头的文本不是私密数据且对效果要求极高API 方案也完全可行。只要记得入库和查询用同一个模型这个铁律就行。3.3 批量写入的工程细节数据入库第一步是把你已有的日记从纯文本转换成集合里的一条条数据。我写的导入脚本核心逻辑大概长这样import hashlib import os from datetime import datetime def load_diary_entries(diary_dir): entries [] for filename in sorted(os.listdir(diary_dir)): if not filename.endswith(.md): continue # 文件名如 2024-06-15.md date_str filename.replace(.md, ) with open(os.path.join(diary_dir, filename), r, encodingutf-8) as f: content f.read().strip() if not content: continue text_id int(hashlib.md5((date_str content[:100]).encode(utf-8)).hexdigest()[:8], 16) entries.append({ id: text_id, content: content, date: date_str, tag: 未分类, embedding: model.encode(content[:512]).tolist() }) return entries注意我 encode 的时候截取了content[:512]。这是个大坑bge 系列模型的最长输入是 512 个 token不是字符直接整篇塞进去超长文本会被截断甚至报错。日记里长文本很常见我的处理方式是只对前 512 个 token 做向量化保证向量能代表整篇的大意。这种做法的代价是超长日记的尾部细节可能会丢失但考虑到搜索场景的核心诉求是召回相关日记不是精确到每一句话在绝大多数情况下够用了。如果某篇日记特别长而且细节很重要更好的做法是把日记切分成多个 chunk每个 chunk 单独向量化搜索时按某个 chunk 命中了来召回整篇日记。这样精度更高但工程复杂度也会成倍上升。我的个人建议是刚开始先跑通全流程用整篇向量化就够了。搜索效果不够再升级成 chunk 方案不要在一开始就过度设计。批量写入的时候要注意一个性能问题一条条插入效率非常低。正确做法是一次性组装多条数据用insert接口批量写入data [ [e[id] for e in entries], [e[content] for e in entries], [e[date] for e in entries], [e[tag] for e in entries], [e[embedding] for e in entries] ] collection.insert(data) collection.flush()实测下来一次性写入 500~1000 条数据的速度远比逐条插入快一个数量级以上。这里的flush()是把内存中的数据刷到存储端确保数据真正落盘。写入完成后可以验证一下条数print(collection.num_entities)看到数字和自己导入的日记数一致说明数据入库成功。4. 索引与搜索从向量到搜得准4.1 为什么需要向量索引从暴力搜索到ANN数据入库之后接下来就是搜索了。但你如果直接搜会发现一个很尴尬的问题当数据量大了之后搜索会越来越慢。原因在于最原始的向量搜索是暴力搜索——把查询向量和库里的每一个向量做相似度计算然后排序取前 K 个。这种做法的复杂度是 O(N)也就是说一万条数据要算一万次相似度一百万条就要算一百万次。我前面说了我的日记三年才攒下几十万字也就是差不多几百篇暴力搜索其实也能撑住。但如果你把每天的文章切分成 chunk数据量轻松到几千几万条暴力搜索的延迟就会明显变大。Milvus 之所以在数据量大之后仍然能保持毫秒级响应靠的是 ANNApproximate Nearest Neighbor近似最近邻索引。它的核心思路是不再挨个向量算相似度而是通过一种特殊的空间划分方式把哪些向量可能在目标附近这件事提前组织好搜索时只检查一小部分候选向量就能以非常高的概率找到最近邻。这是一种典型的用精度换速度的策略——搜索结果跟暴力搜索有微小差异但速度提升是数量级的。Milvus 支持多种索引类型包括 FLAT、IVF_FLAT、IVF_SQ8、HNSW、SCANN 等。其中 HNSW 结合了跳表和图结构的思想是目前在召回精度和查询性能之间平衡得最好的索引之一也是我实际项目里用的。4.2 HNSW索引的参数怎么调HNSW 的核心原理可以理解成把向量构建成一个多层的图上层图里的节点间距较大适合快速定位到目标区域底层图节点密集适合做精细搜索。你可以把它类比成一次找人的过程——先问这个人大概住在哪个城市再问哪个小区最后精确到门牌号。每层筛人效率自然高。HNSW 有两个关键参数在建索引时就要定好M每个节点的最大连接数默认 8。M越大图越稠密检索精度越高但内存占用和建索引时间也会增加。efConstruction构建图时动态候选列表长度默认 200。值越大建索引时考虑的范围越广索引质量越高但建索引越慢。还有一个运行参数ef真正的查询阶段使用代表搜索时动态候选列表长度。ef越大搜索越精确但速度越慢。创建索引的代码from pymilvus import Index index_params { index_type: HNSW, metric_type: COSINE, params: { M: 16, efConstruction: 200 } } collection.create_index(embedding, index_params)这里有三个值得单独解释的细节。第一metric_type我用的 COSINE余弦相似度。这是语义搜索场景最常用的度量方式衡量的是两个向量的方向一致性对向量模长不敏感。bge 模型官方推荐的就是余弦相似度。如果你用的模型或场景里有其他需求还可以选 IP内积或 L2欧氏距离但语义相似度默认选 COSINE 一般不会错。第二M16是我在实际测试后定下的值。M8时索引更小、建索引更快但召回率在数据量上来后下降明显M16是精度和资源占用都比较均衡的一个档位参考了 HNSW 论文里的建议和 Milvus 社区的实践。第三索引创建之后搜索参数和建索引参数要区分开。很多人第一次用会混淆以为搜索时也会用efConstruction其实查询时用的是efsearch_params { metric_type: COSINE, params: {ef: 64} }ef64是我试出来的经验值搜索精度已经足够好延迟也稳定。想追求更高精度就调大到 128响应时间会翻两三倍。项目上线后可以根据实际数据集做一次简单的A/B 调参不用一开始就追求最优。还有一点索引构建完成前数据是不能被搜索的。每次插入新数据后Milvus 默认只会做增量索引数据量小时感知不明显数据量大了一定要留意collection.load()这个动作。加载操作会把索引加载到内存中是搜索的前置条件。4.3 完整查询链路与搜索代码搜索流程是这样的把用户输入的自然语言查询文本执行和入库时完全相同的向量化操作得到一个查询向量然后调用search()接口让 Milvus 在索引里找出最接近的 K 条结果。query_text 一个人在阳台上发呆楼下有人弹吉他 query_embedding model.encode(query_text).tolist() results collection.search( data[query_embedding], anns_fieldembedding, paramsearch_params, limit5, output_fields[content, date, tag] ) for hits in results: for hit in hits: print(hit.id, hit.score) print(hit.entity.get(content)[:100]) print(hit.entity.get(date))这里有几个我实际踩过坑的地方逐个说。output_fields一定要带上。如果不指定搜索结果里只有向量的 id 和相似度分数没有原始文本内容。我一开始就漏了这个参数搜出来的结果只有一堆 id还得拿 id 去反查数据多绕了一圈。直接在 search 里带上output_fields是最省事的方式。limit指定返回 K 条结果。我在这里设的是 5更适合找出最相关的几篇日记这个场景。如果你做的是一个相似推荐功能limit可以加大到 10~20。score是当前相似度度量下的距离值。COSINE 度量下得分范围在 [-1, 1]越大表示越相似。我实测发现 bge 模型的余弦相似度在 0.5~0.7 之间就已经是比较相关的结果了低于 0.45 的基本上关联不大。这个阈值你可以按自己的数据实测调整不要在项目里写死一个没验证过的硬编码。上面这段代码跑通之后一次简单的语义搜日记就在技术上闭环了。但我很快就遇到了新的问题——纯语义搜索只考虑了内容像不像没有考虑时间对不对。4.4 组合过滤日期范围与标签维度日记搜索有个区别于通用向量搜索的特点用户通常会带时间记忆来搜。比如去年夏天写的那件事上个月关于工作的记录。如果只用向量相关度搜索Milvus 会把库里所有时间范围的内容都搜一遍效率低而且可能召回到年份完全不对的结果。Milvus 支持在search()时通过expr参数做标量字段过滤把向量检索和传统条件过滤组合在一起。这是它相比纯粹的向量检索库最大的优势之一。results collection.search( data[query_embedding], anns_fieldembedding, paramsearch_params, limit5, exprdate 2023-01-01 and date 2023-12-31, output_fields[content, date, tag] )expr的语法类似 SQL 的 WHERE 条件。这里的表达式会将搜索范围限制在 2023 年全年然后再在指定范围内做向量相似度检索。更灵活一点标签也可以参与过滤results collection.search( data[query_embedding], anns_fieldembedding, paramsearch_params, limit10, exprtag ! 工作 or date 2024-06-01, output_fields[content, date, tag] )组合过滤能让语义上相似和时间/标签上匹配两个维度协同作用大幅提升搜索结果的可用性。比如我搜那晚的吉他声限制date 2023-07-15得到的就是指定那天的日记而不是其他日期里也提到吉他的片段。这里要提醒一个容易犯的错expr 的过滤字段类型必须和 Schema 定义一致。比如我在 Schema 里把date定义为 VARCHARexpr 里就必须用引号包裹字符串值如果把date定义成 INT64expr 里引用时就不要带引号。类型不一致会导致表达式解析失败搜索结果直接报错。5. 把日记搜索做成可用的工具工程化收尾5.1 项目整体结构与模块说明到这一步核心的向量检索链路已经全部跑通了。但脚本能跑和工具能用之间还有一段距离。我花了一点时间把它整理成了一个像样的项目方便每天使用也方便后期扩展。最终的项目结构大概是这样diary-search/ ├── docker-compose.yml ├── requirements.txt ├── config.py # 全局配置连接地址、Collection 名称、模型名称 ├── embedder.py # Embedding 模型封装 ├── milvus_client.py # Milvus 连接与 Collection 管理 ├── data_loader.py # 日记导入脚本 ├── search_app.py # Web 检索界面 └── scripts/ ├── init_index.py # 初始化 Collection 和索引 └── rebuild.py # 全量重建索引职责划分得很清楚embedder.py负责文本向量化milvus_client.py负责数据库操作data_loader.py负责数据导入search_app.py是用户交互入口。每个文件尽量只做一件事以后任何一部分替换掉都不会影响其他模块。requirements.txt内容如下pymilvus2.3.6 sentence-transformers2.7.0 flask3.0.3三个依赖分别对应 Milvus 客户端、embedding 模型、Web 展示框架没有多余的东西。初次安装时建议先建一个独立的虚拟环境避免把系统 Python 环境搞乱。5.2 极简Web检索界面命令行方式直接调用搜索脚本也能用但每搜一次都要敲一行命令体验很差。我用 Flask 写了一个极简的 Web 页面在浏览器里输入一句话就能看到搜索结果效果足够直观代码量也不大。from flask import Flask, request, jsonify, render_template_string from embedder import get_embedding from milvus_client import search_diaries app Flask(__name__) PAGE !DOCTYPE html html head title日记语义搜索/title /head body h1日记语义搜索/h1 form methodpost input typetext namequery placeholder输入你想找的记忆... stylewidth:400px;padding:8px; button typesubmit搜索/button /form {% if results %} h2搜索结果/h2 {% for r in results %} div stylemargin-bottom:20px;padding:10px;border:1px solid #ddd; divb{{ r[date] }}/b | {{ r[tag] }} | 相似度: {{ r[score] }}/div pre stylewhite-space:pre-wrap;{{ r[content][:200] }}.../pre /div {% endfor %} {% endif %} /body /html app.route(/, methods[GET, POST]) def index(): results [] if request.method POST: query request.form.get(query, ).strip() if query: results search_diaries(query) return render_template_string(PAGE, resultsresults) if __name__ __main__: app.run(host0.0.0.0, port8000)运行python search_app.py后浏览器打开 http://localhost:8000输入一段模糊的记忆描述回车就能看到匹配的日记列表。这个界面的处理逻辑跟第五章展示的搜索流程完全一致只是做了 Web 化包装。这里有个小设计值得借鉴搜索结果里我把相似度分数也展示出来了。这样用一段时间之后你就知道这个模型在你自己的数据上分数大约是多少算相关、多少算无关后续设置过滤阈值就有了数据依据。5.3 数据维护日常增量导入与重建索引日记每天都要写所以导入工作不能做一次就结束。我写了一个增量导入脚本核心逻辑是扫描本地日记目录把尚未入库的日期对应的日记做向量化插入 Milvus。判断是否已入库最简单的方式是遍历现有 id 集合from pymilvus import connections, Collection connections.connect(host127.0.0.1, port19530) collection Collection(diary_entries) # 已存在的 id 列表 existing set() for batch in collection.query(exprid 0, output_fields[id], batch_size1000): existing.update(batch[id])然后只处理filename_md5不在existing里的日志入库逻辑复用data_loader.py里的函数。增量导入可以在每天写完后手动跑一下也可以挂到系统定时任务里自动执行。另一个我曾经踩过坑的地方是索引更新。Milvus 的新增数据会走增量索引但如果你修改了索引参数或者 embedding 模型升级了旧数据的向量全部要重新生成。这时需要全量重建。我写了一个rebuild.py做的事情是删除旧 Collection、重新创建新的 Collection、重新加载全量数据并建索引。过程很简单但因为要重新跑一遍所有文本的向量化耗时取决于文本量我的几百篇日记大概跑了一两分钟。日常运维里有几个数据管理的注意点备份Milvus 的数据持久化在 MinIO 和 etcd 里最简单粗暴的备份方式就是把 docker-compose.yml 里volumes挂载的目录整体打包。我用压缩工具定时打个 tar 包存到移动硬盘耗时几秒钟图个心安。删除单条数据collection.delete(exprid in [1,2,3])可以按条件删除。注意删除后如果还需要搜索记得collection.compact()触发一次数据压缩否则被删数据占用的空间不会立即回收。集合状态数据变更后建议用collection.flush()确认落盘再开始搜索测试否则可能出现刚插入的数据搜不到的错觉。5.4 资源占用与性能实测给一个真实的观测数据我的机器是 2021 款 MacBook ProM1 Pro16GB 内存Docker Desktop 分配了 6GB 内存给虚拟机。整套 Milvus 三件套在空闲时大约占用 2.2GB 内存三个容器加起来其中 MinIO 占大头。加载索引之后Milvus 主服务的内存在这个数量级的数据下几乎可以忽略。实际的搜索延迟在几百条日记、全部加载到内存的前提下单次向量化大约 10~20 毫秒单次向量搜索大约 3~8 毫秒整体端到端延迟在 30 毫秒以内。这个数字对交互式搜索场景来说毫无压力。数据量翻到几万条向量时相当于把每篇日记切成多个 chunk实测搜索延迟依然稳定在 20 毫秒以内这主要归功于 HNSW 索引的召回逻辑——它不需要扫描全量向量搜索开销是按候选集大小而非全库大小来算的。我自己的感受是Milvus 单机版在千万级向量以下性能都非常够用完全不用考虑集群部署。从痛苦翻日记到秒级语义搜索整个过程比我预想的要平滑。Milvus 把向量数据库最复杂的那部分分布式、索引算法、数据持久化都封装好了留给开发者的核心工作其实是两件事选一个好的 embedding 模型、设计一个合理的数据模型。这两件事做对了整个搜索系统的效果就有七成把握。如果你也想把手里的私人笔记、读书摘录、灵感碎片甚至聊天记录做成语义搜索完全可以照着这套方案复刻一套。Docker Compose 拉起 Milvus、Python 脚本做向量化入库、Flask 起一个检索页面三个步骤下来你就能拥有一个真正懂意思的私人检索库。
RELATED

相关推荐

STM32CubeMX2与Keil Studio构建体系深度解析

STM32CubeMX2与Keil Studio构建体系深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/18 8:19:40
机器学习在乳腺癌诊断中的应用与优化

机器学习在乳腺癌诊断中的应用与优化

1. 项目背景与核心价值乳腺癌是全球女性最常见的恶性肿瘤之一,早期准确诊断对治疗方案选择和预后改善至关重要。传统病理诊断依赖医生经验,存在主观性强、效率低下的痛点。这个项目通过机器学习方法构建自动化分类模型,将乳腺肿瘤影像特征转化…

📅 2026/9/18 8:14:39
2026年AI编程工具选型指南:33款工具分类盘点与实战策略

2026年AI编程工具选型指南:33款工具分类盘点与实战策略

打开技术社区随便一刷,十个问题里至少有五个在问:2026年了,AI编程工具到底怎么选?这个问题我在不同场合被问了不下几十次,每次有人找我要推荐,我第一反应都不是先报工具名单,而是反问他一句&…

📅 2026/9/18 8:14:39
MORE NEWS

更多资讯

📰

Unity FUI架构实战:用登录页解耦UGUI与权限边界

1. 项目概述:为什么一个登录页能讲清楚FUI架构的生死线FUI——这个在Unity中被越来越多中大型项目团队挂在嘴边的词,不是新出的UI框架,也不是某个开源库的名字,而是“Functional UI”的缩写,一种以函数式思维重构UI层的…

📰

COMSOL激光烧蚀多物理场仿真技术与应用

1. 项目背景与核心价值激光烧蚀技术在精密加工、微纳制造等领域有着广泛应用,但传统实验方法往往难以直观观察烧蚀过程中的温度场变化和材料响应。COMSOL Multiphysics作为一款强大的多物理场仿真软件,能够完美模拟激光与材料相互作用时产生的复杂物理现…

📰

Modbus协议流量取证实战:从抓包到证据链构建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Agent跨会话记忆架构:三层分层与可演化设计

1. 这不是“记住聊天”那么简单:跨会话记忆的本质是构建用户认知模型你有没有试过和某个AI助手聊了三次,每次它都得从头问“你是谁?”“上次我们聊了什么?”——哪怕你刚在十分钟前告诉过它你的职业、偏好甚至讨厌的食物。这不是A…

📰

轻越野市场分析指南:从行业报告到可复用数据底表

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

梯度下降与梯度上升:从数学推导到代码实战与调参避坑

“梯度下降”这四个字,大概是每个机器学习入门者绕不开的第一道坎。我当年第一次翻开周志华那本《机器学习》,看到损失函数、偏导数、迭代更新那一堆符号时,脑子里只有一个念头:这玩意儿到底在干嘛?后来真正把代码跑通…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬