第三章 索引构建(2-all -in-rag学习)

第三章 索引构建

第四节 Milvus实践

一、简介

Milvus 是一个开源的、专为大规模向量相似性搜索和分析而设计的向量数据库。它诞生于 Zilliz 公司,并已成为 LF AI & Data 基金会的顶级项目,在AI领域拥有广泛的应用。

与 FAISS、ChromaDB 等轻量级本地存储方案不同,Milvus 从设计之初就瞄准了生产环境。其采用云原生架构,具备高可用、高性能、易扩展的特性,能够处理十亿、百亿甚至更大规模的向量数据。

官网地址: https://milvus.io/

GitHub: https://github.com/milvus-io/milvus

FAISS和Milvus的区别:FAISS是个算法库,代码里import就能用,索引存在本地文件;Milvus是个数据库服务,得先docker启动,然后用客户端连它。一个像工具函数,一个像独立服务。

二、安装

1.环境准备

安装 Docker 与 Docker Compose : 确保系统中已安装并正在运行 Docker 和 Docker Compose。如果你对 Docker 不熟悉,可以参考这篇详细的教程:Docker 万字教程:从入门到掌握

2.下载并启动

在你选定的工作目录下,打开终端(Terminal)或命令行工具(PowerShell),执行以下步骤

第一步:下载配置文件

使用以下命令下载官方的 docker-compose.yml 文件。这个文件定义了 Milvus Standalone 及其运行所需的两个核心依赖服务:etcd 用于存储元数据,MinIO 用于对象存储(更多架构细节请参考官方文档)。

bash 复制代码
# macOS / Linux (使用 wget)
wget https://github.com/milvus-io/milvus/releases/download/v2.5.14/milvus-standalone-docker-compose.yml -O docker-compose.ymlCopy to clipboardErrorCopied
# Windows (使用 PowerShell)
Invoke-WebRequest -Uri "https://github.com/milvus-io/milvus/releases/download/v2.5.14/milvus-standalone-docker-compose.yml" -OutFile "docker-compose.yml"Copy to clipboardErrorCopied

第二步:启动 Milvus 服务

docker-compose.yml 文件所在的目录中,运行以下命令以后台模式启动 Milvus:

bash 复制代码
docker compose up -dCopy to clipboardErrorCopied

Docker 将会自动拉取所需的镜像并启动三个容器:milvus-standalone, milvus-minio, 和 milvus-etcd。这个过程可能需要几分钟,具体取决于你的网络状况。

3. 验证安装

可以通过以下方式验证 Milvus 是否成功启动:

查看 Docker 容器 : 打开 Docker Desktop 的仪表盘 (Windows/macOS) 或在终端运行 docker ps 命令 (Linux),确认三个 Milvus 相关容器(milvus-standalone, milvus-minio, milvus-etcd)都处于 runningup 状态。

检查服务端口 : Milvus Standalone 默认通过 19530 端口提供服务,这是后续代码连接时需要用到的地址。

4.常用管理命令
  • 停止服务:

    bash 复制代码
    docker compose downCopy to clipboardErrorCopied

    此命令会停止并移除容器,但保留存储的数据卷。

  • 彻底清理 (停止并删除数据): 如果想彻底删除所有数据(包括向量、元数据等),可以执行以下命令:

    bash 复制代码
    docker compose down -v

三、核心组件

3.1 Collection (集合)

可以用一个图书馆的比喻来理解 Collection:

  • Collection (集合) : 相当于一个图书馆,是所有数据的顶层容器。一个 Collection 可以包含多个 Partition,每个 Partition 可以包含多个 Entity。
  • Partition (分区) : 相当于图书馆里的不同区域(如"小说区"、"科技区"),将数据物理隔离,让检索更高效。
  • Schema (模式) : 相当于图书馆的图书卡片规则,定义了每本书(数据)必须登记哪些信息(字段)。
  • Entity (实体) : 相当于一本具体的书,是数据本身。
  • Alias (别名) : 相当于一个动态的推荐书单(如"本周精选"),它可以指向某个具体的 Collection,方便应用层调用,实现数据更新时的无缝切换。

Collection 是 Milvus 中最基本的数据组织单位,类似于关系型数据库中的一张表 (Table)。是我们存储、管理和查询向量及相关元数据的容器。所有的数据操作,如插入、删除、查询等,都是围绕 Collection 展开的。

一个 Collection 由其 Schema 定义,并包含以下重要的子概念和特性:

3.1.1 Schema

在创建 Collection 之前,必须先定义它的 SchemaSchema 规定了 Collection 的数据结构 ,定义了其中包含的所有字段 (Field) 及其属性。一个设计良好的 Schema 是能够保证数据一致性并提升查询性能。

Schema 通常包含以下几类字段:

  • 主键字段 (Primary Key Field): 每个 Collection 必须有且仅有一个主键字段,用于唯一标识每一条数据(实体)。它的值必须是唯一的,通常是整数或字符串类型。
  • 向量字段 (Vector Field): 用于存储核心的向量数据。一个 Collection 可以有一个或多个向量字段,以满足多模态等复杂场景的需求。
  • 标量字段 (Scalar Field): 用于存储除向量之外的元数据,如字符串、数字、布尔值、JSON 等。这些字段可以用于过滤查询,实现更精确的检索。

上图以一篇新闻文章为例,展示了一个典型的多模态、混合向量 Schema 设计。它将一篇文章拆解为:唯一的 Article (ID)、文本元数据(如 TitleAuthor Info)、图像信息(Image URL),并为图像和摘要内容分别生成了密集向量(Image Embedding, Summary Embedding)和稀疏向量(Summary Sparse Embedding)。

3.1.2 Partition (分区)

Partition 是 Collection 内部的一个逻辑划分。每个 Collection 在创建时都会有一个名为 _default 的默认分区。我们可以根据业务需求创建更多的分区,将数据按特定规则(如类别、日期等)存入不同分区。

为什么使用分区?

  • 提升查询性能: 在查询时,可以指定只在一个或几个分区内进行搜索,从而大幅减少需要扫描的数据量,显著提升检索速度。
  • 数据管理: 便于对部分数据进行批量操作,如加载/卸载特定分区到内存,或者删除整个分区的数据。

一个 Collection 最多可以有 1024 个分区。合理利用分区是 Milvus 性能优化的重要手段之一。

3.1.3 Alias (别名)

Alias (别名) 是为 Collection 提供的一个"昵称"。通过为一个 Collection 设置别名,我们可以在应用程序中使用这个别名来执行所有操作,而不是直接使用真实的 Collection 名称。

为什么使用别名?

  • 安全地更新数据

    :想象一下,你需要对一个在线服务的 Collection 进行大规模的数据更新或重建索引。直接在原 Collection 上操作风险很高。正确的做法是:

    1. 创建一个新的 Collection (collection_v2) 并导入、索引好所有新数据。
    2. 将指向旧 Collection (collection_v1) 的别名(例如 my_app_collection)原子性地切换到新 Collection (collection_v2) 上。
  • 代码解耦:整个切换过程对上层应用完全透明,无需修改任何代码或重启服务,实现了数据的平滑无缝升级。

