RAG实战-从 NoSQL 到 Milvus 混合检索的架构演进

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 基础特性

  1. MongoDB 是文档型数据库,懒创建机制 :只获取库、集合句柄不会真正创建;第一次写入(insert)时,如果数据库、集合不存在,则自动创建;单纯 find 查询不会创建库和集合。集合不需要预先定义 Schema,同一个集合内不同文档可以拥有完全不一样的字段 key。
  2. 插入文档时,若不手动指定 _id,Mongo 自动生成 ObjectId 作为文档主键。
    • ObjectId:12 字节二进制,分为 4 段:4 字节时间戳 + 3 字节机器标识 + 2 字节进程 ID + 3 字节进程内自增计数器。
    • _id 集合唯一,自带默认唯一索引,一旦写入不能直接修改。
  3. 操作的不是 JSON 字符串:代码里写 Python 字典,底层磁盘/网络存储是 BSON(二进制 JSON) 。驱动自动完成 dict ↔ BSON 序列化/反序列化。
    • BSON 支持 ObjectId、日期、二进制等 JSON 不支持的类型;可以直接局部修改文档内部字段,不需要整体字符串替换。

2.2 MongoDB 索引原理

  1. 索引是集合级别,不是单条文档级别。给某个字段创建索引,是对整个集合生效。
  2. 默认单字段索引底层是 B 树 (不是哈希表;Mongo 有哈希索引,但极少使用)。索引存储:索引字段值 → 文档磁盘位置 RecordId,索引不保存完整文档。
  3. 文档行为:文档拥有索引字段,则该文档会被加入索引;文档没有该字段,则不会进入这个索引;字段值为 null 会存入索引。
  4. 查询流程:查询优化器优先尝试匹配索引,命中索引则走 IXSCAN 索引扫描,拿到指针回表读取 BSON 文档;没有合适索引则 COLLSCAN 全集合暴力扫描。
  5. 即使存在索引,也不一定会走索引:
    • 查询命中数据占集合比例很高;
    • 集合数据量很小,全表扫描开销更低;
    • 对索引字段使用函数运算,索引失效。
  6. 查询条件语法:不用 > < 符号,使用 $ 开头操作符
    • 等值:直接键值对;
    • $gt 大于、$lt 小于、$gte 大于等于、$lte 小于等于、$ne 不等于、$in 在数组内;
    • 同字典多个 key 默认 AND;OR 需要 $or 操作符。
  7. 查看索引是否生效:使用 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

  1. MongoDB:存放原始完整文档 ,一条记录对应一份原始文档,分配唯一 doc_id。
  2. 原始文档切分成多个 chunk:
    • Milvus:存储 chunk 稠密向量,元数据携带 doc_id、chunk_id,负责稠密语义召回。
    • ES:存储 chunk 文本,使用 ES 原生 BM25 关键词检索(底层是倒排索引,不属于稀疏向量 );每条 chunk 携带 doc_id 元数据。
  3. 检索流程:
    • 用户 Query,两路并行召回:Milvus 稠密召回、ES BM25 关键词召回;
    • 业务代码拿到两路结果,手动做 RRF(倒数排名融合)、去重;
    • 根据结果内 doc_id 去 MongoDB 查询原始文档。
  4. 缺点:需要维护两套检索引擎;双写 Milvus + ES,存在数据一致性问题;需要业务代码手动合并两路召回结果。
  5. ES 优势:强大分词、同义词、停用词、文本高亮、复杂 bool 查询、聚合、模糊检索。

4.2 新架构:高版本 Milvus(2.5+) + MongoDB,移除 ES

  1. Milvus 2.5+ 内置 BM25 能力:Milvus 内部将文本转为稀疏向量,在 Milvus 内部完成稠密向量 + BM25 稀疏向量混合检索,内置 RRF 融合,不需要外部代码合并结果。
  2. Milvus 单 Collection 同时存储:稠密向量、文本(用于 BM25)、稀疏向量、元数据 doc_id、chunk_id。
  3. 优势:减少 ES 组件,降低运维成本;只需要写入 Milvus 一次,无双写一致性问题;混合检索逻辑内置。
  4. 局限:Milvus 分词、文本高级能力弱于 ES;如果业务需要高亮、自定义词典、复杂文本过滤,依然建议保留 ES。

4.3 稠密向量 vs 稀疏向量

  1. 稠密向量 Dense Vector
    • 固定维度(768/1024 维),向量内绝大多数元素是非 0 浮点数;由 Embedding 模型生成。
    • 作用:表达文本语义信息;同义词、改写句子,稠密向量相似度高;不关心具体关键词。
  2. 稀疏向量 Sparse Vector
    • 维度极高(数万~几十万维),绝大多数元素等于 0;只存储非零项 {词ID:权重}。
    • 来源:BM25、TF-IDF、SPLADE 模型;每一维对应词表里的单词,数值代表词重要程度。
    • 作用:关键词字面匹配,词重合度越高分数越高。
  3. 区分:传统 ES 的 BM25 底层是倒排索引,不是稀疏向量;Milvus 为了统一向量检索框架,把 BM25 包装转换成稀疏向量做检索。
  4. 混合检索 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 版本)

  1. 原始文档入库 MongoDB,一条记录对应一份原始文档,分配全局唯一 doc_id;
  2. 原始文档进行文本切分,生成多个 chunk;
  3. 对每个 chunk:
    • 生成稠密向量;
    • 文本送入 Milvus BM25 函数生成稀疏向量;
    • 将 chunk_id、doc_id、chunk 文本、稠密向量、稀疏向量写入 Milvus;
  4. 用户输入 Query:
    • 分支 1:Query 通过 Embedding 模型生成稠密向量,走 Milvus 稠密向量语义召回;
    • 分支 2:原始 Query 文本直接送入 Milvus BM25 做关键词稀疏召回;
  5. Milvus 内部执行混合检索,RRF 融合两路结果,返回 topN 候选 chunk;
  6. 从命中 chunk 元数据取出 doc_id,请求 MongoDB,查询对应的完整原始文档;
  7. 将召回上下文交给 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 基于排名融合 无需调参,鲁棒 丢失分数绝对值信息
相关推荐
这个DBA有点耶1 小时前
数据融合平台的下一代形态:数据库内核自己就能融合,为什么还要ETL?
数据库·架构·aigc
91刘仁德1 小时前
RAG实战 - 向量数据库(Milvus)
数据库·milvus
孟健3 小时前
Stripe出海收款架构设计:水星银行与香港账户实测对比与资金流闭环
后端·架构
这个DBA有点耶3 小时前
连接池与MySQL交互实战:连接风暴、连接泄漏与连接状态异常排查
数据库·mysql·架构
IT大白鼠3 小时前
列族系列 · 第 05 篇——选型对比:列族与相邻方案
nosql·列族
艾莉丝努力练剑4 小时前
【AI大模型接入SDK】ChatSDK整体实现
网络·c++·人工智能·学习·架构
我是小白呀4 小时前
19-Temporal项目实战:将客户开通流程迁移到持久执行架构
java·开发语言·人工智能·架构·workflow
风123456789~4 小时前
【架构专栏】第19章 大数据架构设计 1/2
架构
许彰午5 小时前
53-审计三表
java·低代码·架构