# 离线 RAG 索引构建系统 (Offline RAG Indexer) 该模块负责 RAG 系统的阶段一:**离线索引构建**。它将外部的非结构化数据(如文档、PDF、网页等)清洗、切分并转化为向量,最终存入向量数据库中。 ## 🎯 核心架构 ### 技术栈 | 组件 | 技术选型 | 版本 | 说明 | |:-----|:---------|:-----|:-----| | **文档解析** | `unstructured` | 0.22+ | 多格式文档解析(PDF/DOCX/TXT等) | | **文本切分** | `langchain-text-splitters` | 内置 | 递归字符切分 + 语义切分 | | **语义切分** | `langchain-experimental` | 内置 | `SemanticChunker` 基于句子相似度 | | **嵌入模型** | `llama.cpp` | 本地服务 | `Qwen3-Embedding-0.6B` GGUF 模型 | | **稀疏嵌入** | `fastembed` | 内置 | BM25 关键词检索 | | **向量数据库** | `Qdrant` | 1.17+ | HNSW 索引,支持稠密/稀疏向量 + RRF 融合 | | **文档存储** | `PostgreSQL` | 16+ | 异步连接池,持久化父块 | | **编排框架** | `asyncio` | Python 3.10+ | 全异步批量处理 | ### 数据流向总览 ``` ┌─────────────────────────────────────────────────────────────┐ │ builder.py │ │ IndexBuilder 入口 │ └──────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ loaders.py │ │ DocumentLoader.load_file() │ │ → 返回 List[Document] │ └──────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 自定义父子块索引实现 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ parent_splitter (粗切) │ │ │ │ 父块 ~1000 字符 │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ 父文档存入 PostgreSQL (UUID 映射) │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ child_splitter (细切) │ │ │ │ 子块 ~200 字符 │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ 子文档生成 dense + sparse 双向量 │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ┌──────────────────────▼──────────────────────────────┐ │ │ │ 子文档存入 Qdrant (payload 含 parent_id) │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### 技术特性 - ✅ **多格式支持**:PDF、DOCX、TXT、MD、HTML、PPTX、XLSX、JSON - ✅ **三种切分策略**:递归字符切分、语义切分、父子块策略 - ✅ **Parent-Child 架构**:子块精准检索,父块完整上下文 - ✅ **PostgreSQL DocStore**:持久化存储父块,支持异步连接池 - ✅ **混合检索**:稠密向量(语义)+ 稀疏向量(关键词),Qdrant 原生 RRF 融合 - ✅ **完全异步化**:索引构建、检索全链路 async / await - ✅ **批量写入**:高效批量处理,自动分批 - ✅ **上下文管理器**:支持同步/异步资源管理 ## 📂 架构与文件结构 ``` rag_indexer/ ├── __init__.py ├── index_builder.py # 索引构建主流水线(自定义父子块实现) ├── loaders.py # 文档加载器(多格式支持) ├── splitters.py # 文本切分器(递归/语义/父子块) ├── config.py # 配置管理 ├── cli.py # 命令行接口 ├── clear_qdrant.py # 清空 Qdrant 集合 ├── reset_qdrant.py # 重置 Qdrant 集合 └── README.md # 本文档 ``` ``` backend/rag_core/ ├── __init__.py ├── vector_store.py # Qdrant 混合存储(异步) ├── sparse_embedder.py # BM25 稀疏嵌入 ├── embedders.py # 嵌入模型封装 ├── doc_store.py # PostgreSQL 文档存储 ├── client.py # Qdrant 同步/异步客户端工厂 └── config.py # 配置管理 ``` ``` backend/app/rag/ ├── retriever.py # 混合检索器(异步) ├── rerank.py # llama.cpp 远程重排序器 ├── query_transform.py # 多路查询改写生成器 ├── fusion.py # RRF 倒数排名融合算法 ├── pipeline.py # RAG 流水线编排 ├── tools.py # LangChain Tool 封装 ├── evaluate.py # 评估工具 └── README.md # 本文档 ``` ``` backend/app/model_services/ ├── embedding_services.py # 嵌入服务 ├── chat_services.py # LLM 服务 └── rerank_services.py # 重排序服务 ``` ## 🎯 演进路线与核心算法 (Roadmap) ### Level 1: 基础暴力切分 (Basic Recursive Splitting) - **核心算法**: 递归字符切分。它按照预定义的分隔符列表(如 `["\n\n", "\n", "。", "!", "?", " ", ""]`)从大到小尝试切分文本,直到每块的大小满足最大长度限制。 - **优缺点**: 实现极简单,速度快。但非常容易将一句话拦腰截断,导致上下文语义丢失。 - **实现指南**: - 从 `langchain_text_splitters` 导入 `RecursiveCharacterTextSplitter` - 实例化时设置 `chunk_size`(如 500)和 `chunk_overlap`(如 50) - 直接调用 `.split_documents(raw_docs)` 方法 ### Level 2: 语义动态切分 (Semantic Chunking) - **核心算法**: 句子级相似度阈值算法。 1. 将文章按标点符号按句子拆分 2. 使用轻量级 Embedding 模型将每一句向量化 3. 计算相邻两句之间的余弦相似度 (Cosine Similarity) 4. 当相似度低于设定阈值时(说明两句话讲的不是同一件事,语义发生了转折),在此处"切断"形成一个新的块 - **优缺点**: 极大程度保留了段落内语义的连贯性,对 LLM 回答非常友好。但由于在切分阶段就需要调用向量模型,耗时略长。 - **实现指南**: - 从 `langchain_experimental.text_splitter` 导入 `SemanticChunker` - 实现 `SemanticChunkerAdapter` 继承 `TextSplitter`,解决类型不兼容问题 - 实例化时需要传入已配置好的 Embedding 模型实例 ### Level 3: 高级父子块策略 (Parent-Child / Auto-merging) - **核心算法**: 层次化双重存储与映射(自定义实现)。 - **切分机制**: 首先将文档粗切为较大的"父块 (Parent Chunk, 约 1000 字符)",随后将父块细切为较小的"子块 (Child Chunk, 约 200 字符)" - **存储机制**: - **子块**: 存入 Qdrant,同时生成 dense 向量(语义)和 sparse 向量(关键词),payload 中包含 `parent_id` - **父块**: 存入 PostgreSQL,通过 UUID 与子块映射 - **核心思路**: 解决 RAG 领域经典的矛盾——检索时块越小越容易精确命中(去除噪声);但生成回答时,块越大越能给大模型提供充足的上下文背景。 - **实现**: - 完全自定义实现,不依赖 LangChain 的 `ParentDocumentRetriever` - 支持异步批量写入 - 支持双向量混合检索 ### Level 3.1: PostgreSQL DocStore 集成 - **核心优势**: 利用 PostgreSQL 作为持久化存储,适合生产环境。使用异步连接池,支持高并发。 - **实现步骤**: 1. **配置连接**: 设置 `DB_URI` 环境变量或通过 `docstore_conn_string` 参数指定 2. **创建 docstore**: 使用 `rag_core.doc_store.create_docstore()` 工厂函数 3. **注入到 IndexBuilder**: 通过构造函数参数注入 ### Level 3.2: 语义切分与父子块策略结合 - **核心优势**: 结合语义切分的连贯性和父子块策略的层次化存储优势,实现更精准的检索和更丰富的上下文。 - **实现原理**: - **父块切分**: 使用 `RecursiveCharacterTextSplitter` 创建大块(约 1000 字符),提供完整的上下文背景 - **子块切分**: 使用 `SemanticChunkerAdapter` 创建小块,根据语义连贯性动态切分,提高检索精度 - **存储机制**: 子块向量存入 Qdrant 用于精准检索,父块内容存入 PostgreSQL 提供完整上下文 ### Level 3.3: 混合检索架构(稠密 + 稀疏) - **核心算法**: Qdrant 原生双向量存储 + RRF 分数融合 - **稠密向量 (Dense)**: 语义相似度检索,捕捉深层含义 - **稀疏向量 (Sparse)**: BM25 关键词检索,精确匹配术语 - **RRF 融合 (Reciprocal Rank Fusion)**: 服务端分数融合,无需客户端后处理 - **核心思路**: 结合语义理解和关键词匹配的双重优势,大幅提升召回率 - **实现原理**: - 每个子文档同时生成 dense 向量和 sparse 向量 - 使用 Qdrant 的 `query_points` API + `Prefetch` 并行检索 - 通过 `FusionQuery` 自动进行 RRF 分数融合 --- ## 📦 存储结构详解 ### 整体数据流向 ``` ┌─────────────────────────────────────────┐ │ 原始文档 │ │ (Document + Metadata) │ └───────────────┬─────────────────────────┘ │ 切分 ┌───────────────▼─────────────────────────┐ │ 父文档块 (Parent Chunks) │ │ 大粒度:1000-2000字符/块 │ │ 存:PostgreSQL JSONB │ └───────────────┬─────────────────────────┘ │ 再切分 ┌───────────────▼─────────────────────────┐ │ 子文档块 (Child Chunks) │ │ 小粒度:200-400字符/块 │ │ 存:Qdrant (稠密+稀疏双向量) │ └─────────────────────────────────────────┘ ``` --- ### PostgreSQL 存储结构(父文档) #### 表结构 ```sql CREATE TABLE parent_documents ( key TEXT PRIMARY KEY, value JSONB NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); ``` #### 数据格式(JSONB) ```json { "page_content": "这是一个父文档块,包含完整的上下文信息,用于最终给 LLM 生成回答...", "metadata": { "source": "file_name.pdf", "page": 10, "chunk_id": "parent-12345", "timestamp": "2024-05-04T12:34:56Z" } } ``` --- ### Qdrant 存储结构(子文档) **集合配置**: - 支持 dense 向量配置:根据嵌入模型输出维度,距离函数使用 Cosine - 支持 sparse 向量配置:BM25 稀疏向量 **点结构(Point)**: - `id`: 子文档唯一标识 - `vector`: 包含 dense 和 sparse 双向量 - `payload`: 包含文本内容、parent_id、来源元数据 --- ## 🔄 完整数据流 ### 索引构建阶段 ``` 原始文档 ↓ 切分为父块(1000字符/块) ↓ 为每个父块分配唯一 ID (parent_id) ↓ 存父块到 PostgreSQL (key=parent_id, value=Document) ↓ 每个父块再切分为子块(200字符/块) ↓ 为每个子块生成: - dense 向量 - sparse 向量 - payload 中加入 parent_id ↓ 存子块到 Qdrant ``` ### 检索阶段 ``` 用户查询 ↓ 生成查询的 dense + sparse 向量 ↓ Qdrant 混合检索(RRF 分数融合) ↓ 得到相关子文档列表 ↓ 收集子文档的 parent_id(去重) ↓ 用 parent_id 批量查询 PostgreSQL ↓ 得到完整的父文档 ↓ 返回给 LLM ``` --- ## 📊 存储消耗分析(估算) 假设我们有 **100 个 PDF 文档,平均每个文档 100,000 字符**,总字符数 10,000,000。 | 存储类型 | 数量 | 单条大小 | 总大小 | |---------|------|---------|--------| | **PostgreSQL 父文档** | ~10,000 块 | 1KB (text) + 0.5KB (metadata) | **15MB** | | **Qdrant 子文档** | ~50,000 块 | 见下文 | **~450-500MB** | ### Qdrant 单条子文档详细分解 | 项 | 说明 | 大小 | |---|-------|------| | dense 向量 | float32[2048] | 8,192 bytes (~8KB) | | sparse 向量 | 平均 50-100 非零维 | 400-800 bytes | | payload | 子文本 + metadata | 200-500 bytes | | **合计** | | **~9-10KB / 条** | 对于 50,000 条子文档:**~450-500MB** --- ## ⚡ 优化策略 ### 1. 分层存储 - **热数据(频繁访问)**:父文档 + 子文档都在 Qdrant(更快) - **冷数据(不常访问)**:父文档在 PostgreSQL,子文档在 Qdrant(更省) ### 2. 向量压缩 - Qdrant 支持 Scalar Quantization (SQ) 或 Product Quantization (PQ) - 可将 dense 向量从 8KB 压缩到 2-4KB,节省 50-75% ### 3. 稀疏向量优化 - BM25 可以剪枝(prune)低权重的词 - 保留 top 50 关键词即可,不用全量 ### 4. 父子块大小调整 - 父块:1000-2000(平衡上下文完整性) - 子块:100-300(平衡检索精度) --- ## ✨ 核心优势总结 | 特性 | 说明 | |------|------| | **检索精度** | 子块小 → 语义更精准 | | **回答质量** | 父块大 → 上下文完整 | | **混合检索** | dense(语义)+ sparse(关键词)= 召回率高 | | **存储效率** | 父子分离 → 不用重复存储大段文本 | ### Level 4: GraphRAG(基于图和关系的 RAG) - **核心算法**: LLM 实体关系抽取 (NER & Relation Extraction)。 - **核心思路**: 解决传统纯向量检索难以处理"跨文档复杂关系推理"的痛点(如:A公司的CEO是谁?他名下的B公司主要业务是什么?这种需要横跨多页 PDF 的跳跃性问题)。 - **实现原理**: 1. **实体提取**: 利用 LLM 从文档中提取实体(如人物、组织、地点、事件等) 2. **关系抽取**: 识别实体之间的关系(如"CEO of"、"founded by"、"located in"等) 3. **图构建**: 将实体作为节点,关系作为边,构建知识图谱 4. **混合检索**: 结合向量检索和图查询,同时利用语义相似性和结构关系 - **技术栈**: - **图数据库**: Neo4j 或 RedisGraph - **LLM 工具**: `LLMGraphTransformer` 或自定义 Prompt - **集成方式**: 与向量存储并行,形成混合检索系统 - **实现指南**: - 使用 `langchain_community.graphs` 模块 - 配置本地大模型(如 `Gemma-4-E4B`)用于实体关系抽取 - 构建包含实体和关系的图结构,存储到图数据库 - 实现混合检索逻辑,结合向量相似度和图路径分析 ```python from langchain_community.graphs import Neo4jGraph from langchain_experimental.graph_transformers import LLMGraphTransformer # 实体关系抽取 transformer = LLMGraphTransformer(llm=local_llm) graph_documents = transformer.convert_to_graph_documents(documents) # 存储到图数据库 graph.add_graph_documents(graph_documents) ``` ### Level 5: 多模态 RAG (Multi-modal RAG) - **核心算法**: 跨模态嵌入和多模态融合。 - **核心思路**: 突破纯文本限制,支持图像、表格、音频等多种数据类型的理解和检索。 - **实现原理**: 1. **多模态嵌入**: 使用 CLIP 等模型将不同模态数据映射到统一向量空间 2. **多模态索引**: 为不同类型的内容创建专用索引 3. **跨模态检索**: 支持以文搜图、以图搜文等跨模态查询 - **技术栈**: - **多模态模型**: CLIP、BLIP 等 - **存储**: 向量数据库 + 对象存储 - **检索**: 混合向量检索 ## 🔧 核心组件详解 ### 1. 文档加载器 (loaders.py) 使用 `unstructured` 库解析多种文件格式。 **支持格式**:PDF、DOCX、DOC、TXT、MD、HTML、PPTX、XLSX、JSON ### 2. 文本切分器 (splitters.py) 提供三种切分策略: **递归字符切分**: - 使用 `SplitterType.RECURSIVE` 类型 - 可配置 `chunk_size` 和 `chunk_overlap` **语义切分**: - 使用 `SplitterType.SEMANTIC` 类型 - 基于句子相似度阈值动态切分 - 需要 Embedding 模型支持 **父子块策略**:在 `IndexBuilder` 中自动配置。 ### 3. 索引构建器 (index_builder.py) 核心编排模块,串联整个索引构建流程。 **主要功能**: - 支持单块切分模式和父子块切分模式 - 自动管理 PostgreSQL 文档存储和 Qdrant 向量存储 - 支持异步批量写入和重试机制 - 提供上下文管理器资源管理 ### 4. 向量存储 (vector_store.py) 封装 Qdrant 向量数据库操作。 **主要功能**: - 创建和管理向量集合 - 支持 dense 和 sparse 双向量写入 - 提供同步和异步客户端 - 自动处理批量操作和重试 ### 5. PostgreSQL DocStore (doc_store.py) 持久化存储父块内容,支持异步连接池。 **主要功能**: - 异步连接池管理 - 文档的增删改查 - 批量操作支持 - UUID 映射管理 ## 📊 切分策略对比 | 策略 | 原理 | 优点 | 缺点 | 适用场景 | |:-----|:-----|:-----|:-----|:---------| | **递归字符** | 按分隔符递归切分 | 速度快,实现简单 | 可能截断语义 | 简单文档 | | **语义切分** | 基于句子相似度阈值 | 语义连贯性好 | 需要 Embedding 模型 | 专业文档 | | **父子块** | 大块存储+小块检索 | 检索精准+上下文完整 | 存储复杂度高 | 生产环境 | ## 🚀 快速开始 ### 命令行方式 使用 `rag_indexer/cli.py` 提供的命令行工具: - `build`: 从文件或目录构建索引 - `clear`: 清空指定 Qdrant 集合 - `reset`: 重置指定 Qdrant 集合 ### Python API 方式 使用 `IndexBuilder` 类进行程序化索引构建: - 配置 `IndexBuilderConfig` 设置切分策略和存储参数 - 使用 `build_from_file()` 从单个文件构建 - 使用 `build_from_directory()` 从目录批量构建 - 推荐使用异步上下文管理器 `async with` 自动管理资源 ## ⚙️ 环境配置 | 变量名 | 说明 | 默认值 | |:-------|:-----|:-------| | `QDRANT_URL` | Qdrant 向量数据库地址 | `http://127.0.0.1:6333` | | `QDRANT_API_KEY` | Qdrant API 密钥 | - | | `DB_HOST` | PostgreSQL 主机 | `127.0.0.1` | | `DB_PORT` | PostgreSQL 端口 | `5432` | | `DB_USER` | PostgreSQL 用户 | `postgres` | | `DB_PASSWORD` | PostgreSQL 密码 | `postgres` | | `DB_NAME` | PostgreSQL 数据库 | `rag_db` | | `LLAMACPP_EMBEDDING_URL` | Embedding 服务地址 | `http://127.0.0.1:18001` | | `LLAMACPP_API_KEY` | llama.cpp API 密钥 | `huang1998` | ## 🔄 与 app/rag 集成 - **向量存储**:共享 Qdrant 集合,确保嵌入模型一致 - **文档存储**:父块存入 PostgreSQL,通过 UUID 与子块关联 - **集合名称**:默认使用 `rag_documents` 集合 - **服务接入**:使用 `model_services` 统一获取嵌入、LLM、重排序服务 详见 [app/rag/README.md](../backend/app/rag/README.md) ## 📝 高级配置 ### 自定义切分参数 `IndexBuilderConfig` 支持以下配置: - `collection_name`: 集合名称 - `splitter_type`: 切分器类型(RECURSIVE/SEMANTIC/PARENT_CHILD) - `parent_chunk_size`: 父块大小(默认 1000) - `child_chunk_size`: 子块大小(默认 200) - `parent_chunk_overlap`: 父块重叠 - `child_chunk_overlap`: 子块重叠 - `child_splitter_type`: 子块切分器类型 - `search_k`: 检索返回数量 ### 批量处理与重试 索引构建器内置自动重试机制,处理网络波动: - 最大重试次数:5 次 - 退避策略:指数退避(2s, 4s, 8s, 16s, 32s) - 批量大小:10 个文档/批次 ### 资源管理 推荐使用异步上下文管理器自动管理资源,也支持手动 `await builder.aclose()` 释放。