Collection这套概念的对应关系:Collection=表=整座图书馆,Partition=分区=图书馆的区域,Schema=表结构=图书卡片规则,Entity=一行数据=一本书。跟关系型数据库很像,就是多了个向量字段。

3.2 索引 (Index)

如果说 Collection 是 Milvus 的骨架,那么索引 (Index) 就是其加速检索的神经系统。从宏观上看,索引本身就是一种为了加速查询而设计的复杂数据结构。对向量数据创建索引后,Milvus 可以极大地提升向量相似性搜索的速度,代价是会占用额外的存储和内存资源。

上图清晰地展示了 Milvus 向量索引的内部组件及其工作流程:

  • 数据结构:这是索引的骨架,定义了向量的组织方式(如 HNSW 中的图结构)。
  • 量化(可选):数据压缩技术,通过降低向量精度来减少内存占用和加速计算。
  • 结果精炼(可选):在找到初步候选集后,进行更精确的计算以优化最终结果。

Milvus 支持对标量字段和向量字段分别创建索引。

  • 标量字段索引 :主要用于加速元数据过滤,常用的有 INVERTEDBITMAP 等。通常使用推荐的索引类型即可。
  • 向量字段索引:这是 Milvus 的核心。选择合适的向量索引是在查询性能、召回率和内存占用之间做出权衡的艺术。
3.2.1 主要向量索引类型

Milvus 提供了多种向量索引算法,以适应不同的应用场景。以下是几种最核心的类型:

  • FLAT (精确查找)
    • 原理:暴力搜索(Brute-force Search)。它会计算查询向量与集合中所有向量之间的实际距离,返回最精确的结果。
    • 优点:100% 的召回率,结果最准确。
    • 缺点:速度慢,内存占用大,不适合海量数据。
    • 适用场景:对精度要求极高,且数据规模较小(百万级以内)的场景。
  • IVF 系列 (倒排文件索引)
    • 原理 :类似于书籍的目录。它首先通过聚类将所有向量分成多个"桶"(nlist),查询时,先找到最相似的几个"桶",然后只在这几个桶内进行精确搜索。IVF_FLATIVF_SQ8IVF_PQ 是其不同变体,主要区别在于是否对桶内向量进行了压缩(量化)。
    • 优点:通过缩小搜索范围,极大地提升了检索速度,是性能和效果之间很好的平衡。
    • 缺点:召回率不是100%,因为相关向量可能被分到了未被搜索的桶中。
    • 适用场景:通用场景,尤其适合需要高吞吐量的大规模数据集。
  • HNSW (基于图的索引)
    • 原理:构建一个多层的邻近图。查询时从最上层的稀疏图开始,快速定位到目标区域,然后在下层的密集图中进行精确搜索。
    • 优点:检索速度极快,召回率高,尤其擅长处理高维数据和低延迟查询。
    • 缺点:内存占用非常大,构建索引的时间也较长。
    • 适用场景:对查询延迟有严格要求(如实时推荐、在线搜索)的场景。
  • DiskANN (基于磁盘的索引)
    • 原理:一种为在 SSD 等高速磁盘上运行而优化的图索引。
    • 优点:支持远超内存容量的海量数据集(十亿级甚至更多),同时保持较低的查询延迟。
    • 缺点:相比纯内存索引,延迟稍高。
    • 适用场景:数据规模巨大,无法全部加载到内存的场景。
3.2.2 如何选择索引?

选择索引没有唯一的"最佳答案",需要根据业务场景在数据规模、内存限制、查询性能和召回率之间进行权衡。

场景 推荐索引 备注
数据可完全载入内存,追求低延迟 HNSW 内存占用较大,但查询性能和召回率都很优秀。
数据可完全载入内存,追求高吞吐 IVF_FLAT / IVF_SQ8 性能和资源消耗的平衡之选。
数据量巨大,无法载入内存 DiskANN 在 SSD 上性能优异,专为海量数据设计。
追求 100% 准确率,数据量不大 FLAT 暴力搜索,确保结果最精确。

在实际应用中,通常需要通过测试来找到最适合自己数据和查询模式的索引类型及其参数。

索引选择自己总结的:FLAT全量暴力搜,小数据精度要求高用;IVF先聚类再搜,平衡之选;HNSW基于图快但吃内存,低延迟场景首选;DiskANN放磁盘,数据大到内存装不下时用。

3.3 检索
3.3.1 基础向量检索 (ANN Search)

拥有了数据容器 (Collection) 和检索引擎 (Index) 后,最后一步就是从海量数据中高效地检索信息。这是 Milvus 的核心功能之一,近似最近邻 (Approximate Nearest Neighbor, ANN) 检索。与需要计算全部数据的暴力检索(Brute-force Search)不同,ANN 检索利用预先构建好的索引,能够极速地从海量数据中找到与查询向量最相似的 Top-K 个结果。这是一种在速度和精度之间取得极致平衡的策略。

  • 主要参数

    :

    • anns_field: 指定要在哪个向量字段上进行检索。
    • data: 传入一个或多个查询向量。
    • limit (或 top_k): 指定需要返回的最相似结果的数量。
    • search_params: 指定检索时使用的参数,例如距离计算方式 (metric_type) 和索引相关的查询参数。
3.3.2 增强检索

在基础的 ANN 检索之上,Milvus 提供了多种增强检索功能,以满足更复杂的业务需求。

过滤检索 (Filtered Search)

在实际应用中,我们很少只进行单纯的向量检索。更常见的需求是"在满足特定条件的向量中,查找最相似的结果",这就是过滤检索。它将向量相似性检索标量字段过滤结合在一起。

  • 工作原理 :先根据提供的过滤表达式 (filter) 筛选出符合条件的实体,然后仅在这个子集内执行 ANN 检索。这极大地提高了查询的精准度。

  • 应用示例

    • 电商:"检索与这件红色连衣裙最相似的商品,但只看价格低于500元且有库存的。"
    • 知识库:"查找与'人工智能'相关的文档,但只从'技术'分类下、且发布于2023年之后的文章中寻找。"

范围检索 (Range Search)

有时我们关心的不是最相似的 Top-K 个结果,而是"所有与查询向量的相似度在特定范围内的结果"。

  • 工作原理:范围检索允许定义一个距离(或相似度)的阈值范围。Milvus 会返回所有与查询向量的距离落在这个范围内的实体。

  • 应用示例

    • 人脸识别:"查找所有与目标人脸相似度超过 0.9 的人脸",用于身份验证。
    • 异常检测:"查找所有与正常样本向量距离过大的数据点",用于发现异常。

多向量混合检索 (Hybrid Search)

这是 Milvus 提供的一种极其强大的高级检索模式,它允许在一个请求中同时检索多个向量字段,并将结果智能地融合在一起。

  • 工作原理
    1. 并行检索:应用针对不同的向量字段(如一个用于文本语义的密集向量,一个用于关键词匹配的稀疏向量,一个用于图像内容的多模态向量)分别发起 ANN 检索请求。
    2. 结果融合 (Rerank) :Milvus 使用一个重排策略(Reranker)将来自不同检索流的结果合并成一个统一的、更高质量的排序列表。常用的策略有 RRFRanker(平衡各方结果)和 WeightedRanker(可为特定字段结果加权)。
  • 应用示例
    • 多模态商品检索 :用户输入文本"安静舒适的白色耳机",系统可以同时检索商品的文本描述向量图片内容向量,返回最匹配的商品。
    • 增强型 RAG : 结合密集向量 (捕捉语义)和稀疏向量(精确匹配关键词),实现比单一向量更精准的文档检索效果。

