Files
ailine/rag_indexer/README.md
root a07e398739
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Failing after 6m34s
refactor!: 完全异步化 RAG 系统,移除 LangChain ParentDocumentRetriever 依赖
- 重写 rag_core/vector_store.py:完全异步实现 aadd_documents、asimilarity_search
- 重写 app/rag/retriever.py:异步混合检索,移除同步兼容代码
- 修改 rag_indexer/index_builder.py:全链路异步调用
- 删除 rag_core/retriever_factory.py:不再使用 LangChain ParentDocumentRetriever
- 清理冗余导入和代码:移除 model_services 兼容、不需要的异常导入
- 更新 rag_indexer/README.md:反映新架构

核心改进:
- 完全异步化:索引构建和检索全链路 async/await
- 自定义实现:不再依赖 LangChain 的 ParentDocumentRetriever
- 双向量支持:子文档同时存储 dense + sparse 向量到 Qdrant
- 架构清晰:rag_core 公共组件、rag_indexer 索引、app/rag 检索
2026-05-04 14:33:12 +08:00

698 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 离线 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 # 文本切分器(递归/语义/父子块)
└── README.md # 本文档
```
```
backend/rag_core/
├── __init__.py
├── vector_store.py # Qdrant 混合存储(异步)
├── sparse_embedder.py # BM25 稀疏嵌入
├── embedders.py # 嵌入模型封装
├── store.py # PostgreSQL 文档存储
├── client.py # Qdrant 同步/异步客户端工厂
└── config.py # 配置管理
```
```
backend/app/rag/
└── retriever.py # 混合检索器(异步)
```
## 🎯 演进路线与核心算法 (Roadmap)
### Level 1: 基础暴力切分 (Basic Recursive Splitting)
- **核心算法**: 递归字符切分。它按照预定义的分隔符列表(如 `["\n\n", "\n", "。", "", "", " ", ""]`)从大到小尝试切分文本,直到每块的大小满足最大长度限制。
- **优缺点**: 实现极简单,速度快。但非常容易将一句话拦腰截断,导致上下文语义丢失。
- **实现指南**:
-`langchain_text_splitters` 导入 `RecursiveCharacterTextSplitter`
- 实例化时设置 `chunk_size`(如 500`chunk_overlap`(如 50
- 直接调用 `.split_documents(raw_docs)` 方法
```python
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "", "", " ", ""]
)
chunks = splitter.split_documents(documents)
```
### Level 2: 语义动态切分 (Semantic Chunking)
- **核心算法**: 句子级相似度阈值算法。
1. 将文章按标点符号按句子拆分
2. 使用轻量级 Embedding 模型将每一句向量化
3. 计算相邻两句之间的余弦相似度 (Cosine Similarity)
4. 当相似度低于设定阈值时(说明两句话讲的不是同一件事,语义发生了转折),在此处"切断"形成一个新的块
- **优缺点**: 极大程度保留了段落内语义的连贯性,对 LLM 回答非常友好。但由于在切分阶段就需要调用向量模型,耗时略长。
- **实现指南**:
-`langchain_experimental.text_splitter` 导入 `SemanticChunker`
- 实现 `SemanticChunkerAdapter` 继承 `TextSplitter`,解决类型不兼容问题
- 实例化时需要传入已配置好的 Embedding 模型实例
```python
from langchain_experimental.text_splitter import SemanticChunker
chunker = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95,
min_chunk_size=100
)
chunks = chunker.split_documents(documents)
```
### Level 3: 高级父子块策略 (Parent-Child / Auto-merging)
- **核心算法**: 层次化双重存储与映射(自定义实现)。
- **切分机制**: 首先将文档粗切为较大的"父块 (Parent Chunk, 约 1000 字符)",随后将父块细切为较小的"子块 (Child Chunk, 约 200 字符)"
- **存储机制**:
- **子块**: 存入 Qdrant同时生成 dense 向量(语义)和 sparse 向量关键词payload 中包含 `parent_id`
- **父块**: 存入 PostgreSQL通过 UUID 与子块映射
- **核心思路**: 解决 RAG 领域经典的矛盾——检索时块越小越容易精确命中(去除噪声);但生成回答时,块越大越能给大模型提供充足的上下文背景。
- **实现**
- 完全自定义实现,不依赖 LangChain 的 `ParentDocumentRetriever`
- 支持异步批量写入
- 支持双向量混合检索
```python
from rag_indexer.index_builder import IndexBuilder, IndexBuilderConfig
from rag_indexer.splitters import SplitterType
config = IndexBuilderConfig(
collection_name="rag_documents",
splitter_type=SplitterType.PARENT_CHILD,
parent_chunk_size=1000,
child_chunk_size=200,
)
builder = IndexBuilder(config)
await builder.build_from_file("document.pdf")
```
### Level 3.1: PostgreSQL DocStore 集成
- **核心优势**: 利用 PostgreSQL 作为持久化存储,适合生产环境。使用异步连接池,支持高并发。
- **实现步骤**:
1. **配置连接**: 设置 `DB_URI` 环境变量或通过 `docstore_conn_string` 参数指定
2. **创建 docstore**: 使用 `rag_indexer.store.create_docstore()` 工厂函数
3. **注入到 IndexBuilder**: 通过构造函数参数注入
```python
from rag_indexer.store import create_docstore
docstore, conn_info = create_docstore(
connection_string="postgresql://user:pass@host:5432/db",
pool_config={"min_size": 5, "max_size": 20},
)
```
### Level 3.2: 语义切分与父子块策略结合
- **核心优势**: 结合语义切分的连贯性和父子块策略的层次化存储优势,实现更精准的检索和更丰富的上下文。
- **实现原理**:
- **父块切分**: 使用 `RecursiveCharacterTextSplitter` 创建大块约1000字符提供完整的上下文背景
- **子块切分**: 使用 `SemanticChunkerAdapter` 创建小块,根据语义连贯性动态切分,提高检索精度
- **存储机制**: 子块向量存入 Qdrant 用于精准检索,父块内容存入 PostgreSQL 提供完整上下文
```python
from rag_indexer.index_builder import IndexBuilder, IndexBuilderConfig
from rag_indexer.splitters import SplitterType
config = IndexBuilderConfig(
collection_name="rag_documents",
splitter_type=SplitterType.PARENT_CHILD,
parent_chunk_size=1000,
child_chunk_size=200,
child_splitter_type=SplitterType.SEMANTIC, # 子块使用语义切分
docstore=DocstoreConfig(
connection_string="postgresql://user:***@host:5432/db",
),
)
```
### Level 3.3: 混合检索架构(稠密 + 稀疏)
- **核心算法**: Qdrant 原生双向量存储 + RRF 分数融合
- **稠密向量 (Dense)**: 语义相似度检索,捕捉深层含义
- **稀疏向量 (Sparse)**: BM25 关键词检索,精确匹配术语
- **RRF 融合 (Reciprocal Rank Fusion)**: 服务端分数融合,无需客户端后处理
- **核心思路**: 结合语义理解和关键词匹配的双重优势,大幅提升召回率
- **实现原理**:
- 每个子文档同时生成 dense 向量和 sparse 向量
- 使用 Qdrant 的 `query_points` API + `Prefetch` 并行检索
- 通过 `FusionQuery` 自动进行 RRF 分数融合
```python
from app.rag.retriever import create_parent_hybrid_retriever
# 创建父子文档混合检索器
retriever = create_parent_hybrid_retriever(
collection_name="rag_documents",
search_k=5
)
# 异步检索相关文档
docs = await retriever.ainvoke("用户查询")
```
---
## 📦 存储结构详解
### 整体数据流向
```
┌─────────────────────────────────────────┐
│ 原始文档 │
│ (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 存储结构(子文档)
#### 集合配置
```python
vectors_config = {
"dense": VectorParams(
size=2048, # 或 1024、4096取决于嵌入模型
distance=Distance.COSINE
)
}
sparse_vectors_config = {
"sparse": SparseVectorParams()
}
```
#### 点结构Point
```json
{
"id": "child-12345",
"vector": {
"dense": [0.123, 0.456, ...],
"sparse": {
"indices": [10, 50, 234, ...],
"values": [0.8, 0.5, 0.3, ...]
}
},
"payload": {
"text": "这是一个子文档块,用于检索...",
"parent_id": "parent-12345",
"source": "file_name.pdf",
"page": 10,
"chunk_index": 0
}
}
```
---
## 🔄 完整数据流
### 索引构建阶段
```
原始文档
切分为父块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
```python
from rag_indexer.loaders import DocumentLoader
loader = DocumentLoader(
strategy="auto", # 解析策略auto/fast/hi_res/ocr_only
ocr_languages=["chi_sim", "eng"], # OCR 语言
languages=["zh"], # 文档主语言
extract_images=False, # 是否提取图片
pdf_infer_table_structure=True, # 是否识别表格
)
# 加载单个文件
docs = loader.load_file("document.pdf")
# 加载整个目录
docs = loader.load_directory("./docs/", recursive=True)
```
### 2. 文本切分器 (splitters.py)
提供三种切分策略:
**递归字符切分**
```python
from rag_indexer.splitters import SplitterType, get_splitter
splitter = get_splitter(
SplitterType.RECURSIVE,
chunk_size=500,
chunk_overlap=50,
)
```
**语义切分**
```python
splitter = get_splitter(
SplitterType.SEMANTIC,
embeddings=embeddings,
breakpoint_threshold_type="percentile",
min_chunk_size=100,
)
```
**父子块策略**:在 `IndexBuilder` 中自动配置。
### 3. 索引构建器 (index_builder.py)
核心编排模块,串联整个索引构建流程。
```python
from rag_indexer.index_builder import IndexBuilder, IndexBuilderConfig, DocstoreConfig
from rag_indexer.splitters import SplitterType
# 配置
config = IndexBuilderConfig(
collection_name="rag_documents",
splitter_type=SplitterType.PARENT_CHILD,
parent_chunk_size=1000,
child_chunk_size=200,
child_splitter_type=SplitterType.SEMANTIC,
docstore=DocstoreConfig(
connection_string="postgresql://user:pass@host:5432/db",
),
)
# 构建索引
async with IndexBuilder(config) as builder:
# 从单个文件构建
count = await builder.build_from_file("document.pdf")
# 或从目录批量构建
count = await builder.build_from_directory("./docs/")
print(f"已索引 {count} 个文档")
```
### 4. 向量存储 (vector_store.py)
封装 Qdrant 向量数据库操作。
```python
from rag_core import QdrantHybridStore
vector_store = QdrantHybridStore(
collection_name="rag_documents",
embeddings=embeddings,
)
# 创建集合
vector_store.create_collection()
# 添加文档
vector_store.add_documents(chunks)
```
### 5. PostgreSQL DocStore (store/postgres.py)
持久化存储父块内容,支持异步连接池。
```python
from rag_core.store import create_docstore
docstore, conn_info = create_docstore(
connection_string="postgresql://user:pass@host:5432/db",
pool_config={"min_size": 5, "max_size": 20},
)
```
## 📊 切分策略对比
| 策略 | 原理 | 优点 | 缺点 | 适用场景 |
|:-----|:-----|:-----|:-----|:---------|
| **递归字符** | 按分隔符递归切分 | 速度快,实现简单 | 可能截断语义 | 简单文档 |
| **语义切分** | 基于句子相似度阈值 | 语义连贯性好 | 需要 Embedding 模型 | 专业文档 |
| **父子块** | 大块存储+小块检索 | 检索精准+上下文完整 | 存储复杂度高 | 生产环境 |
## 🚀 快速开始
### 命令行方式
```bash
# 设置环境变量
export QDRANT_URL="http://115.190.121.151:6333"
export DB_URI="postgresql://postgres:password@host:5432/langgraph_db?sslmode=disable"
# 执行索引构建
python -m rag_indexer.cli --path data/user_docs/ --recursive
```
### Python API 方式
```python
import asyncio
from rag_indexer.index_builder import IndexBuilder, IndexBuilderConfig, DocstoreConfig
from rag_indexer.splitters import SplitterType
async def main():
config = IndexBuilderConfig(
collection_name="rag_documents",
splitter_type=SplitterType.PARENT_CHILD,
parent_chunk_size=1000,
child_chunk_size=200,
child_splitter_type=SplitterType.SEMANTIC,
)
async with IndexBuilder(config) as builder:
count = await builder.build_from_directory("./user_docs/")
print(f"索引构建完成,共处理 {count} 个文档")
asyncio.run(main())
```
## ⚙️ 环境配置
| 变量名 | 说明 | 默认值 |
|:-------|:-----|:-------|
| `QDRANT_URL` | Qdrant 向量数据库地址 | `http://127.0.0.1:6333` |
| `QDRANT_API_KEY` | Qdrant API 密钥 | - |
| `DB_URI` | PostgreSQL 连接字符串 | - |
| `LLAMACPP_EMBEDDING_URL` | Embedding 服务地址 | `http://127.0.0.1:8082/v1` |
| `LLAMACPP_API_KEY` | llama.cpp API 密钥 | - |
## 🔄 与 app/rag 集成
- **向量存储**:共享 Qdrant 集合,确保嵌入模型一致
- **文档存储**:父块存入 PostgreSQL通过 UUID 与子块关联
- **集合名称**:默认使用 `rag_documents` 集合
- **嵌入模型**:使用相同的 `LlamaCppEmbedder` 确保向量空间一致
详见 [app/rag/README.md](../app/rag/README.md)
## 📝 高级配置
### 自定义切分参数
```python
config = IndexBuilderConfig(
collection_name="my_docs",
splitter_type=SplitterType.PARENT_CHILD,
parent_chunk_size=1500, # 更大的父块
child_chunk_size=300, # 更大的子块
parent_chunk_overlap=150, # 父块重叠
child_chunk_overlap=30, # 子块重叠
search_k=10, # 检索返回数量
)
```
### 批量处理与重试
索引构建器内置自动重试机制,处理网络波动:
- 最大重试次数5 次
- 退避策略指数退避2s, 4s, 8s, 16s, 32s
- 批量大小10 个文档/批次
### 资源管理
```python
# 方式一:上下文管理器(推荐)
async with IndexBuilder(config) as builder:
await builder.build_from_directory("./docs/")
# 方式二:手动管理
builder = IndexBuilder(config)
try:
await builder.build_from_directory("./docs/")
finally:
await builder.aclose()
```