RAG实战-从 NoSQL 到 Milvus 混合检索的架构演进
-
- [一、NoSQL 数据库概述](#一、NoSQL 数据库概述)
-
- [1.1 什么是 NoSQL](#1.1 什么是 NoSQL)
- [1.2 NoSQL (以 MongoDB 为例) vs MySQL](#1.2 NoSQL (以 MongoDB 为例) vs MySQL)
- [二、MongoDB 核心概念与操作](#二、MongoDB 核心概念与操作)
-
- [2.1 MongoDB 基础特性](#2.1 MongoDB 基础特性)
- [2.2 MongoDB 索引原理](#2.2 MongoDB 索引原理)
- [2.3 MongoDB CRUD 操作与代码注释知识点](#2.3 MongoDB CRUD 操作与代码注释知识点)
- [2.4 MongoDB 创建数据库与集合](#2.4 MongoDB 创建数据库与集合)
- [2.5 MongoDB 索引创建](#2.5 MongoDB 索引创建)
- [2.6 MongoDB 存储简历案例](#2.6 MongoDB 存储简历案例)
- [三、Elasticsearch 核心概念与操作](#三、Elasticsearch 核心概念与操作)
-
- [3.1 Elasticsearch 是什么](#3.1 Elasticsearch 是什么)
- [3.2 核心概念解析](#3.2 核心概念解析)
- [3.3 Elasticsearch 基础操作](#3.3 Elasticsearch 基础操作)
- [四、RAG 架构演进](#四、RAG 架构演进)
-
- [4.1 旧架构:Milvus + ES + MongoDB](#4.1 旧架构:Milvus + ES + MongoDB)
- [4.2 新架构:高版本 Milvus(2.5+) + MongoDB,移除 ES](#4.2 新架构:高版本 Milvus(2.5+) + MongoDB,移除 ES)
- [4.3 稠密向量 vs 稀疏向量](#4.3 稠密向量 vs 稀疏向量)
- [4.4 Milvus 混合检索伪代码(BM25+稠密向量)](#4.4 Milvus 混合检索伪代码(BM25+稠密向量))
- [4.5 Milvus Collection Schema 定义(支持 BM25)](#4.5 Milvus Collection Schema 定义(支持 BM25))
- [4.6 整套 RAG 完整业务链路(Milvus+Mongo 版本)](#4.6 整套 RAG 完整业务链路(Milvus+Mongo 版本))
- 五、不同代码方法对比总结
-
- [5.1 MongoDB 新旧 API 对比](#5.1 MongoDB 新旧 API 对比)
- [5.2 Elasticsearch 关键函数与参数](#5.2 Elasticsearch 关键函数与参数)
- [5.3 混合检索融合器对比](#5.3 混合检索融合器对比)
一、NoSQL 数据库概述
1.1 什么是 NoSQL
NoSQL 全称是 "Not Only SQL",泛指所有非关系型数据库。这类数据库的诞生,是为了解决传统关系型数据库(如 MySQL)在面对超大规模数据、高并发请求和非结构化数据时的挑战。
NoSQL 数据库的核心特点是其灵活的数据模型。四种主流 NoSQL 数据库:
键值数据库 (Key-Value Store)
- 代表:Redis
- 特点:数据以最简单的"键-值"对形式存储,查询速度极快。
- 存储示例:就像一个巨大的 Python 字典,非常适合做缓存。
python
# 存储用户会话信息
"session:user123": "{'username': 'Alice', 'login_time': '2023-10-27T10:00:00Z'}",
"session:user456": "{'username': 'Bob', 'login_time': '2023-10-27T10:05:12Z'}"
文档数据库 (Document Store)
- 代表:MongoDB
- 特点:以类似 JSON 的文档存储数据。Schema(结构)是灵活的。
- 存储示例:同一个"集合"中,不同文档的结构可以不同。
json
// 用户1的文档
{
"_id": "user001",
"name": "张伟",
"age": 30,
"tags": ["developer", "python"]
}
// 用户2的文档,比用户1多了一个 "location" 字段
{
"_id": "user002",
"name": "李娜",
"age": 28,
"tags": ["designer", "ui"],
"location": "上海"
}
这就是"灵活 Schema":可以在不改动整个"表"结构的情况下,为新文档增加新字段。
列式数据库 (Wide-Column Store)
- 代表:Cassandra, HBase
- 特点:数据按"列族"存储,非常适合海量数据写入和分析。
- 存储示例:每一行可以有不同的"列"。
json
// RowKey: user123_timestamp1
{
"event_details": {"page": "/home", "duration": "15s"},
"user_info": {"device": "mobile", "browser": "Chrome"}
}
// RowKey: user456_timestamp2
{
"event_details": {"page": "/pricing", "button_clicked": "buy_now"},
"user_info": {"device": "desktop"} // 注意:这里没有 browser 字段
}
图数据库 (Graph Database)
- 代表:Neo4j
- 特点:使用节点和边来存储数据,专门处理实体间的复杂关系。
- 存储示例:描述一个简单的社交网络。
json
// 节点 (Nodes)
Node(Person, {name: "Alice"})
Node(Person, {name: "Bob"})
Node(City, {name: "Beijing"})
// 关系 (Edges)
(Alice) -[:FRIENDS_WITH]-> (Bob)
(Alice) -[:LIVES_IN]-> (Beijing)
1.2 NoSQL (以 MongoDB 为例) vs MySQL
MongoDB 与 MySQL 的核心对比:
- 数据模型:MongoDB 使用灵活的 BSON 文档;MySQL 使用固定的行和列。
- Schema:MongoDB 无需预定义,同一集合内文档字段可不同;MySQL 需要预定义表结构。
- 扩展性:MongoDB 支持水平扩展(分片);MySQL 通常垂直扩展,水平扩展较复杂。
- 事务:MongoDB 支持多文档事务(4.0+);MySQL 支持完整 ACID 事务。
- 查询语言:MongoDB 使用 JSON 风格的查询;MySQL 使用 SQL。
二、MongoDB 核心概念与操作
2.1 MongoDB 基础特性
- MongoDB 是文档型数据库,懒创建机制 :只获取库、集合句柄不会真正创建;第一次写入(insert)时,如果数据库、集合不存在,则自动创建;单纯 find 查询不会创建库和集合。集合不需要预先定义 Schema,同一个集合内不同文档可以拥有完全不一样的字段 key。
- 插入文档时,若不手动指定
_id,Mongo 自动生成ObjectId作为文档主键。- ObjectId:12 字节二进制,分为 4 段:4 字节时间戳 + 3 字节机器标识 + 2 字节进程 ID + 3 字节进程内自增计数器。
_id集合唯一,自带默认唯一索引,一旦写入不能直接修改。
- 操作的不是 JSON 字符串:代码里写 Python 字典,底层磁盘/网络存储是 BSON(二进制 JSON) 。驱动自动完成 dict ↔ BSON 序列化/反序列化。
- BSON 支持 ObjectId、日期、二进制等 JSON 不支持的类型;可以直接局部修改文档内部字段,不需要整体字符串替换。
2.2 MongoDB 索引原理
- 索引是集合级别,不是单条文档级别。给某个字段创建索引,是对整个集合生效。
- 默认单字段索引底层是 B 树 (不是哈希表;Mongo 有哈希索引,但极少使用)。索引存储:
索引字段值 → 文档磁盘位置 RecordId,索引不保存完整文档。 - 文档行为:文档拥有索引字段,则该文档会被加入索引;文档没有该字段,则不会进入这个索引;字段值为 null 会存入索引。
- 查询流程:查询优化器优先尝试匹配索引,命中索引则走
IXSCAN索引扫描,拿到指针回表读取 BSON 文档;没有合适索引则 COLLSCAN 全集合暴力扫描。 - 即使存在索引,也不一定会走索引:
- 查询命中数据占集合比例很高;
- 集合数据量很小,全表扫描开销更低;
- 对索引字段使用函数运算,索引失效。
- 查询条件语法:不用
> <符号,使用$开头操作符- 等值:直接键值对;
$gt大于、$lt小于、$gte大于等于、$lte小于等于、$ne不等于、$in在数组内;- 同字典多个 key 默认 AND;OR 需要
$or操作符。
- 查看索引是否生效:使用
explain(),IXSCAN代表走索引,COLLSCAN全表扫描。
2.3 MongoDB CRUD 操作与代码注释知识点
代码示例:
python
from pymongo import MongoClient
from bson import ObjectId
# 1. 连接 MongoDB
client = MongoClient("mongodb://127.0.0.1:27017")
# 2. 选择数据库和集合(不存在则写入时自动创建)
db = client["rag_demo_db"] # 数据库名
coll = db["document_chunk"] # 集合名(相当于表)
# 可选:创建索引,用于根据 chunk_id 快速查询
coll.create_index("chunk_id", unique=True)
print("已创建 chunk_id 唯一索引\n")
# ---------------------- 增(Insert) ----------------------
# 插入单条文档
doc1 = {
"chunk_id": 1001,
"parent_id": 5001,
"text": "MongoDB 是文档型数据库,不需要预定义表结构",
"meta": {"source": "demo.pdf", "page": 1}
}
res_insert_one = coll.insert_one(doc1)
print(f"插入单条,自动生成 _id: {res_insert_one.inserted_id}")
# 插入多条文档
doc_list = [
{
"chunk_id": 1002,
"parent_id": 5001,
"text": "索引是B树结构,加速字段查询",
"meta": {"source": "demo.pdf", "page": 2}
},
{
"chunk_id": 1003,
"parent_id": 5002,
"text": "RAG场景:Milvus召回chunk_id,再查Mongo拿原文",
"meta": {"source": "rag_doc.pdf", "page": 5}
}
]
res_insert_many = coll.insert_many(doc_list)
print(f"批量插入,生成的 _id列表: {res_insert_many.inserted_ids}\n")
# ---------------------- 查(Find) ----------------------
# 1. 查询单条:根据 chunk_id 精确查找(走索引)
find_one_result = coll.find_one({"chunk_id": 1002})
print("find_one 结果:")
print(find_one_result)
# 2. 条件查询多条,parent_id=5001
print("\nparent_id=5001 的所有文档:")
for item in coll.find({"parent_id": 5001}):
print(item["chunk_id"], item["text"])
# 3. 只返回指定字段,不返回 _id
print("\n只查询text字段:")
for item in coll.find({"chunk_id": {"$gt": 1001}}, {"_id": 0, "chunk_id":1, "text":1}):
print(item)
# ---------------------- 改(Update) ----------------------
# update_one:匹配到第一条,局部修改 $set,不会覆盖整个文档
update_result = coll.update_one(
{"chunk_id":1001},
{"$set": {"text": "【更新后】MongoDB是文档型数据库", "meta.page": 99}}
)
print(f"\nupdate_one:匹配到{update_result.matched_count}条,修改{update_result.modified_count}条")
# update_many:批量更新,匹配所有符合条件文档
update_many_result = coll.update_many(
{"parent_id":5001},
{"$set": {"tag": "important"}}
)
print(f"update_many:匹配{update_many_result.matched_count}条,修改{update_many_result.modified_count}条\n")
# ---------------------- 删(Delete) ----------------------
# delete_one 删除匹配的第一条
del_one_res = coll.delete_one({"chunk_id":1003})
print(f"delete_one 删除数量:{del_one_res.deleted_count}")
# delete_many 删除所有匹配条件文档
# del_many_res = coll.delete_many({"parent_id":5001})
# print(f"delete_many 删除数量:{del_many_res.deleted_count}")
# ---------------------- 查看执行计划,判断是否命中索引 ----------------------
explain = coll.find({"chunk_id":1001}).explain()
print("\n查询执行计划:")
print(f"执行阶段: {explain['executionStats']['executionStages']['stage']}")
# IXSCAN = 索引扫描(走索引),COLLSCAN = 全集合暴力扫描
# 关闭连接
client.close()
代码注释提取为知识点:
知识点 1:集合不存在时自动创建
注释原文:
# 2. 选择数据库和集合(不存在则写入时自动创建)
- 位置:
db = client["rag_demo_db"]和coll = db["document_chunk"] - 讲解:MongoDB 的数据库和集合不需要提前创建。通过
client[DATABASE_NAME]选择数据库和db[COLLECTION_NAME]选择集合时,MongoDB 只是准备好操作对象,实际的数据库和集合会在第一次 insert 数据时自动创建。单纯 find 查询不会创建库和集合。 - 关联考点:MongoDB 懒创建机制。
知识点 2:ObjectId 自动生成
注释原文:
# 插入单条文档后的res_insert_one.inserted_id
- 位置:
res_insert_one = coll.insert_one(doc1) - 讲解:插入文档时,若不手动指定
_id,Mongo 自动生成ObjectId作为文档主键。ObjectId是 12 字节二进制,分为 4 段:4 字节时间戳 + 3 字节机器标识 + 2 字节进程 ID + 3 字节进程内自增计数器。_id集合唯一,自带默认唯一索引,一旦写入不能直接修改。 - 关联考点:ObjectId 结构、主键唯一性。
知识点 3:insert_many 批量插入
注释原文:
# 插入多条文档
- 位置:
res_insert_many = coll.insert_many(doc_list) - 讲解:
insert_many()接受一个文档列表,返回InsertManyResult,包含inserted_ids列表。批量插入比逐条插入效率高。 - 关联考点:批量操作。
知识点 4:find_one 与 find
注释原文:
# 1. 查询单条:根据 chunk_id 精确查找(走索引)
- 位置:
find_one_result = coll.find_one({"chunk_id": 1002}) - 讲解:
find_one()返回匹配的第一条文档,若没有则返回None。find()返回游标,可迭代获取所有匹配文档。查询条件使用字典,支持$gt、$lt等操作符。 - 关联考点:查询操作符。
知识点 5:投影(只返回指定字段)
注释原文:
# 3. 只返回指定字段,不返回 _id
- 位置:
coll.find({"chunk_id": {"$gt": 1001}}, {"_id": 0, "chunk_id":1, "text":1}) - 讲解:
find()的第二个参数是投影字典,1表示包含该字段,0表示排除。_id默认包含,要排除需显式设置"_id": 0。 - 关联考点:投影。
知识点 6:update_one 与 $set
注释原文:
# update_one:匹配到第一条,局部修改 $set,不会覆盖整个文档
- 位置:
coll.update_one({"chunk_id":1001}, {"$set": {"text": "...", "meta.page": 99}}) - 讲解:
update_one()只更新匹配的第一条文档。$set操作符用于设置字段值,若字段不存在则创建,存在则修改。使用点号meta.page可以修改嵌套字段。不会覆盖整个文档。 - 关联考点:更新操作符。
知识点 7:update_many 批量更新
注释原文:
# update_many:批量更新,匹配所有符合条件文档
- 位置:
coll.update_many({"parent_id":5001}, {"$set": {"tag": "important"}}) - 讲解:
update_many()更新所有匹配的文档。返回UpdateResult,包含matched_count和modified_count。 - 关联考点:批量更新。
知识点 8:delete_one 与 delete_many
注释原文:
# delete_one 删除匹配的第一条和# delete_many 删除所有匹配条件文档
- 位置:
coll.delete_one({"chunk_id":1003})和coll.delete_many({"parent_id":5001}) - 讲解:
delete_one()删除匹配的第一条,delete_many()删除所有匹配。返回DeleteResult,包含deleted_count。 - 关联考点:删除操作。
知识点 9:explain 查看执行计划
注释原文:
# IXSCAN = 索引扫描(走索引),COLLSCAN = 全集合暴力扫描
- 位置:
explain = coll.find({"chunk_id":1001}).explain() - 讲解:
explain()返回查询执行计划,executionStats.executionStages.stage字段显示执行阶段。IXSCAN表示走索引,COLLSCAN表示全集合扫描。用于判断索引是否生效。 - 关联考点:查询优化、索引命中。
2.4 MongoDB 创建数据库与集合
代码示例:
python
from pymongo import MongoClient
MONGO_URI = "mongodb://admin:123456@82.156.249.211:27017/"
DATABASE_NAME = "my_test_db1"
COLLECTION_NAME = "user_profiles1"
# 连接 MongoDB
client = MongoClient(MONGO_URI)
db = client[DATABASE_NAME]
collection = db[COLLECTION_NAME]
# 此时数据库和集合尚未创建
print("选择数据库和集合,但尚未插入数据...")
dbs = client.list_database_names()
print(f"当前数据库列表: {dbs}")
# 插入一个文档
collection.insert_one({"name": "测试用户"})
print("插入数据后,数据库和集合自动创建")
# 检查数据库是否存在
dbs = client.list_database_names()
print(f"当前数据库列表: {dbs}")
client.close()
关键函数:
MongoClient(uri, serverSelectionTimeoutMS=5000):连接 MongoDB,serverSelectionTimeoutMS设置服务器选择超时时间。client.list_database_names():列出所有数据库名称。db[COLLECTION_NAME]:选择集合。collection.insert_one(document):插入单个文档。collection.drop():删除集合。collection.delete_many({}):清空集合(删除所有文档)。collection.count_documents({}):统计文档数量。collection.index_information():查看索引信息。
2.5 MongoDB 索引创建
代码示例:
python
# 创建索引
collection.create_index([("name", 1)], unique=True)
print("已为 name 字段创建唯一索引")
# 查看索引信息
indexes = collection.index_information()
for index_name, index_info in indexes.items():
print(f"索引名称: {index_name}, 详细信息: {index_info}")
关键函数与参数:
collection.create_index(keys, unique=False):keys:列表,如[("name", 1)]。1表示升序,-1表示降序。unique:是否唯一索引。
- 索引作用:加速查询、支持高效排序、支持范围查询、确保唯一性、优化复杂查询、支持特殊查询(如多键索引支持数组字段)。
- 代价:占用额外存储空间;写操作需要更新索引,增加写开销;过多或不合理索引可能导致性能下降。
2.6 MongoDB 存储简历案例
代码示例:
python
from pymongo import MongoClient
from pypdf import PdfReader
import hashlib
from datetime import datetime
MONGO_URI = "mongodb://admin:123456@82.156.249.211:27017/"
DATABASE_NAME = "my_test_db"
COLLECTION_NAME = "resumes"
client = MongoClient(MONGO_URI)
db = client[DATABASE_NAME]
collection = db[COLLECTION_NAME]
collection.drop()
def read_pdf(file_path: str) -> str:
reader = PdfReader(file_path)
text = ""
for page in reader.pages:
text += page.extract_text() or ""
return text
def store_resume(doc_content: str, file_path: str):
doc_hash = hashlib.md5(doc_content.encode()).hexdigest()
timestamp = datetime.now().isoformat()
if collection.find_one({"doc_hash": doc_hash}):
print(f"简历已存在,hash: {doc_hash}, 文件: {file_path}")
return False
collection.insert_one({
"doc_hash": doc_hash,
"content": doc_content,
"file_path": file_path,
"timestamp": timestamp,
})
print(f"简历存储成功,hash: {doc_hash}")
return True
def get_full_resume(doc_hash: str) -> str:
result = collection.find_one({"doc_hash": doc_hash})
if result:
return result["content"]
return ""
if __name__ == '__main__':
pdf_path = r"D:\...\test_resume.pdf"
doc_content = read_pdf(pdf_path)
success = store_resume(doc_content, pdf_path)
result_one = collection.find_one({"doc_hash": "16509539c25a0e6fb1dd1f4f59df637c"})
print(result_one['content'])
知识点:
- 使用
hashlib.md5生成文档哈希,用于去重。 - 存储完整文档内容、文件路径、时间戳。
- 通过
doc_hash查询完整简历。
三、Elasticsearch 核心概念与操作
3.1 Elasticsearch 是什么
Elasticsearch 是一个基于 Apache Lucene 构建的、开源的、分布式的搜索和分析引擎。你可以把它想象成一个拥有超能力的、为速度而生的数据库。
Lucene 是 Apache 软件基金会 Jakarta 项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
它的核心功能不是像传统数据库那样仅仅存储数据,而是让海量数据的全文搜索、结构化搜索和分析变得极其快速和简单。
如何定位 Elasticsearch?
- 文档 (Document):基本数据单元是 JSON 文档,所以它是一个文档数据库。
- 搜索 (Search Engine):首要设计目标和核心能力是搜索。
- 数据库 (Database):具备数据存储、管理和查询的能力,是一个功能完备的数据库。
- 官方最常用的定位是"分布式搜索和分析引擎",强调两大核心应用场景(搜索、分析)及其分布式架构的特点。
典型应用场景:
- 网站全文搜索:电商网站的商品搜索、博客的文章搜索、招聘网站的职位搜索。
- 日志和事件数据分析 (ELK Stack):收集、分析和可视化来自服务器、应用等的日志数据,进行故障排查和监控。
- 应用程序性能监控 (APM):监控应用的性能指标、追踪请求链路。
- RAG 混合检索:在 AI 应用中,作为关键词检索引擎,与向量数据库互补,实现更鲁棒的召回。
3.2 核心概念解析
Elasticsearch 的核心概念与 MySQL 对比:
- 索引 (Index):类似 MySQL 的数据库。
- 类型 (Type):7.0 后已废弃。
- 文档 (Document):类似 MySQL 的行。
- 字段 (Field):类似 MySQL 的列。
- 映射 (Mapping):类似 MySQL 的表结构。
注意:Elasticsearch 没有数据库的概念。
核心能力:倒排索引 (Inverted Index)
Elasticsearch 成为"搜索引擎"的秘密武器,在于它处理文本的方式与 MySQL 完全不同。
查询过程示例:
假设 product 索引中有两个文档:
- 文档1:
{ "description": "Elasticsearch is fast." } - 文档2:
{ "description": "Search is fun and fast." }
用户 query:"fast search"
阶段一:索引阶段 (写入数据时发生)
① 分析 (Analysis):每个文档的 description 字段内容被"分析器"处理。
- "Elasticsearch is fast." -> 拆解为词元
[elasticsearch, is, fast] - "Search is fun and fast." -> 拆解为词元
[search, is, fun, and, fast]
② 生成倒排索引:系统创建一个类似字典的结构,映射了从"词元"到"包含该词元的文档列表"。
- "elasticsearch" ->
[文档1] - "is" ->
[文档1, 文档2] - "fast" ->
[文档1, 文档2] - "search" ->
[文档2] - "fun" ->
[文档2] - "and" ->
[文档2]
阶段二:搜索阶段 (用户查询时发生)
① 分析查询字符串:用户的查询 "fast search" 会经过与索引时完全相同的分析器处理,得到查询词元:[fast, search]。
② 查找匹配文档:利用倒排索引,为每个查询词元拉取一个文档列表。
- 查找 fast -> 得到
[文档1, 文档2] - 查找 search -> 得到
[文档2] - 合并去重,候选集:
[文档1, 文档2]
③ 评分与排序 (BM25 算法):为候选集中的每个文档计算相关度分数。
- 对于文档2:同时包含 fast 和 search,BM25 计算两个词得分贡献,加总,得到较高总分。
- 对于文档1:只包含 fast,总分会相对较低。
最终按分数降序排序,返回[文档2, 文档1]。
总结: Elasticsearch 的魔法 = 高效的倒排索引 + 智能的 BM25 评分算法。首先通过倒排索引快速筛选出候选集,然后通过 BM25 精准地计算出每个结果与查询的"相关程度",最终呈现出最符合用户意图的排序结果。
3.3 Elasticsearch 基础操作
安装:
bash
pip install elasticsearch
创建索引:
python
from elasticsearch import Elasticsearch
es_client = Elasticsearch('http://8.1:9200')
es_client.ping()
print("Elasticsearch 连接成功!")
INDEX_NAME = "product_index"
if es_client.indices.exists(index=INDEX_NAME):
es_client.indices.delete(index=INDEX_NAME)
print(f"已删除旧索引 '{INDEX_NAME}'")
mapping = {
"mappings": {
"properties": {
"name": {"type": "text"}, # 商品名称,支持全文搜索
"brand": {"type": "keyword"}, # 品牌,精确匹配
"price": {"type": "float"}, # 价格,浮点数
"tags": {"type": "keyword"} # 标签数组,精确匹配
}
}
}
es_client.indices.create(index=INDEX_NAME, body=mapping)
print(f"已创建索引 '{INDEX_NAME}' 并定义映射")
index_info = es_client.indices.get(index=INDEX_NAME)
print(f"索引 '{INDEX_NAME}' 创建成功,映射信息:")
print(index_info)
es_client.close()
知识点:
mappings的结构是半固定的:它是一个 JSON 对象,用于定义索引的字段映射和元数据。properties是mappings对象中的一个标准键(固定键),其值是一个对象,映射字段名称到其属性(如类型type、分析器等)。这不是完全固定的,因为可以添加新字段、定义动态模板或其他参数(如_source、dynamic),但核心结构(如mappings下嵌套properties)是标准的推荐格式。如果不遵守,可能会导致映射无效或默认行为。新字段可以动态添加,但现有字段的类型通常不可更改(需重新索引)。- 字段类型:
text支持全文搜索,keyword用于精确匹配和聚合,float浮点数。 es_client.indices.create(index, body):创建索引并定义映射。es_client.indices.exists(index):检查索引是否存在。es_client.indices.delete(index):删除索引。es_client.indices.get(index):获取索引信息。
增删改查操作:
python
from elasticsearch import Elasticsearch
es_client = Elasticsearch('http://localhost:9200')
es_client.ping()
print("Elasticsearch 连接成功!")
INDEX_NAME = "product_index"
# 创建文档
doc1 = {"name": "笔记本电脑", "brand": "Apple", "price": 12000, "tags": ["electronics", "M3 chip"]}
doc2 = {"name": "机械键盘", "brand": "Cherry", "price": 800, "tags": ["electronics", "gaming"]}
es_client.index(index=INDEX_NAME, id="1", document=doc1)
es_client.index(index=INDEX_NAME, document=doc2)
print("已创建两个产品文档。")
es_client.indices.refresh(index=INDEX_NAME)
print("索引已刷新")
# 读取与搜索
retrieved_doc = es_client.get(index=INDEX_NAME, id="1")
print(retrieved_doc["_source"])
query = {"match": {"name": "电脑"}}
search_results = es_client.search(index=INDEX_NAME, query=query)
for hit in search_results["hits"]["hits"]:
print(hit["_source"])
# 更新
update_body = {"doc": {"price": 11500}}
es_client.update(index=INDEX_NAME, id="1", doc=update_body)
es_client.indices.refresh(index=INDEX_NAME)
updated_doc = es_client.get(index=INDEX_NAME, id="1")
print("更新后的文档:", updated_doc["_source"])
# 删除
es_client.delete(index=INDEX_NAME, id="1")
es_client.indices.refresh(index=INDEX_NAME)
doc_exists = es_client.exists(index=INDEX_NAME, id="1")
print(f"ID '1' 的文档是否存在? {'是' if doc_exists else '否'}")
es_client.close()
关键函数与参数:
es_client.index(index, id=None, document):创建或替换文档。若指定id则使用该 ID,否则自动生成。es_client.get(index, id):根据 ID 获取文档。es_client.search(index, query):搜索文档。query是查询 DSL。es_client.update(index, id, doc):更新文档。doc参数使用{"doc": {...}}格式。es_client.delete(index, id):删除文档。es_client.exists(index, id):检查文档是否存在。es_client.indices.refresh(index):刷新索引,使文档立即可搜索。
四、RAG 架构演进
4.1 旧架构:Milvus + ES + MongoDB
- MongoDB:存放原始完整文档 ,一条记录对应一份原始文档,分配唯一
doc_id。 - 原始文档切分成多个 chunk:
- Milvus:存储 chunk 稠密向量,元数据携带
doc_id、chunk_id,负责稠密语义召回。 - ES:存储 chunk 文本,使用 ES 原生 BM25 关键词检索(底层是倒排索引,不属于稀疏向量 );每条 chunk 携带
doc_id元数据。
- Milvus:存储 chunk 稠密向量,元数据携带
- 检索流程:
- 用户 Query,两路并行召回:Milvus 稠密召回、ES BM25 关键词召回;
- 业务代码拿到两路结果,手动做 RRF(倒数排名融合)、去重;
- 根据结果内
doc_id去 MongoDB 查询原始文档。
- 缺点:需要维护两套检索引擎;双写 Milvus + ES,存在数据一致性问题;需要业务代码手动合并两路召回结果。
- ES 优势:强大分词、同义词、停用词、文本高亮、复杂 bool 查询、聚合、模糊检索。
4.2 新架构:高版本 Milvus(2.5+) + MongoDB,移除 ES
- Milvus 2.5+ 内置 BM25 能力:Milvus 内部将文本转为稀疏向量,在 Milvus 内部完成稠密向量 + BM25 稀疏向量混合检索,内置 RRF 融合,不需要外部代码合并结果。
- Milvus 单 Collection 同时存储:稠密向量、文本(用于 BM25)、稀疏向量、元数据
doc_id、chunk_id。 - 优势:减少 ES 组件,降低运维成本;只需要写入 Milvus 一次,无双写一致性问题;混合检索逻辑内置。
- 局限:Milvus 分词、文本高级能力弱于 ES;如果业务需要高亮、自定义词典、复杂文本过滤,依然建议保留 ES。
4.3 稠密向量 vs 稀疏向量
- 稠密向量 Dense Vector
- 固定维度(768/1024 维),向量内绝大多数元素是非 0 浮点数;由 Embedding 模型生成。
- 作用:表达文本语义信息;同义词、改写句子,稠密向量相似度高;不关心具体关键词。
- 稀疏向量 Sparse Vector
- 维度极高(数万~几十万维),绝大多数元素等于 0;只存储非零项
{词ID:权重}。 - 来源:BM25、TF-IDF、SPLADE 模型;每一维对应词表里的单词,数值代表词重要程度。
- 作用:关键词字面匹配,词重合度越高分数越高。
- 维度极高(数万~几十万维),绝大多数元素等于 0;只存储非零项
- 区分:传统 ES 的 BM25 底层是倒排索引,不是稀疏向量;Milvus 为了统一向量检索框架,把 BM25 包装转换成稀疏向量做检索。
- 混合检索 Hybrid Search:同时使用稠密向量做语义召回、稀疏向量做关键词召回,两路结果 RRF 融合,兼顾语义理解和关键词精准匹配。
4.4 Milvus 混合检索伪代码(BM25+稠密向量)
python
from pymilvus import connections, Collection, RRFRanker, AnnSearchRequest
connections.connect(alias="default", host="127.0.0.1", port="19530")
coll = Collection("rag_chunk_collection")
coll.load()
query_embedding = [0.1,0.2,0.3,...]
request_dense = AnnSearchRequest(
data=query_embedding,
anns_field="dense_vector",
param={"metric_type":"COSINE"},
limit=10
)
query_text = "用户的检索问题"
request_bm25 = AnnSearchRequest(
data=[query_text],
anns_field="sparse_vector",
param={"metric_type":"BM25"},
limit=10
)
res = coll.hybrid_search(
reqs=[request_dense, request_bm25],
ranker=RRFRanker(),
limit=5
)
for hit_group in res:
for hit in hit_group:
doc_id = hit.entity.get("doc_id")
chunk_id = hit.entity.get("chunk_id")
print(f"命中chunk_id:{chunk_id}, 所属原始文档doc_id:{doc_id}, 分数:{hit.score}")
4.5 Milvus Collection Schema 定义(支持 BM25)
python
from pymilvus import FieldSchema, CollectionSchema, DataType
fields = [
FieldSchema(name="chunk_id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="doc_id", dtype=DataType.INT64),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535),
FieldSchema(name="dense_vector", dtype=DataType.FLOAT_VECTOR, dim=768),
FieldSchema(name="sparse_vector", dtype=DataType.SPARSE_FLOAT_VECTOR),
]
schema = CollectionSchema(fields, description="rag chunk 集合,支持BM25混合检索")
4.6 整套 RAG 完整业务链路(Milvus+Mongo 版本)
- 原始文档入库 MongoDB,一条记录对应一份原始文档,分配全局唯一
doc_id; - 原始文档进行文本切分,生成多个 chunk;
- 对每个 chunk:
- 生成稠密向量;
- 文本送入 Milvus BM25 函数生成稀疏向量;
- 将
chunk_id、doc_id、chunk 文本、稠密向量、稀疏向量写入 Milvus;
- 用户输入 Query:
- 分支 1:Query 通过 Embedding 模型生成稠密向量,走 Milvus 稠密向量语义召回;
- 分支 2:原始 Query 文本直接送入 Milvus BM25 做关键词稀疏召回;
- Milvus 内部执行混合检索,RRF 融合两路结果,返回 topN 候选 chunk;
- 从命中 chunk 元数据取出
doc_id,请求 MongoDB,查询对应的完整原始文档; - 将召回上下文交给 LLM,生成最终回答。
五、不同代码方法对比总结
5.1 MongoDB 新旧 API 对比
| 操作 | 旧版 / 基础用法 | 新版 / 推荐用法 |
|---|---|---|
| 连接 | MongoClient(uri) |
MongoClient(uri, serverSelectionTimeoutMS=5000) |
| 选择库 | client[DATABASE_NAME] |
同左 |
| 选择集合 | db[COLLECTION_NAME] |
同左 |
| 插入单条 | collection.insert_one(doc) |
同左 |
| 插入多条 | collection.insert_many(docs) |
同左 |
| 查询单条 | collection.find_one(filter) |
同左 |
| 查询多条 | collection.find(filter) |
同左 |
| 更新单条 | collection.update_one(filter, update) |
同左 |
| 更新多条 | collection.update_many(filter, update) |
同左 |
| 删除单条 | collection.delete_one(filter) |
同左 |
| 删除多条 | collection.delete_many(filter) |
同左 |
| 创建索引 | collection.create_index(keys, unique) |
同左 |
| 查看索引 | collection.index_information() |
同左 |
| 执行计划 | collection.find(...).explain() |
同左 |
5.2 Elasticsearch 关键函数与参数
| 函数 | 参数 | 作用 |
|---|---|---|
Elasticsearch(hosts) |
hosts:连接地址 |
创建客户端 |
es_client.ping() |
无 | 检查连接 |
es_client.indices.exists(index) |
index:索引名 |
检查索引是否存在 |
es_client.indices.create(index, body) |
index:索引名,body:映射定义 |
创建索引 |
es_client.indices.delete(index) |
index:索引名 |
删除索引 |
es_client.indices.get(index) |
index:索引名 |
获取索引信息 |
es_client.indices.refresh(index) |
index:索引名 |
刷新索引 |
es_client.index(index, id, document) |
index:索引名,id:文档 ID,document:文档内容 |
创建/替换文档 |
es_client.get(index, id) |
index:索引名,id:文档 ID |
获取文档 |
es_client.search(index, query) |
index:索引名,query:查询 DSL |
搜索文档 |
es_client.update(index, id, doc) |
index:索引名,id:文档 ID,doc:更新内容 |
更新文档 |
es_client.delete(index, id) |
index:索引名,id:文档 ID |
删除文档 |
es_client.exists(index, id) |
index:索引名,id:文档 ID |
检查文档是否存在 |
5.3 混合检索融合器对比
| 融合器 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| WeightedRanker | 加权求和 | 可手动调权重,灵活 | 需要调参,对分数范围敏感 |
| RRFRanker | 基于排名融合 | 无需调参,鲁棒 | 丢失分数绝对值信息 |