分组检索 (Grouping Search)

分组检索解决了一个常见的痛点:检索结果多样性不足。想象一下,你检索"机器学习",返回的前10篇文章都来自同一本教科书不同章节。这显然不是理想的结果。

  • 工作原理 :分组检索允许指定一个字段(如 document_id)对结果进行分组。Milvus 会在检索后,确保返回的结果中每个组(每个 document_id)只出现一次(或指定的次数),且返回的是该组内与查询最相似的那个实体。

  • 应用示例

    • 视频检索:检索"可爱的猫咪",确保返回的视频来自不同的博主。
    • 文档检索:检索"数据库索引",确保返回的结果来自不同的书籍或来源。

通过这些灵活的检索功能组合,开发者可以构建出满足各种复杂业务需求的向量检索应用。

四种检索方式:

  • 基础ANN:直接向量搜相似
  • 过滤检索:搜之前先用标量字段筛一遍(比如只看2023年的)
  • 范围检索:不限定Top-K,返回相似度在某范围内的所有结果
  • 混合检索:同时搜多个向量字段,结果合并重排
  • 分组检索:按字段分组,保证结果不扎堆

四、milvus多模态实践

在本节中,我们将通过一个完整的示例,演示如何使用 Milvus 和 Visualized-BGE 模型构建一个端到端的图文多模态检索引擎。

4.1 初始化与工具定义

首先导入所有必需的库,定义好模型路径、数据目录等常量。为了代码的整洁和复用,将 Visualized-BGE 模型的加载和编码逻辑封装在一个 Encoder 类中,并定义了一个 visualize_results 函数用于后续的结果可视化。

python 复制代码
import os
from tqdm import tqdm
from glob import glob
import torch
from visual_bge.visual_bge.modeling import Visualized_BGE
from pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataType
import numpy as np
import cv2
from PIL import Image

# ============================================================
# 1. 初始化配置
# ============================================================
# 注意:运行前确保 Milvus Docker 已启动(docker compose up -d)
# Milvus 默认 gRPC 端口 19530,对应 MILVUS_URI

MODEL_NAME = "BAAI/bge-base-en-v1.5"            # Visualized-BGE 的文本基座模型
MODEL_PATH = "../../models/bge/Visualized_base_en_v1.5.pth"  # 视觉权重文件路径
DATA_DIR = "../../data/C3"                       # 数据目录(包含 dragon/ 子目录)
COLLECTION_NAME = "multimodal_demo"              # Milvus 集合名称(类似关系型数据库的表名)
MILVUS_URI = "http://localhost:19530"            # Milvus 服务地址(Docker 默认端口)

# ============================================================
# 2. 定义工具类(编码器 + 可视化函数)
# ============================================================

class Encoder:
    """
    多模态编码器,封装 Visualized-BGE 模型。

    核心能力:
    - encode_image()  :仅传入图片 → 得到纯图像向量(用于索引端,入库时使用)
    - encode_query()   :同时传入图片+文本 → 得到图文融合向量(用于查询端,检索时使用)
    
    原理:Visualized-BGE 将图像 patch token 与文本 token 一起送入 BGE Transformer,
          产生一个统一的多模态向量,维度为 768。
    """
    def __init__(self, model_name: str, model_path: str):
        """加载 Visualized-BGE 模型:基座 BGE + 视觉权重"""
        self.model = Visualized_BGE(model_name_bge=model_name, model_weight=model_path)
        self.model.eval()  # 切换到评估模式(关闭 dropout / batch norm 等训练行为)

    def encode_image(self, image_path: str) -> list[float]:
        """
        纯图像编码 → 用于索引端入库。
        返回 Python list 格式(Milvus 需要 list 而非 tensor)。
        """
        with torch.no_grad():  # 推理模式,不计算梯度,节省显存
            query_emb = self.model.encode(image=image_path)
        return query_emb.tolist()[0]  # tensor → list[float]

    def encode_query(self, image_path: str, text: str) -> list[float]:
        """
        图文融合编码 → 用于查询端检索。
        同时传入 image + text,让模型生成融合了两者语义的向量。
        这是"多模态检索"的核心:以图+文搜图。
        """
        with torch.no_grad():
            query_emb = self.model.encode(image=image_path, text=text)
        return query_emb.tolist()[0]

def visualize_results(query_image_path: str, retrieved_images: list,
                     img_height: int = 300, img_width: int = 300,
                     row_count: int = 3) -> np.ndarray:
    """
    将查询图片 + 检索结果拼接成一张全景对比图。
    
    布局说明:
    - 左侧:查询图片(蓝色边框,标注 "Query")
    - 右侧:检索结果(每行 row_count 张,黑色边框,红色索引编号)
    
    使用的工具链:PIL 读图 → OpenCV 处理 → np.hstack 拼接
    """
    panoramic_width = img_width * row_count
    panoramic_height = img_height * row_count
    # 创建白色画布
    panoramic_image = np.full((panoramic_height, panoramic_width, 3), 255, dtype=np.uint8)
    query_display_area = np.full((panoramic_height, img_width, 3), 255, dtype=np.uint8)

    # ---- 处理查询图像 ----
    query_pil = Image.open(query_image_path).convert("RGB")
    query_cv = np.array(query_pil)[:, :, ::-1]          # RGB → BGR(OpenCV 格式)
    resized_query = cv2.resize(query_cv, (img_width, img_height))
    bordered_query = cv2.copyMakeBorder(resized_query, 10, 10, 10, 10,
                                        cv2.BORDER_CONSTANT, value=(255, 0, 0))  # 蓝色边框
    query_display_area[img_height * (row_count - 1):, :] = cv2.resize(bordered_query,
                                                                      (img_width, img_height))
    cv2.putText(query_display_area, "Query", (10, panoramic_height - 20),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)

    # ---- 处理检索到的图像 ----
    for i, img_path in enumerate(retrieved_images):
        row, col = i // row_count, i % row_count
        start_row, start_col = row * img_height, col * img_width
        
        retrieved_pil = Image.open(img_path).convert("RGB")
        retrieved_cv = np.array(retrieved_pil)[:, :, ::-1]
        resized_retrieved = cv2.resize(retrieved_cv, (img_width - 4, img_height - 4))
        bordered_retrieved = cv2.copyMakeBorder(resized_retrieved, 2, 2, 2, 2,
                                                cv2.BORDER_CONSTANT, value=(0, 0, 0))  # 黑色边框
        panoramic_image[start_row:start_row + img_height,
                        start_col:start_col + img_width] = bordered_retrieved
        # 红色索引编号
        cv2.putText(panoramic_image, str(i), (start_col + 10, start_row + 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)

    return np.hstack([query_display_area, panoramic_image])
4.2 创建 Collection

这是与 Milvus 交互的开始。首先初始化 Milvus 客户端,然后定义 Collection 的 Schema,它规定了集合的数据结构。

python 复制代码
# ============================================================
# 3. 初始化 Milvus 客户端和编码器
# ============================================================
# MilvusClient 是 pymilvus 的高层封装,比旧版 connections.connect() 更简洁
# 直接传入 URI 即可,无需手动管理连接
print("--> 正在初始化编码器和Milvus客户端...")
encoder = Encoder(MODEL_NAME, MODEL_PATH)
milvus_client = MilvusClient(uri=MILVUS_URI)

# ============================================================
# 4. 创建 Milvus Collection(类似关系型数据库中的"表")
# ============================================================
# Collection 由 Schema 定义,Schema 规定了字段结构
# 幂等性处理:如果同名 Collection 已存在则先删除,确保可重复运行
print(f"\n--> 正在创建 Collection '{COLLECTION_NAME}'")
if milvus_client.has_collection(COLLECTION_NAME):
    milvus_client.drop_collection(COLLECTION_NAME)
    print(f"已删除已存在的 Collection: '{COLLECTION_NAME}'")

# 获取图片列表,并计算向量维度(768 维)
image_list = glob(os.path.join(DATA_DIR, "dragon", "*.png"))
if not image_list:
    raise FileNotFoundError(f"在 {DATA_DIR}/dragon/ 中未找到任何 .png 图像。")
dim = len(encoder.encode_image(image_list[0]))

# 定义 Schema 的三个字段
# ┌─────────────┬──────────────────┬────────────────────────────────┐
# │ 字段名      │ 类型             │ 说明                           │
# ├─────────────┼──────────────────┼────────────────────────────────┤
# │ id          │ INT64 + 主键自增 │ 自动生成的唯一 ID             │
# │ vector      │ FLOAT_VECTOR(768)│ 图像嵌入向量(检索核心)      │
# │ image_path  │ VARCHAR(512)     │ 图片路径(用于定位原始文件)   │
# └─────────────┴──────────────────┴────────────────────────────────┘
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dim),
    FieldSchema(name="image_path", dtype=DataType.VARCHAR, max_length=512),
]

# 创建集合 Schema
schema = CollectionSchema(fields, description="多模态图文检索")
print("Schema 结构:")
print(schema)

# 创建集合
milvus_client.create_collection(collection_name=COLLECTION_NAME, schema=schema)
print(f"成功创建 Collection: '{COLLECTION_NAME}'")
print("Collection 结构:")
print(milvus_client.describe_collection(collection_name=COLLECTION_NAME))Copy to clipboardErrorCopied

输出结果:

bash 复制代码
--> 正在创建 Collection 'multimodal_demo'

Schema 结构:
{
    'auto_id': True, 
    'description': '多模态图文检索', 
    'fields': [
        {'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True}, 
        {'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, 
        {'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}
    ], 
    'enable_dynamic_field': False
}

成功创建 Collection: 'multimodal_demo'

Collection 结构:
{
    'collection_name': 'multimodal_demo', 
    'auto_id': True, 
    'num_shards': 1, 
    'description': '多模态图文检索', 
    'fields': [
        {'field_id': 100, 'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'params': {}, 'auto_id': True, 'is_primary': True}, 
        {'field_id': 101, 'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, 
        {'field_id': 102, 'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}
    ], 
    'functions': [], 
    'aliases': [], 
    'collection_id': 459243798405253751, 
    'consistency_level': 2, 
    'properties': {}, 
    'num_partitions': 1, 
    'enable_dynamic_field': False, 
    'created_timestamp': 459249546649403396, 
    'update_timestamp': 459249546649403396
}Copy to clipboardErrorCopied

上面的输出详细展示了刚刚创建的 multimodal_demo Collection 的完整结构。其 Schema 包含了三个核心字段(Field ):一个自增的 id 作为主键 ,一个 768 维的 vector 向量字段 用于存储图像嵌入,以及一个 image_path 标量字段来记录原始图片路径。

4.3 准备并插入数据

创建好 Collection 后,需要将数据填充进去。通过遍历指定目录下的所有图片,将它们逐一编码成向量,然后与图片路径一起组织成符合 Schema 结构的格式,最后批量插入到 Collection 中。

python 复制代码
# ============================================================
# 5. 准备并插入数据(图片 → 向量 → 入库)
# ============================================================
# 注意:这里使用的是 encode_image(纯图像向量),而非 encode_query(图文融合)
# 这是 RAG 中常见的策略:索引端用单模态(纯图像),查询端用多模态(图文融合)
print(f"\n--> 正在向 '{COLLECTION_NAME}' 插入数据")
data_to_insert = []
for image_path in tqdm(image_list, desc="生成图像嵌入"):
    vector = encoder.encode_image(image_path)
    data_to_insert.append({"vector": vector, "image_path": image_path})

# 批量插入(比逐条插入效率更高)
# 注意:auto_id=True 时,数据中不能包含 id 字段,否则会冲突
if data_to_insert:
    result = milvus_client.insert(collection_name=COLLECTION_NAME, data=data_to_insert)
    print(f"成功插入 {result['insert_count']} 条数据。")Copy to clipboardErrorCopied
4.4 创建索引

为了实现快速检索,需要为向量字段创建索引。这里选择 HNSW 索引,它在召回率和查询性能之间有着很好的平衡。创建索引后,必须调用 load_collection 将集合加载到内存中才能进行搜索。

python 复制代码
# ============================================================
# 6. 创建索引(加速检索的关键步骤)
# ============================================================
# 选择 HNSW 索引,它在召回率和查询性能之间平衡极佳
# 
# 索引参数说明:
# ┌────────────────┬──────┬─────────────────────────────────────┐
# │ 参数           │ 值   │ 说明                                │
# ├────────────────┼──────┼─────────────────────────────────────┤
# │ index_type     │ HNSW │ 分层可导航小世界图(图索引)        │
# │ metric_type    │ COSINE│ 余弦相似度(向量已归一化 = 内积)  │
# │ M              │ 16   │ 每个节点的最大连接数(4~64)        │
# │ efConstruction │ 256  │ 构建时的搜索宽度(8~512)           │
# └────────────────┴──────┴─────────────────────────────────────┘
# 提示:M 和 efConstruction 越大,召回率越高,但内存和构建时间也越大
print(f"\n--> 正在为 '{COLLECTION_NAME}' 创建索引")
index_params = milvus_client.prepare_index_params()
index_params.add_index(
    field_name="vector",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 256}
)
milvus_client.create_index(collection_name=COLLECTION_NAME, index_params=index_params)
print("成功为向量字段创建 HNSW 索引。")
print("索引详情:")
print(milvus_client.describe_index(collection_name=COLLECTION_NAME, index_name="vector"))

# ⚠️ 必须调用 load_collection!否则 search() 会报错
# 作用:将索引和数据加载到内存,之后才能执行搜索
milvus_client.load_collection(collection_name=COLLECTION_NAME)
print("已加载 Collection 到内存中。")Copy to clipboardErrorCopied

输出结果:

bash 复制代码
--> 正在为 'multimodal_demo' 创建索引
成功为向量字段创建 HNSW 索引。
索引详情:
{'M': '16', 'efConstruction': '256', 'metric_type': 'COSINE', 'index_type': 'HNSW', 'field_name': 'vector', 'index_name': 'vector', 'total_rows': 0, 'indexed_rows': 0, 'pending_index_rows': 0, 'state': 'Finished'}
已加载 Collection 到内存中。Copy to clipboardErrorCopied

可以看出,索引创建成功,在 vector 字段上成功创建了 HNSW 索引,并使用 COSINE 作为距离度量。M: '16'efConstruction: '256' 是 HNSW 索引的两个关键参数,分别控制着图中每个节点的最大连接数和索引构建时的搜索范围,这些参数直接影响检索的性能和准确性。state: 'Finished' 状态表明索引已成功构建。

4.5 执行多模态检索

这里通过定义一个包含图片和文本的组合查询,将其编码为查询向量,然后调用 search 方法在 Milvus 中执行近似最近邻搜索。

python 复制代码
# ============================================================
# 7. 执行多模态检索(图文融合查询)
# ============================================================
print(f"\n--> 正在 '{COLLECTION_NAME}' 中执行检索")
query_image_path = os.path.join(DATA_DIR, "dragon", "query.png")
query_text = "一条龙"

# 关键区别:这里用 encode_query(图文融合),而非 encode_image(纯图像)
# encode_query 同时传入 image + text,生成一个同时理解"龙"的视觉和语义的向量
query_vector = encoder.encode_query(image_path=query_image_path, text=query_text)

# 执行 ANN 近似最近邻搜索
# search 参数说明:
#   data          : 查询向量列表(支持批量查询)
#   output_fields : 返回结果中附带哪些标量字段(需要 image_path 才能可视化)
#   limit=5       : 返回 Top-5 最相似结果
#   ef=128        : HNSW 查询时的搜索宽度(越大越准越慢,通常 >= limit)
search_results = milvus_client.search(
    collection_name=COLLECTION_NAME,
    data=[query_vector],
    output_fields=["image_path"],
    limit=5,
    search_params={"metric_type": "COSINE", "params": {"ef": 128}}
)[0]  # [0] 取第一个查询向量的结果

# 解析检索结果:每个 hit 的结构为
# {"id": ..., "distance": ..., "entity": {"image_path": ...}}
retrieved_images = []
print("检索结果:")
for i, hit in enumerate(search_results):
    print(f"  Top {i+1}: ID={hit['id']}, 距离={hit['distance']:.4f}, 路径='{hit['entity']['image_path']}'")
    retrieved_images.append(hit['entity']['image_path'])Copy to clipboardErrorCopied

输出结果:

bash 复制代码
--> 正在 'multimodal_demo' 中执行检索
检索结果:
  Top 1: ID=459243798403756667, 距离=0.9411, 路径='../../data/C3\dragon\dragon01.png'
  Top 2: ID=459243798403756668, 距离=0.5818, 路径='../../data/C3\dragon\dragon02.png'
  Top 3: ID=459243798403756671, 距离=0.5731, 路径='../../data/C3\dragon\dragon05.png'
  Top 4: ID=459243798403756670, 距离=0.4894, 路径='../../data/C3\dragon\dragon04.png'
  Top 5: ID=459243798403756669, 距离=0.4100, 路径='../../data/C3\dragon\dragon03.png'Copy to clipboardErrorCopied

这段输出展示了与图文组合查询最相似的5个*实体 (Entity)**。distance 字段代表了*余弦相似度 ,值越接近 1 表示越相似。可以看到,Top 1 结果正是查询图片本身,其相似度得分最高(0.9411),这说明了检索的有效性。其余结果也都是龙的图片,并按相似度从高到低精确排列。

4.6 可视化与清理]

最后,将检索到的图片路径用于可视化,生成一张直观的结果对比图。在完成所有操作后,应该释放 Milvus 中的资源,包括从内存中卸载 Collection 和删除整个 Collection。

python 复制代码
# ============================================================
# 8. 可视化结果 + 清理资源
# ============================================================
# 将检索结果拼成全景图保存到磁盘
print(f"\n--> 正在可视化结果并清理资源")
if not retrieved_images:
    print("没有检索到任何图像。")
else:
    panoramic_image = visualize_results(query_image_path, retrieved_images)
    combined_image_path = os.path.join(DATA_DIR, "search_result.png")
    cv2.imwrite(combined_image_path, panoramic_image)
    print(f"结果图像已保存到: {combined_image_path}")
    Image.open(combined_image_path).show()

# ---- 清理 Milvus 资源 ----
# 注意:生产环境中通常不会删除 Collection,而是保留供后续查询
# 以下清理步骤仅适用于此演示脚本

# 释放内存:将 Collection 从内存中卸载
milvus_client.release_collection(collection_name=COLLECTION_NAME)
print(f"已从内存中释放 Collection: '{COLLECTION_NAME}'")

# 删除整个 Collection(包含数据和索引)
milvus_client.drop_collection(COLLECTION_NAME)
print(f"已删除 Collection: '{COLLECTION_NAME}'")Copy to clipboardErrorCopied

通过上图可以看出,这个多模态检索引擎成功地理解了"一条龙"这个图文组合查询的意图,并从图库中找到了最相关的几张图片并进行排序。

整个多模态实践捋一遍:Encoder把图转向量 → 建Collection定义Schema → 入库 → 建HNSW索引 → load到内存 → 图文融合向量去搜 → 可视化 → 清理。印象最深的是"索引端用纯图像向量,查询端用图文融合向量"这个策略。

第五节 索引优化

一、上下文扩展

在RAG系统中,常常面临一个权衡问题:使用小块文本 进行检索可以获得更高的精确度,但小块文本缺乏足够的上下文,可能导致大语言模型(LLM)无法生成高质量的答案;而使用大块文本 虽然上下文丰富,却容易引入噪音,降低检索的相关性。为了解决这一矛盾,LlamaIndex 提出了一种实用的索引策略------句子窗口检索(Sentence Window Retrieval) 2。该技术巧妙地结合了两种方法的优点 :它在检索时聚焦于高度精确的单个句子 ,在送入LLM生成答案前,又智能地将上下文扩展回一个更宽的"窗口",从而同时保证检索的准确性和生成的质量。

意思应该是先检索单个句子-将上下文扩展-送入LLM生成答案

1.1 主要思路

句子窗口检索的思想可以概括为:为检索精确性而索引小块,为上下文丰富性而检索大块

其工作流程如下:

(1)索引阶段 :在构建索引时,文档被分割单个句子 。每个句子都作为一个独立的"节点(Node)"存入向量数据库。同时,每个句子节点都会在元数据(metadata)中存储其上下文窗口,即该句子原文中的前N个和后N个句子。这个窗口内的文本不会被索引,仅仅是作为元数据存储。

(2)检索阶段 :当用户发起查询时,系统会在所有单一句子节点上执行相似度搜索。因为句子是表达完整语义的最小单位,所以这种方式可以非常精确地定位到与用户问题最相关的核心信息。

(3)后处理阶段 :在检索到最相关的句子节点后,系统会使用一个名为 MetadataReplacementPostProcessor 的后处理模块。该模块会读取到检索到的句子节点的元数据,并用元数据中存储的完整上下文窗口来替换节点中原来的单一句子内容。

(4)生成阶段:最后,这些被替换了内容的、包含丰富上下文的节点被传递给LLM,用于生成最终的答案。

1.先分割-存入数据库并且存储其上下文窗口

2.最小单位句子进行检索

3.使用后处理模块-并且用完整的上下文窗口替换

4.返回LLM

1.2 代码实现

下面通过 LlamaIndex 官网的示例,来演示如何实现句子窗口检索,并与常规的检索方法进行对比。该示例将加载一份PDF格式的IPCC气候报告,并就其中的专业问题进行提问。

核心代码如下:

python 复制代码
# 假设 Settings.llm 和 Settings.embed_model 已经预先配置好

# 1. 加载文档 
documents = SimpleDirectoryReader(
    input_files=["../../data/C3/pdf/IPCC_AR6_WGII_Chapter03.pdf"]
).load_data()

# 2. 创建节点与构建索引
# 2.1 句子窗口索引
node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)
sentence_nodes = node_parser.get_nodes_from_documents(documents)
sentence_index = VectorStoreIndex(sentence_nodes)

根据 LlamaIndex 的底层源码,SentenceWindowNodeParser 的核心逻辑位于 build_window_nodes_from_documents 方法中。其实现过程可以分解为以下几个关键步骤:

(1)句子切分 (sentence_splitter) :解析器首先接收一个文档(Document),然后调用 self.sentence_splitter(doc.text) 方法。这个 sentence_splitter 是一个可配置的函数,默认为 split_by_sentence_tokenizer,它负责将文档的全部文本精确地切分成一个句子列表(text_splits)。

(2)创建基础节点 (build_nodes_from_splits) :切分出的 text_splits 列表被传递给 build_nodes_from_splits 工具函数。这个函数会为列表中的每一个句子 都创建一个独立的 TextNode。此时,每个 TextNodetext 属性就是这个句子的内容。

(3)构建窗口并填充元数据 (主要循环) :接下来,解析器会遍历所有新创建的 TextNode。对于位于第 i 个位置的节点,它会执行以下操作:

  • 定位窗口 :通过列表切片 nodes[max(0, i - self.window_size) : min(i + self.window_size + 1, len(nodes))] 来获取一个包含中心句子及其前后 window_size(默认为3)个邻近节点的列表(window_nodes)。这个切片操作很巧妙地处理了文档开头和结尾的边界情况。
  • 组合窗口文本 :将 window_nodes 列表中所有节点的 text(即所有在窗口内的句子)用空格拼接成一个长字符串。
  • 填充元数据 :将上一步生成的长字符串(完整的上下文窗口)存入当前节点(第i个节点)的元数据中,键为 self.window_metadata_key(默认为 "window")。同时,也会将节点自身的文本(原始句子)存入元数据,键为 self.original_text_metadata_key(默认为 "original_text")。
  1. 设置元数据排除项 :这是一个非常关键的细节。在填充完元数据后,代码会执行 node.excluded_embed_metadata_keys.extend(...)node.excluded_llm_metadata_keys.extend(...)。这行代码的作用是告诉后续的嵌入模型和LLM,在处理这个节点时,应当忽略 "window""original_text" 这两个元数据字段。这确保了只有单个句子的纯净文本被用于生成向量嵌入,从而保证了检索的高精度。而 "window" 字段仅供后续的 MetadataReplacementPostProcessor 使用。

通过以上步骤,SentenceWindowNodeParser 最终返回一个 TextNode 列表。列表中的每个节点都代表一个独立的句子,其 text 属性用于精确检索,而其 metadata 中则"隐藏"了用于生成答案的丰富上下文窗口。

python 复制代码
# 2.2 常规分块索引 (基准)
base_parser = SentenceSplitter(chunk_size=512)
base_nodes = base_parser.get_nodes_from_documents(documents)
base_index = VectorStoreIndex(base_nodes)

# 3. 构建查询引擎
sentence_query_engine = sentence_index.as_query_engine(
    similarity_top_k=2,
    node_postprocessors=[
        MetadataReplacementPostProcessor(target_metadata_key="window")
    ],
)
base_query_engine = base_index.as_query_engine(similarity_top_k=2)

# 4. 执行查询并对比结果
query = "What are the concerns surrounding the AMOC?"
print(f"查询: {query}\n")

print("--- 句子窗口检索结果 ---")
window_response = sentence_query_engine.query(query)
print(f"回答: {window_response}\n")

print("--- 常规检索结果 ---")
base_response = base_query_engine.query(query)
print(f"回答: {base_response}\n")Copy to clipboardErrorCopied

(1)构建句子窗口索引 :这一步利用了 SentenceWindowNodeParser。它将文档解析为以单个句子为单位的 Node,同时将包含上下文的"窗口"文本(默认为前后各3个句子)存储在每个 Node 的元数据中。这一步是实现"为检索精确性而索引小块"思想的关键。

(2)构建查询引擎与后处理:查询引擎的构建是实现"为生成质量而扩展上下文"的关键。

  • 在创建 sentence_query_engine 时,配置中加入了一个重要的后处理器 MetadataReplacementPostProcessor
  • 它的作用是:当检索器根据用户查询找到最相关的节点(也就是单个句子)后,这个后处理器会立即介入。
  • 它会从该节点的元数据中读取出预先存储的完整"窗口"文本,并用它替换掉节点中原来的单个句子内容。
  • 这样,最终传递给大语言模型的就不再是孤立的句子,而是包含丰富上下文的完整文本段落,从而确保了生成答案的质量和连贯性。

我们向两个引擎提出的问题是:"关于大西洋经向翻转环流(AMOC),人们主要担忧什么?" (What are the concerns surrounding the AMOC?)。

代码输出如下:

bash 复制代码
查询: What are the concerns surrounding the AMOC?

--- 句子窗口检索结果 ---
回答: The Atlantic Meridional Overturning Circulation (AMOC) is projected to decline over the 21st century with high confidence, though there is low confidence in quantitative projections of this decline. Observational records since the mid-2000s are too short to determine the relative contributions of internal variability, natural forcing, and anthropogenic forcing to AMOC changes. Additionally, there is low confidence in reconstructed and modeled AMOC changes for the 20th century due to limited agreement in quantitative trends. While an abrupt collapse before 2100 is not expected, the decline could have significant implications for global climate patterns.

--- 常规检索结果 ---
回答: The concerns surrounding the Atlantic Meridional Overturning Circulation (AMOC) primarily involve its projected decline over the 21st century across all Shared Socioeconomic Pathway (SSP) scenarios. While an abrupt collapse before 2100 is not expected, there is high confidence in this decline, though quantitative projections remain uncertain. Observational records since the mid-2000s are too short to clearly distinguish the contributions of internal variability, natural forcing, and anthropogenic forcing to these changes. This uncertainty highlights the need for further research to better understand and predict AMOC behavior and its broader climate impacts.Copy to clipboardErrorCopied

从输出结果中可以观察到:

  • 两个答案都抓住了核心:两个引擎都正确地识别出,对AMOC的主要担忧是其在21世纪预计的衰退。

  • 句子窗口检索的答案更详尽、更连贯:句子窗口检索的回答不仅指出了衰退的趋势,还补充了关于"定量预测的置信度低"、"观测记录时间过短"、"20世纪重建和模拟的变化置信度低"等多个维度的细节。这使得答案的信息量更大,上下文更完整,更像一个综述。

  • 常规检索的答案相对宽泛:常规检索的回答虽然正确,但内容相对概括,最后以"需要进一步研究"这样较为笼同的结论收尾。

    这种差异正是句子窗口检索策略优势的体现。它通过"精确检索小文本块(单个句子),再扩展上下文(句子窗口)"的方式,为大语言模型提供了高度相关且信息丰富的上下文,从而生成了质量更高的答案。

句子窗口检索的思路:索引按单个句子建(保证检索精度),查完之后用MetadataReplacementPostProcessor把句子替换回它前后N个句子的上下文(保证生成质量)。说白了就是"以小博大"------用小单位检索,拿大窗口生成。

二、结构化索引

随着知识库的规模不断扩大(例如,包含数百个PDF文件),传统的RAG方法(即对所有文本块进行top-k相似度搜索)会遇到瓶颈。当一个查询可能只与其中一两个文档相关时,在整个文档库中进行无差别的向量搜索,不仅效率低下,还容易被不相关的文本块干扰,导致检索结果不精确。

为了解决这个问题,一个有效的方法是利用结构化索引 。其原理是在索引文本块的同时,为其附加结构化的元数据(Metadata)。这些元数据可以是任何有助于筛选和定位信息的标签,例如:

  • 文件名
  • 文档创建日期
  • 章节标题
  • 作者
  • 任何自定义的分类标签

实际上,在第二章"文本分块"中介绍的基于文档结构的分块 方法,就是实现结构化索引的一种前置步骤。例如,在使用 MarkdownHeaderTextSplitter 时,分块器会自动将Markdown文档的各级标题(如 Header 1, Header 2 等)提取并存入每个文本块的元数据中。这些标题信息就是非常有价值的结构化数据,可以直接用于后续的元数据过滤。

通过这种方式,可以在检索时实现"元数据过滤"和"向量搜索"的结合。例如,当用户查询"请总结一下2023年第二季度财报中关于AI的论述"时,系统可以:

(1)元数据预过滤 :首先通过元数据筛选,只在 document_type == '财报'year == 2023quarter == 'Q2' 的文档子集中进行搜索。

(2)向量搜索:然后,在经过滤的、范围更小的文本块集合中,执行针对查询"关于AI的论述"的向量相似度搜索。

这种"先过滤,再搜索"的策略,能够极大地缩小检索范围,显著提升大规模知识库场景下RAG应用的检索效率和准确性。LlamaIndex 提供了包括"自动检索"(Auto-Retrieval)在内的多种工具来支持这种结构化的检索范式。

也就是说结构化索引 就是给这个元数据一个能筛选的标签

2.1 基于多表格的检索

在更复杂的场景中,结构化数据可能分布在多个来源中,例如一个包含多个工作表(Sheet)的 Excel 文件,每个工作表都代表一个独立的表格。在这种情况下,需要一种更强大的策略:递归检索 3。它能实现"路由"功能,先将查询引导至正确的知识来源(正确的表格),然后再在该来源内部执行精确查询。

下面使用一个包含多个工作表的电影数据 Excel 文件(movie.xlsx)来演示,其中每个工作表(如 年份_1994, 年份_2002 等)都存储了对应年份的电影信息。

python 复制代码
# 1. 为每个工作表创建查询引擎和摘要节点
excel_file = '../../data/C3/excel/movie.xlsx'
xls = pd.ExcelFile(excel_file)

df_query_engines = {}
all_nodes = []

for sheet_name in xls.sheet_names:
    df = pd.read_excel(xls, sheet_name=sheet_name)
    # 为当前工作表创建一个 PandasQueryEngine
    query_engine = PandasQueryEngine(df=df, llm=Settings.llm, verbose=True)
    # 为当前工作表创建一个摘要节点(IndexNode)
    year = sheet_name.replace('年份_', '')
    summary = f"这个表格包含了年份为 {year} 的电影信息,可以用来回答关于这一年电影的具体问题。"
    node = IndexNode(text=summary, index_id=sheet_name)
    all_nodes.append(node)
    # 存储工作表名称到其查询引擎的映射
    df_query_engines[sheet_name] = query_engine

# 2. 创建顶层索引(只包含摘要节点)
vector_index = VectorStoreIndex(all_nodes)

# 3. 创建递归检索器
vector_retriever = vector_index.as_retriever(similarity_top_k=1)
recursive_retriever = RecursiveRetriever(
    "vector",
    retriever_dict={"vector": vector_retriever},
    query_engine_dict=df_query_engines,
    verbose=True,
)

# 4. 创建查询引擎
query_engine = RetrieverQueryEngine.from_args(recursive_retriever)

# 5. 执行查询
query = "1994年评分人数最多的电影是哪一部?"
print(f"查询: {query}")
response = query_engine.query(query)
print(f"回答: {response}")Copy to clipboardErrorCopied
  1. 创建 PandasQueryEngine :遍历 Excel 中的每个工作表,为每个工作表(即一个独立的 DataFrame)都实例化一个 PandasQueryEngine。其强大之处在于,它能将关于表格的自然语言问题(如"评分人数最多的是哪个")转换成实际的 Pandas 代码(如 df.sort_values('评分人数').iloc[-1])来执行。

  2. 创建摘要节点 (IndexNode) :对每个工作表,都创建一个 IndexNode,其内容是关于这个表格的一段摘要文本。这个节点将作为顶层检索的"指针"。

  3. 构建顶层索引 :使用所有创建的 IndexNode 构建一个 VectorStoreIndex。这个索引不包含任何表格的详细数据,只包含指向各个表格的"指针"信息。

  4. 创建 RecursiveRetriever

    :这是实现递归检索的核心。将其配置为:

    • retriever_dict: 指定顶层的检索器,即在摘要节点中进行检索的 vector_retriever
    • query_engine_dict: 提供一个从节点 ID(即工作表名称)到其对应查询引擎的映射。当顶层检索器匹配到某个摘要节点后,递归检索器就知道该调用哪个 PandasQueryEngine 来处理后续查询。

运行结果:

bash 复制代码
查询: 1994年评分人数最少的电影是哪一部?
> Retrieving with query id None: 1994年评分人数最少的电影是哪一部?
> Retrieved node with id, entering: 年份_1994
> Retrieving with query id 年份_1994: 1994年评分人数最少的电影是哪一部?
> Pandas Instructions:Copy to clipboardErrorCopied

dfdf\['年份' == 1994].nsmallest(1, '评分人数')'电影名称'.iloc0

复制代码
> Pandas Output: 燃情岁月
回答: 燃情岁月Copy to clipboardErrorCopied

从输出中可以清晰地看到递归检索的完整流程:

(1)顶层路由Retrieving with query id None,系统首先在顶层的摘要索引中检索,根据问题"1994年..."匹配到了摘要节点 年份_1994

(2)进入子层Retrieved node with id, entering: 年份_1994,系统决定进入与"年份_1994"这个工作表关联的查询引擎。

(3)子层查询Retrieving with query id 年份_1994PandasQueryEngine 接管查询,并将问题发送给 LLM,让其生成 Pandas 代码。

(4)代码生成与执行 :LLM 生成了 df[df['年份'] == 1994].nsmallest(1, '评分人数')['电影名称'].iloc[0],引擎执行后得到输出 燃情岁月

2.2 另一种实现方式

鉴于 PandasQueryEngine 的安全风险,还可以采用一种更安全的方式来实现类似的多表格查询,思路是将路由和检索彻底分离

这种改进方法的具体步骤如下:

(1)创建两个独立的向量索引

  • 摘要索引(用于路由) :为每个Excel工作表(例如,"1994年电影数据")创建一个非常简短的摘要性Document,例如:"此文档包含1994年的电影信息"。然后,用所有这些摘要文档构建一个轻量级的向量索引。这个索引的唯一目的就是充当"路由器"。
  • 内容索引(用于问答) :将每个工作表的实际数据(例如,整个表格)转换为一个大的文本Document,并为其附加一个关键的元数据标签,如 {"sheet_name": "年份_1994"}。然后,用所有这些包含真实内容的文档构建一个向量索引。

(2)执行两步查询

  • 第一步:路由 。当用户提问(例如,"1994年评分人数最少的电影是哪一部?")时,首先在"摘要索引"中进行检索。由于问题中的"1994年"与"此文档包含1994年的电影信息"这个摘要高度相关,检索器会快速返回其对应的元数据,告诉系统目标是 年份_1994 这个工作表。
  • 第二步:检索 。拿到 年份_1994 这个目标后,系统会在"内容索引"中进行检索,但这次会附加一个元数据过滤器MetadataFilter),强制要求只在 sheet_name == "年份_1994" 的文档中进行搜索。这样,LLM就能在正确的、经过筛选的数据范围内找到问题的答案。

通过这种"先路由,后用元数据过滤检索"的方式,既实现了跨多个数据源的查询能力,又避免了执行代码的安全隐患。LlamaIndex 官方也提供了类似的结构化分层检索4可以参考。

两种多表格检索方式对比:

  • 递归检索器:顶层摘要索引做路由 → 命中后递归到PandasQueryEngine去查。简单但有执行代码的安全风险。
  • 路由+元数据过滤:先用摘要索引路由到目标sheet,再用元数据过滤器限定搜索范围。更安全。
    部?
    Retrieved node with id, entering: 年份_1994
    Retrieving with query id 年份_1994: 1994年评分人数最少的电影是哪一部?
    Pandas Instructions:Copy to clipboardErrorCopied
复制代码
df[df['年份'] == 1994].nsmallest(1, '评分人数')['电影名称'].iloc[0]

Pandas Output: 燃情岁月

回答: 燃情岁月Copy to clipboardErrorCopied

复制代码
从输出中可以清晰地看到递归检索的完整流程:

(1)**顶层路由**:`Retrieving with query id None`,系统首先在顶层的摘要索引中检索,根据问题"1994年..."匹配到了摘要节点 `年份_1994`。

(2)**进入子层**:`Retrieved node with id, entering: 年份_1994`,系统决定进入与"年份_1994"这个工作表关联的查询引擎。

(3)**子层查询**:`Retrieving with query id 年份_1994`,`PandasQueryEngine` 接管查询,并将问题发送给 LLM,让其生成 Pandas 代码。

(4)**代码生成与执行**:LLM 生成了 `df[df['年份'] == 1994].nsmallest(1, '评分人数')['电影名称'].iloc[0]`,引擎执行后得到输出 `燃情岁月`。

#### 2.2 另一种实现方式

  鉴于 `PandasQueryEngine` 的安全风险,还可以采用一种更安全的方式来实现类似的多表格查询,思路是**将路由和检索彻底分离**。

这种改进方法的具体步骤如下:

(1)**创建两个独立的向量索引**:

- **摘要索引(用于路由)**:为每个Excel工作表(例如,"1994年电影数据")创建一个非常简短的摘要性`Document`,例如:"此文档包含1994年的电影信息"。然后,用所有这些摘要文档构建一个轻量级的向量索引。这个索引的唯一目的就是充当"路由器"。
- **内容索引(用于问答)**:将每个工作表的实际数据(例如,整个表格)转换为一个大的文本`Document`,并为其附加一个关键的元数据标签,如 `{"sheet_name": "年份_1994"}`。然后,用所有这些包含真实内容的文档构建一个向量索引。

(2)**执行两步查询**:

- **第一步:路由**。当用户提问(例如,"1994年评分人数最少的电影是哪一部?")时,首先在"摘要索引"中进行检索。由于问题中的"1994年"与"此文档包含1994年的电影信息"这个摘要高度相关,检索器会快速返回其对应的元数据,告诉系统目标是 `年份_1994` 这个工作表。
- **第二步:检索**。拿到 `年份_1994` 这个目标后,系统会在"内容索引"中进行检索,但这次会附加一个**元数据过滤器**(`MetadataFilter`),强制要求只在 `sheet_name == "年份_1994"` 的文档中进行搜索。这样,LLM就能在正确的、经过筛选的数据范围内找到问题的答案。

通过这种"先路由,后用元数据过滤检索"的方式,既实现了跨多个数据源的查询能力,又避免了执行代码的安全隐患。LlamaIndex 官方也提供了类似的结构化分层检索[4](https://datawhalechina.github.io/all-in-rag/#fn-4)可以参考。

> 两种多表格检索方式对比:
> - 递归检索器:顶层摘要索引做路由 → 命中后递归到PandasQueryEngine去查。简单但有执行代码的安全风险。
> - 路由+元数据过滤:先用摘要索引路由到目标sheet,再用元数据过滤器限定搜索范围。更安全。
> 个人倾向第二种,不用执行代码,纯靠向量搜索就能完成。
相关推荐
天国梦1 小时前
中学生居家英语听力训练深度解析:从核心痛点到AI赋能的全链路方案
人工智能·学习
胡渠洋5 小时前
postman学习
学习·测试工具·postman
Amazing_Cacao6 小时前
CFCA精品可可产区风土解析(美洲):无情打破风味堆叠假象,建立时间轴上的层次动态阅读系统
学习
六点_dn7 小时前
Linux学习笔记-shell运算符
linux·笔记·学习
其实防守也摸鱼7 小时前
渗透--损坏的对象级别鉴权漏洞(Broken Object Level Authorization, BOLA)
大数据·网络·数据库·学习·tcp/ip·安全·安全威胁分析
念雨思8 小时前
信用卡权益对比:基于HarmonyOS + ArkTS的AI智能金融助手开发实践
人工智能·学习·华为·金融·harmonyos·鸿蒙
铅笔侠_小龙虾8 小时前
Rust 学习(2)-变量、常量与 shadowing
学习·算法·rust
it小生01018 小时前
从底层思维到高效行动,顶尖人才进阶核心修炼指南
学习·学习方法
恣逍信点8 小时前
《凌微经》通俗解读——哲学第一因
学习·职场和发展·创业创新·学习方法·业界资讯·交友·哲学