From 60ac3a5ed1e7079183c3e6f631c4f599c5f76190 Mon Sep 17 00:00:00 2001 From: earayu Date: Fri, 24 Apr 2026 22:40:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20task=20#18=20Phase=202=20=E2=80=94=20de?= =?UTF-8?q?lete=2011=20obsolete=20zh-CN=20design=20source=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per PM task #18 + PR #1637 Section 4 triage + Bryce PR #1643 + huangheng PR #1644 both merged。 ### 对应 target merge 完成触发的 Phase 2 delete - `authentication.md` → task #10 `architecture/identity-governance-model-platform-marketplace.md` (PR #1643 `c11b0ad8`) - `quota-system-design.md` → task #10 architecture + task #15 admin-guide (PR #1643) - `collection_marketplace_design.md` → task #10 architecture + task #15 user-guide (PR #1643) - `indexing_architecture.md` → task #11 `architecture/indexing-retrieval-kg.md` (PR #1644 `618b91a9`) - `vector_db_abstraction.md` → task #11 (PR #1644) - `search_flow_design.md` → task #11 (PR #1644) - `vision_index_creation.md` → task #11 (PR #1644) - `graph_curation.md` → task #11 (PR #1644) - `graph_index_creation.md` → task #11 (PR #1644) - `lightrag_entity_extraction_and_merging.md` → task #11 (PR #1644) - `web-search-design.md` → task #12 `architecture/web-access.md` (PR #1643) ### 顺带清理 - `docs/zh-CN/design/_category.yaml` — design/ 目录现已空,category yaml 废弃一并删除 ### task #18 完成状态 Phase 1 (PR #1642 `c75a739b`): en-US + web/docs + 22 source docs deleted Phase 2 (本 PR): 最后 11 source docs + empty design/_category.yaml = 12 files task #18 合入即可 → Done。 Co-Authored-By: Claude Opus 4.7 --- docs/zh-CN/design/_category.yaml | 2 - docs/zh-CN/design/authentication.md | 502 ------- .../design/collection_marketplace_design.md | 1303 ----------------- docs/zh-CN/design/graph_curation.md | 373 ----- docs/zh-CN/design/graph_index_creation.md | 1084 -------------- docs/zh-CN/design/indexing_architecture.md | 556 ------- .../lightrag_entity_extraction_and_merging.md | 696 --------- docs/zh-CN/design/quota-system-design.md | 482 ------ docs/zh-CN/design/search_flow_design.md | 654 --------- docs/zh-CN/design/vector_db_abstraction.md | 435 ------ docs/zh-CN/design/vision_index_creation.md | 261 ---- docs/zh-CN/design/web-search-design.md | 740 ---------- 12 files changed, 7088 deletions(-) delete mode 100644 docs/zh-CN/design/_category.yaml delete mode 100644 docs/zh-CN/design/authentication.md delete mode 100644 docs/zh-CN/design/collection_marketplace_design.md delete mode 100644 docs/zh-CN/design/graph_curation.md delete mode 100644 docs/zh-CN/design/graph_index_creation.md delete mode 100644 docs/zh-CN/design/indexing_architecture.md delete mode 100644 docs/zh-CN/design/lightrag_entity_extraction_and_merging.md delete mode 100644 docs/zh-CN/design/quota-system-design.md delete mode 100644 docs/zh-CN/design/search_flow_design.md delete mode 100644 docs/zh-CN/design/vector_db_abstraction.md delete mode 100644 docs/zh-CN/design/vision_index_creation.md delete mode 100644 docs/zh-CN/design/web-search-design.md diff --git a/docs/zh-CN/design/_category.yaml b/docs/zh-CN/design/_category.yaml deleted file mode 100644 index 1211298a0..000000000 --- a/docs/zh-CN/design/_category.yaml +++ /dev/null @@ -1,2 +0,0 @@ -title: 设计 -position: 1 diff --git a/docs/zh-CN/design/authentication.md b/docs/zh-CN/design/authentication.md deleted file mode 100644 index 4b50d66cb..000000000 --- a/docs/zh-CN/design/authentication.md +++ /dev/null @@ -1,502 +0,0 @@ -# ApeRAG 认证系统架构文档 - -## 概述 - -ApeRAG 采用基于 Cookie 的认证系统,支持本地用户名/密码认证和 OAuth2 社交登录(GitHub、Google)。系统基于 FastAPI-Users 库构建,提供完整的用户管理和认证功能。 - -## 核心架构 - -### 技术栈 -- **后端**: FastAPI + FastAPI-Users + SQLAlchemy + PostgreSQL -- **前端**: React + TypeScript + Ant Design + UmiJS -- **认证**: JWT + HttpOnly Cookie + OAuth 2.0 -- **安全**: bcrypt 密码加密 + CSRF 保护 - -### 认证方式 -1. **本地认证**: 用户名/密码登录 -2. **OAuth 社交登录**: GitHub 和 Google 第三方登录 -3. **API Key 认证**: 用于程序化访问 - -## 数据模型 - -### 用户表 (User) -```python -class User(Base): - id: str # 用户ID - username: str # 用户名(唯一) - email: str # 邮箱(唯一) - hashed_password: str # bcrypt加密密码 - role: Role # 用户角色(ADMIN/RW/RO) - is_active: bool # 是否激活 - is_verified: bool # 是否验证 - date_joined: datetime # 注册时间 -``` - -### OAuth账户表 (OAuthAccount) -```python -class OAuthAccount(Base): - id: str # OAuth账户ID - user_id: str # 关联用户ID - oauth_name: str # OAuth提供商名称 - account_id: str # 第三方账户ID - account_email: str # 第三方账户邮箱 - access_token: str # 访问令牌 -``` - -### API密钥表 (ApiKey) -```python -class ApiKey(Base): - id: str # API密钥ID - key: str # API密钥值 - user: str # 关联用户ID - description: str # 描述 - status: ApiKeyStatus # 状态(ACTIVE/DELETED) - is_system: bool # 是否系统生成 - last_used_at: datetime # 最后使用时间 -``` - -## 认证流程 - -### 1. 本地认证流程 - -```mermaid -sequenceDiagram - participant U as 用户 - participant F as 前端 - participant B as 后端 - participant D as 数据库 - - U->>F: 输入用户名/密码 - F->>B: POST /api/v1/login - B->>D: 验证用户凭据 - D-->>B: 返回用户信息 - B->>B: 生成JWT令牌 - B->>F: 设置HttpOnly Cookie - F->>U: 跳转到主页面 -``` - -### 2. OAuth认证流程 - -```mermaid -sequenceDiagram - participant U as 用户 - participant F as 前端 - participant B as 后端 - participant G as GitHub/Google - participant D as 数据库 - - U->>F: 点击OAuth登录 - F->>B: GET /api/v1/auth/{provider}/authorize - B->>G: 重定向到OAuth授权页面 - G->>U: 显示授权页面 - U->>G: 确认授权 - G->>F: 重定向到回调URL (带code) - F->>B: GET /api/v1/auth/{provider}/callback - B->>G: 使用code换取access_token - G-->>B: 返回用户信息 - B->>D: 查找或创建用户账户 - B->>F: 设置认证Cookie (204 No Content) - F->>U: 跳转到主页面 -``` - -参考:https://github.com/fastapi-users/fastapi-users/issues/434 - -#### OAuth API说明 - -OAuth认证涉及两个关键API,这些API由FastAPI-Users自动生成: - -1. **授权端点** (`/api/v1/auth/{provider}/authorize`) - - 生成OAuth授权URL - - 包含state参数防止CSRF攻击 - - 重定向用户到第三方OAuth提供商 - -2. **回调端点** (`/api/v1/auth/{provider}/callback`) - - 处理OAuth提供商的回调请求 - - 使用authorization code换取access token - - 获取用户信息并创建/登录用户 - - 设置认证Cookie并返回204 No Content - -## 核心组件 - -### 1. FastAPI-Users 配置 - -#### JWT策略 -```python -COOKIE_MAX_AGE = 86400 # 24小时 - -def get_jwt_strategy() -> JWTStrategy: - return JWTStrategy(secret=settings.jwt_secret, lifetime_seconds=COOKIE_MAX_AGE) -``` - -#### Cookie传输 -```python -cookie_transport = CookieTransport( - cookie_name="session", - cookie_max_age=COOKIE_MAX_AGE, - cookie_secure=False, # 开发环境设为False - cookie_httponly=True, # 防止XSS攻击 - cookie_samesite="lax" # 防止CSRF攻击 -) -``` - -#### 认证后端 -```python -auth_backend = AuthenticationBackend( - name="cookie", - transport=cookie_transport, - get_strategy=get_jwt_strategy, -) -``` - -### 2. 用户管理器 -```python -class UserManager(BaseUserManager[User, str]): - async def on_after_register(self, user: User, request: Optional[Request] = None): - # 设置第一个注册用户为管理员 - user_count = await async_db_ops.query_user_count() - if user_count == 1 and user.role != Role.ADMIN: - user.role = Role.ADMIN -``` - -### 3. OAuth客户端配置 -```python -# GitHub OAuth -if is_github_oauth_enabled(): - github_oauth_client = GitHubOAuth2( - settings.github_oauth_client_id, - settings.github_oauth_client_secret - ) - github_oauth_router = get_oauth_router( - github_oauth_client, - auth_backend, - get_user_manager, - settings.jwt_secret, - redirect_url=settings.oauth_redirect_url, # 回调URL配置 - associate_by_email=True, # 通过邮箱关联账户 - is_verified_by_default=True, # 默认验证用户 - ) -``` - -#### OAuth路由生成 -FastAPI-Users的`get_oauth_router`函数会自动生成以下路由: -- `GET /auth/{provider}/authorize` - 获取授权URL -- `GET /auth/{provider}/callback` - 处理OAuth回调 - -## API接口 - -### 认证相关接口 - -#### 1. 获取配置信息 -```http -GET /api/v1/config -``` -**响应**: 包含可用登录方式的配置信息 - -#### 2. 本地登录 -```http -POST /api/v1/login -Content-Type: application/json - -{ - "username": "user@example.com", - "password": "password123" -} -``` -**响应**: 用户信息 + 设置session cookie - -#### 3. 用户注册 -```http -POST /api/v1/register -Content-Type: application/json - -{ - "username": "newuser", - "email": "user@example.com", - "password": "password123", - "token": "invitation_token" // 邀请制时必需 -} -``` - -#### 4. 登出 -```http -POST /api/v1/logout -``` -**响应**: 清除session cookie - -#### 5. 获取当前用户 -```http -GET /api/v1/user -Cookie: session=jwt_token -``` - -#### 6. 修改密码 -```http -POST /api/v1/change-password -Content-Type: application/json - -{ - "username": "user@example.com", - "old_password": "old_password", - "new_password": "new_password" -} -``` - -### OAuth接口 - -#### 1. OAuth授权 -```http -GET /api/v1/auth/{provider}/authorize -``` -**响应**: -```json -{ - "authorization_url": "https://github.com/login/oauth/authorize?..." -} -``` - -#### 2. OAuth回调 -```http -GET /api/v1/auth/{provider}/callback?code=xxx&state=yyy -``` -**响应**: 204 No Content + 设置认证Cookie - -**注意**: 这两个OAuth API由FastAPI-Users自动生成,无需手动实现。 - -### 用户管理接口 - -#### 1. 列出用户 -```http -GET /api/v1/users -``` - -#### 2. 删除用户 -```http -DELETE /api/v1/users/{user_id} -``` - -## 前端实现 - -### 1. 登录页面 (`signin.tsx`) - -#### 核心功能 -- 动态获取可用登录方式 -- 本地登录表单处理 -- OAuth登录按钮处理 - -#### OAuth登录实现 -```typescript -// GitHub登录 -onClick={async () => { - try { - localStorage.setItem('oauth_provider', 'github'); - const response = await fetch('/api/v1/auth/github/authorize'); - const data = await response.json(); - if (data.authorization_url) { - window.location.href = data.authorization_url; - } - } catch (error) { - console.error('GitHub OAuth error:', error); - } -}} -``` - -### 2. OAuth回调页面 (`oauth-callback.tsx`) - -#### 核心功能 -- 解析URL参数(code、state等) -- 确定OAuth提供商 -- 调用后端回调接口 -- 处理认证结果 - -#### 实现逻辑 -```typescript -const handleOAuth = async () => { - // 获取OAuth参数 - const code = searchParams.get('code'); - const state = searchParams.get('state'); - - // 确定提供商 - let provider = localStorage.getItem('oauth_provider') || 'github'; - - // 调用回调接口 - const callbackUrl = `/api/v1/auth/${provider}/callback?code=${code}&state=${state}`; - const response = await fetch(callbackUrl, { - method: 'GET', - credentials: 'include', - }); - - // 处理响应 - if (response.status === 204) { - navigate('/'); // 认证成功 - } -}; -``` - -## 认证中间件 - -### 1. 当前用户获取 -```python -async def current_user( - request: Request, - session: AsyncSessionDep, - user: User = Depends(fastapi_users.current_user(optional=True)) -) -> Optional[User]: - # 优先使用JWT/Cookie认证 - if user: - return user - - # 回退到API Key认证 - api_user = await authenticate_api_key(request, session) - if api_user: - return api_user - - return None -``` - -### 2. API Key认证 -```python -async def authenticate_api_key(request: Request, session: AsyncSessionDep) -> Optional[User]: - authorization = request.headers.get("Authorization") - if not authorization or not authorization.startswith("Bearer "): - return None - - api_key = authorization.split(" ")[1] - # 查找并验证API Key - # 更新最后使用时间 - # 返回关联用户 -``` - -## 配置说明 - -### 环境变量 -```bash -# JWT密钥 -JWT_SECRET=your-super-secret-key - -# OAuth回调URL -OAUTH_REDIRECT_URL=http://127.0.0.1:3000/web/oauth-callback - -# GitHub OAuth -GITHUB_OAUTH_CLIENT_ID=your-github-client-id -GITHUB_OAUTH_CLIENT_SECRET=your-github-client-secret - -# Google OAuth -GOOGLE_OAUTH_CLIENT_ID=your-google-client-id -GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret - -# 注册模式 -REGISTER_MODE=invitation # unlimited/invitation -``` - -### OAuth应用配置 - -OAuth提供商需要配置以下关键信息: - -#### 必需配置项 -- **Client ID**: OAuth应用的客户端标识符 -- **Client Secret**: OAuth应用的客户端密钥 -- **Callback URL**: OAuth授权完成后的回调地址 -- **Scopes**: 请求的权限范围(通常包含用户基本信息和邮箱) - -#### GitHub OAuth应用配置 -1. 访问 [GitHub Developer Settings](https://github.com/settings/developers) -2. 点击 "New OAuth App" 创建新应用 -3. 填写应用信息: - - **Application name**: ApeRAG - - **Homepage URL**: `http://127.0.0.1:3000` - - **Authorization callback URL**: `http://127.0.0.1:3000/web/oauth-callback` -4. 创建后获取 Client ID 和 Client Secret -5. 默认权限范围: `user:email`(获取用户基本信息和邮箱) - -#### Google OAuth应用配置 -1. 访问 [Google Cloud Console](https://console.cloud.google.com/) -2. 创建项目或选择现有项目 -3. 启用 Google+ API 或 Google People API -4. 创建 OAuth 2.0 客户端ID: - - 应用类型: Web应用 - - 授权重定向URI: `http://127.0.0.1:3000/web/oauth-callback` -5. 获取客户端ID和客户端密钥 -6. 默认权限范围: `openid email profile`(获取用户基本信息) - -#### 回调URL说明 -- 回调URL必须与OAuth应用配置中的完全一致 -- 开发环境: `http://127.0.0.1:3000/web/oauth-callback` -- 生产环境: `https://yourdomain.com/web/oauth-callback` -- ApeRAG前端的BASE_PATH是`/web`,所以回调URL包含此前缀 - -## 安全特性 - -### 1. JWT令牌安全 -- 使用强密钥签名(HMAC-SHA256) -- 24小时有效期 -- HttpOnly Cookie传输,防止XSS -- SameSite=Lax,防止CSRF - -### 2. 密码安全 -- bcrypt加密存储 -- 随机盐值 -- 密码强度验证 - -### 3. OAuth安全 -- State参数防CSRF -- 标准授权码流程 -- 令牌安全存储 - -### 4. API Key安全 -- 随机生成(sk-前缀) -- 使用跟踪 -- 状态管理 - -## 权限控制 - -### 用户角色 -- **ADMIN**: 管理员,拥有所有权限 -- **RW**: 读写用户,可创建和修改资源 -- **RO**: 只读用户,仅可查看资源 - -### 权限检查 -```python -async def get_current_admin(user: User = Depends(get_current_active_user)) -> User: - if user.role != Role.ADMIN: - raise HTTPException(status_code=403, detail="Only admin members can perform this action") - return user -``` - -## 注册模式 - -### 1. 开放注册 (unlimited) -- 任何人都可以直接注册 -- 第一个注册用户自动成为管理员 - -### 2. 邀请制注册 (invitation) -- 需要管理员发送邀请 -- 通过邀请令牌验证 - -## 故障排除 - -### 常见问题 - -#### 1. OAuth回调失败 -- 检查回调URL配置是否匹配 -- 验证OAuth应用配置 -- 查看浏览器控制台日志 - -#### 2. Cookie认证失败 -- 检查JWT_SECRET配置 -- 验证Cookie域名设置 -- 确认浏览器Cookie策略 - -#### 3. API Key认证失败 -- 验证Authorization头格式: `Bearer sk-xxx` -- 检查API Key状态 -- 确认用户关联关系 - -### 调试方法 -1. 查看后端日志: `tail -f logs/aperag.log` -2. 检查浏览器开发者工具 -3. 验证数据库用户和OAuth账户数据 -4. 测试API接口响应 - -## 总结 - -ApeRAG认证系统基于FastAPI-Users构建,提供了安全、灵活的多种认证方式。系统支持本地认证和OAuth社交登录,采用JWT+Cookie的无状态认证机制,具有良好的安全性和扩展性。通过合理的权限控制和注册模式配置,能够满足不同场景的使用需求。 diff --git a/docs/zh-CN/design/collection_marketplace_design.md b/docs/zh-CN/design/collection_marketplace_design.md deleted file mode 100644 index 71647228e..000000000 --- a/docs/zh-CN/design/collection_marketplace_design.md +++ /dev/null @@ -1,1303 +0,0 @@ -## 设计文档:Collection 分享与市场 (MVP) - -**版本:** 1.6 -**关联 Issue:** [#1127](https://github.com/apecloud/ApeRAG/issues/1127) - -### 1. 概述 - -本文档旨在为 ApeRAG 设计并实现一个 Collection(知识库)分享与市场的最小可行产品 (MVP)。核心目标是允许用户将自己的 Collection 发布到一个公共市场,其他用户可以发现并以**严格只读**的模式访问这些共享的 Collection。 - -MVP 阶段将专注于实现最核心的发布、浏览和只读访问流程,省略复杂的审核、分类、评级、统计分析、用户评价、热度排序、专门的订阅管理页面等功能,以便快速验证核心价值。 - -**核心功能范围:** -- Collection 所有者可以发布和取消发布自己的 Collection -- 已发布的 Collection 出现在公共市场页面供所有用户浏览 -- 非所有者用户可以以严格只读模式访问已发布的 Collection -- 只读模式包括:查看文档列表、阅读文档内容、浏览知识图谱、使用聊天机器人搜索 -- 只读模式禁止:添加/删除/修改文档、修改 Collection 设置、任何写操作 - -### 2. 数据库 Schema 设计 - -基于 Subscribe 模式的需求,我们需要新增两个表来支持 Collection 分享和用户订阅功能。 - -#### 2.1. 新增表设计 - -**表1: `collection_marketplace` - Collection 分享状态表** - -用于记录 Collection 的分享状态和发布信息。 - -```sql -CREATE TABLE collection_marketplace ( - id VARCHAR(24) PRIMARY KEY DEFAULT ('market_' || substr(md5(random()::text), 1, 16)), - collection_id VARCHAR(24) NOT NULL, -- 关联collections表,应用层维护关联关系 - - -- 分享状态:使用VARCHAR存储,不使用数据库enum类型,应用层校验 - status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', - - -- 时间戳字段 - gmt_created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - gmt_updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), -- 代码层更新 - gmt_deleted TIMESTAMP WITH TIME ZONE NULL, - - -- 约束 - CONSTRAINT uq_collection_marketplace_collection UNIQUE (collection_id) -); - --- 注意:gmt_updated字段需要在应用代码中手动更新 --- 在SQLModel中更新记录时,手动设置: gmt_updated = datetime.utcnow() - --- 索引优化 -CREATE INDEX idx_collection_marketplace_status ON collection_marketplace(status); -CREATE INDEX idx_collection_marketplace_gmt_deleted ON collection_marketplace(gmt_deleted); -CREATE INDEX idx_collection_marketplace_collection_id ON collection_marketplace(collection_id); --- 查询市场列表时的复合索引 -CREATE INDEX idx_collection_marketplace_list ON collection_marketplace(status, gmt_created DESC); -``` - -**表2: `user_collection_subscription` - 用户订阅表** - -用于记录用户对已发布 Collection 的订阅关系,采用 Subscribe 模式。 - -```sql -CREATE TABLE user_collection_subscription ( - id VARCHAR(24) PRIMARY KEY DEFAULT ('sub_' || substr(md5(random()::text), 1, 16)), - user_id VARCHAR(24) NOT NULL, -- 关联users表,应用层维护关联关系 - collection_marketplace_id VARCHAR(24) NOT NULL, -- 关联collection_marketplace表,应用层维护关联关系 - - -- 时间戳字段 - gmt_subscribed TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - gmt_deleted TIMESTAMP WITH TIME ZONE NULL, -- 软删除:NULL表示活跃订阅 - - -- 注意:活跃订阅的唯一性通过复合唯一索引实现,包含gmt_deleted字段 - -- 级联删除逻辑需要在应用代码中处理,删除Collection时同时删除相关订阅记录 -); - --- 索引优化 -CREATE UNIQUE INDEX idx_user_marketplace_history_unique ON user_collection_subscription(user_id, collection_marketplace_id, gmt_deleted); -- 允许多条历史记录,但活跃订阅(gmt_deleted=NULL)保持唯一 -CREATE INDEX idx_user_subscription_marketplace ON user_collection_subscription(collection_marketplace_id); -CREATE INDEX idx_user_subscription_user ON user_collection_subscription(user_id); -CREATE INDEX idx_user_subscription_gmt_deleted ON user_collection_subscription(gmt_deleted); -``` - -#### 2.2. 数据库约束说明 - -**业务约束:** -1. **唯一性约束**: 每个 Collection 只能有一条分享记录 -2. **订阅约束**: 一个用户对同一发布实例(collection_marketplace)只能有一个活跃订阅,但可以保留多条历史记录 -3. **所有权约束**: 用户无法订阅自己是所有者的 Collection(业务逻辑禁止) -4. **应用层级联**: Collection 删除时,需要在代码中同时软删除相关的分享和订阅记录 -5. **状态检查**: 分享状态只能是 'DRAFT' 或 'PUBLISHED'(应用层校验) -6. **发布状态绑定**: 订阅关系与Collection的发布状态绑定,Collection取消发布时订阅失效,重新发布时需要重新订阅 - -**性能优化:** -1. **简单索引策略**: 使用常规索引,与项目现有表保持一致,降低维护复杂度 -2. **复合索引**: - - 用户+Collection+删除状态组合查询优化(订阅检查场景) - - 状态+时间复合索引(市场列表查询场景) - - Collection关联查询索引(按collection_id查询优化) -3. **应用层更新**: `gmt_updated` 字段在代码中手动更新,保持项目一致性 -4. **数据规范化**: 遵循项目惯例,不使用外键约束,通过应用层维护数据一致性 - -#### 2.3. 数据生命周期 - -**分享生命周期:** -- **创建**: 用户首次发布 Collection 时创建记录,状态为 'PUBLISHED' -- **取消发布**: 将 collection_marketplace 记录状态改为 'DRAFT'(保留记录),应用层处理相关订阅失效 -- **重新发布**: 将现有 collection_marketplace 记录状态改回 'PUBLISHED',之前的订阅关系不会自动恢复 -- **删除处理**: Collection 删除时,需要在代码中同时软删除 collection_marketplace 记录 - -**订阅生命周期:** -- **订阅**: 用户订阅已发布的 Collection,创建订阅记录(关联到具体的发布实例) -- **取消订阅**: 设置 `gmt_deleted = NOW()`,保留历史记录 -- **自动失效**: Collection 取消发布时,collection_marketplace 记录状态改为 'DRAFT',应用层查询并软删除相关订阅记录 -- **级联删除**: Collection 删除时,在代码中批量软删除相关的 marketplace 和订阅记录 - -### 3. 系统架构与业务流程 - -#### 3.1. 技术架构图 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 前端 (UmiJS + React) │ -├─────────────────────────────────────────────────────────────────┤ -│ /marketplace │ /collections │ /collections/{collection_id} │ -│ (市场浏览页面) │ (统一工作台) │ (Collection详情, 区分owner/订阅者) │ -└─────────────────────────────────────────────────────────────────┘ - │ - │ HTTP/HTTPS - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 后端 API (FastAPI) │ -├─────────────────────────────────────────────────────────────────┤ -│ MarketplaceView │ CollectionView │ -│ - 市场Collection列表 │ - Collection CRUD API │ -│ - 订阅/取消订阅API │ - 发布/取消发布API │ -│ - 用户订阅列表API │ - 分享状态查询API │ -│ │ - 权限控制集成 │ -└─────────────────────────────────────────────────────────────────┘ - │ - │ Service Layer - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 服务层 (Business Logic) │ -├─────────────────────────────────────────────────────────────────┤ -│ MarketplaceService │ CollectionService │ -│ - 发布/取消发布 │ - 现有CRUD操作 (保持不变) │ -│ - 订阅/取消订阅 │ - 添加分享状态字段 │ -│ - 用户订阅列表 │ │ -│ - 市场Collection列表 │ MarketplaceCollectionService │ -│ - 分享状态查询 │ - 订阅权限检查 │ -└─────────────────────────────────────────────────────────────────┘ - │ - │ Database Layer - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ 数据库层 (PostgreSQL) │ -├─────────────────────────────────────────────────────────────────┤ -│ collections │ collection_marketplace │ -│ - 原有Collection数据 │ - 分享状态表 │ -│ │ │ -│ user_collection_subscription │ -│ - 用户订阅关系表 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -#### 3.2. 核心业务流程 - -**流程1: Collection 发布流程** -``` -用户A (Collection所有者) - │ - ├─ 1. POST /api/v1/collections/{collection_id}/sharing - │ │ - │ ├─ 验证用户身份和所有权 - │ ├─ 创建/更新 collection_marketplace 记录 - │ └─ 状态设置为 'PUBLISHED' - │ - └─ 2. Collection 出现在市场列表 - │ - └─ 其他用户可以在 /marketplace 看到 -``` - -**流程2: 用户订阅流程** -``` -用户B (非所有者) - │ - ├─ 1. 浏览市场 (GET /api/v1/marketplace/collections) - │ │ - │ └─ 看到用户A发布的Collection - │ - ├─ 2. 点击订阅 (POST /api/v1/marketplace/collections/{collection_id}/subscribe) - │ │ - │ ├─ 验证Collection已发布 - │ ├─ 验证用户不是Collection所有者(防止订阅自己的Collection) - │ ├─ 检查是否已订阅 - │ ├─ 创建 user_collection_subscription 记录 - │ └─ 返回订阅成功,自动跳转到Collection详情页 - │ - └─ 3. 访问订阅的Collection内容 - │ - ├─ 3a. 在Collection列表页面查看订阅的Collection - │ │ - │ ├─ 页面: /collections (主Collection列表页面) - │ ├─ API调用: - │ │ ├─ GET /api/v1/collections (获取自有Collection) - │ │ └─ GET /api/v1/marketplace/collections/subscriptions (获取订阅Collection) - │ ├─ 前端合并: 两个接口响应合并显示在同一页面 - │ ├─ 区分显示: 订阅Collection显示"已订阅"标签,自有Collection显示"我的"标签 - │ └─ 点击进入: 智能路由(根据用户关系跳转到不同页面) - │ - - └─ 3b. MarketplaceCollection详情页只读访问 - │ - ├─ 页面: /marketplace/collections/{collection_id} - ├─ API: GET /api/v1/marketplace/collections/{collection_id} - ├─ 权限检查: _check_subscription_access() 验证订阅状态 - ├─ 响应类型: SharedCollection(专用只读接口) - ├─ UI显示: 顶部显示只读Banner - ├─ 功能权限: 可查看文档、图谱,可使用聊天Bot - └─ 操作限制: 隐藏所有编辑、删除、上传按钮 -``` - -**流程3: MarketplaceCollection接口权限检查流程(新增)** -``` -用户请求访问 /api/v1/marketplace/collections/{id} - │ - └─ _check_subscription_access() - │ - ├─ 检查Collection是否存在且已发布 - │ └─ 否 → 403 "Collection no longer available" ❌ - │ - ├─ 检查用户是否已订阅 (gmt_deleted IS NULL) - │ └─ 否 → 403 "Need to subscribe first" ❌ - │ - └─ 是 → 只读访问权限 ✅ -``` - -**流程4: 用户取消订阅流程** -``` -用户B (已订阅用户) - │ - ├─ 1. 在MarketplaceCollection详情页点击"取消订阅" - │ │ - │ ├─ 页面: /marketplace/collections/{collection_id} - │ ├─ UI元素: 详情页面显示"取消订阅"按钮 - │ └─ 确认对话框: "确定要取消订阅此知识库吗?" - │ - ├─ 2. 执行取消订阅 (DELETE /api/v1/marketplace/collections/{collection_id}/subscribe) - │ │ - │ ├─ 验证用户身份和订阅状态 - │ ├─ 验证用户确实已订阅该Collection (gmt_deleted IS NULL) - │ ├─ 软删除订阅记录 (设置 gmt_deleted = current_timestamp) - │ └─ 返回取消成功响应 - │ - ├─ 3. 立即失去访问权限 - │ │ - │ ├─ 权限检查: marketplace/collections接口的 _check_subscription_access() 立即返回403 - │ ├─ 前端处理: 自动跳转到市场页面或首页 - │ └─ 提示消息: "已成功取消订阅" - │ - └─ 4. Collection从用户工作区移除 - │ - ├─ API影响: GET /api/v1/marketplace/collections/subscriptions 不再返回该Collection - ├─ 前端更新: Collection列表页面不再显示该Collection - ├─ 重新订阅: 用户可以在市场页面重新订阅 - └─ 历史保留: 数据库保留订阅历史记录(便于审计) -``` - -**流程5: Collection取消发布流程** -``` -用户A (Collection所有者) - │ - ├─ 1. 在Collection详情页点击"取消发布" - │ │ - │ ├─ 页面: /collections/{collection_id} - │ ├─ UI元素: 分享控制组件显示"取消发布"按钮 - │ ├─ 确认对话框: "取消发布后,所有订阅用户将失去访问权限,确定继续吗?" - │ └─ 风险提示: 显示当前订阅用户数量 - │ - ├─ 2. 执行取消发布 (DELETE /api/v1/collections/{collection_id}/sharing) - │ │ - │ ├─ 验证用户身份和所有权 - │ ├─ 将 collection_marketplace 记录状态改为 'DRAFT'(设置 status='DRAFT', gmt_updated=current_timestamp) - │ ├─ 应用层查询并软删除相关订阅记录(设置 gmt_deleted) - │ └─ 返回取消发布成功响应 - │ - ├─ 3. 立即从市场移除 - │ │ - │ ├─ 市场API: GET /api/v1/marketplace/collections 不再返回该Collection - │ ├─ 搜索结果: 市场搜索无法找到该Collection - │ └─ 直接访问: 非所有者访问将返回403 "Collection not published" - │ - ├─ 4. 所有订阅用户失去访问权限 - │ │ - │ ├─ 权限检查: marketplace/collections接口的 _check_subscription_access() 对所有订阅用户返回403 - │ ├─ 活跃连接: 正在使用的用户会在下次请求时收到403错误 - │ ├─ 前端处理: 订阅用户的Collection列表自动移除该项 - │ └─ 通知机制: (可选) 向订阅用户发送取消发布通知 - │ - └─ 5. 重新发布支持 - │ - ├─ 状态恢复: 所有者可以重新发布 (POST /api/v1/collections/{collection_id}/sharing) - ├─ 订阅恢复: 重新发布后不会自动恢复之前的订阅关系 - ├─ 用户重新订阅: 之前的订阅用户需要重新手动订阅 - └─ 历史记录: 保留所有发布/取消发布的历史记录 -``` - -#### 3.3. 安全设计 - -**权限控制策略:** -1. **严格的所有权验证**: 只有Collection所有者可以发布/取消发布 -2. **订阅前置检查**: 非所有者必须订阅才能访问内容 -3. **只读强制执行**: 订阅用户无法进行任何写操作 -4. **自动权限回收**: 取消发布时自动失效所有订阅 - -**数据安全:** -1. **级联删除**: Collection删除时自动清理相关记录 -2. **软删除审计**: 保留订阅历史记录便于审计 -3. **状态一致性**: 通过事务确保分享状态和订阅状态一致 - -#### 3.4. 性能考虑 - -**数据库优化:** -1. **索引策略**: 为高频查询场景创建专门索引,使用简单一致的索引设计 -2. **分页查询**: 所有列表接口支持分页,避免大数据量查询 -3. **复合索引**: 针对多字段查询场景创建复合索引,提升查询效率 - - -**查询优化:** -```sql --- 高效的市场列表查询(利用复合索引 idx_collection_marketplace_list) -SELECT cm.id, c.title, c.description, u.username, cm.gmt_created -FROM collection_marketplace cm -JOIN collections c ON cm.collection_id = c.id -JOIN users u ON c.user_id = u.id -WHERE cm.status = 'PUBLISHED' AND cm.gmt_deleted IS NULL -ORDER BY cm.gmt_created DESC -LIMIT 12 OFFSET ?; - --- 高效的订阅检查查询(利用唯一索引 idx_user_marketplace_active_unique) -SELECT ucs.id FROM user_collection_subscription ucs -JOIN collection_marketplace cm ON ucs.collection_marketplace_id = cm.id -WHERE ucs.user_id = ? AND cm.collection_id = ? AND ucs.gmt_deleted IS NULL AND cm.gmt_deleted IS NULL -LIMIT 1; - --- 获取用户订阅的Collection详情(通过marketplace表关联) -SELECT c.id, c.title, c.description, u.username, ucs.id as subscription_id, ucs.gmt_subscribed -FROM user_collection_subscription ucs -JOIN collection_marketplace cm ON ucs.collection_marketplace_id = cm.id -JOIN collections c ON cm.collection_id = c.id -JOIN users u ON c.user_id = u.id -WHERE ucs.user_id = ? AND ucs.gmt_deleted IS NULL AND cm.gmt_deleted IS NULL -ORDER BY ucs.gmt_subscribed DESC; -``` - -### 4. 后端设计 - -遵循**软件架构分层原则**,按照从底层到高层的顺序进行设计:数据模型 → 服务层 → API层。 - -#### 4.1. 数据模型设计 (OpenAPI / `view_models.py`) - -**4.1.1 新增数据库模型 (aperag/db/models.py):** - -- **`CollectionMarketplaceStatusEnum`**: 分享状态枚举 (Python enum,用于代码逻辑) - - `DRAFT = "DRAFT"`: 未发布状态,仅所有者可见 - - `PUBLISHED = "PUBLISHED"`: 已发布状态,公开可见 - -- **`CollectionMarketplace`**: Collection 分享状态表 (SQLAlchemy 模型) - - `id: str`: 分享记录的唯一标识符 - - `collection_id: str`: 关联的 Collection ID - - `status: str`: 当前分享状态 (VARCHAR存储,值为'DRAFT'或'PUBLISHED') - - `gmt_created: datetime`: 分享记录创建时间 - - `gmt_updated: datetime`: 分享记录最后更新时间 - - `gmt_deleted: Optional[datetime]`: 软删除时间(NULL表示活跃记录) - -- **`UserCollectionSubscription`**: 用户订阅 Collection 表 (SQLAlchemy 模型) - - `id: str`: 订阅记录的唯一标识符 - - `user_id: str`: 订阅用户 ID - - `collection_marketplace_id: str`: 关联的发布实例 ID(collection_marketplace.id) - - `gmt_subscribed: datetime`: 订阅时间 - - `gmt_deleted: Optional[datetime]`: 取消订阅时间(NULL表示活跃订阅) - -**4.1.2 新增视图模型 (aperag/schema/view_models.py):** -> 在 Python / Pydantic 中定义,并通过 `make openapi-check` 验证 code-first OpenAPI 导出。 - -- **`SharedCollection`**: 共享的 Collection 信息(视图模型) - - `id: str`: Collection ID - - `title: str`: Collection 标题 - - `description: str`: Collection 描述 - - `owner_user_id: str`: 原所有者用户ID - - `owner_username: str`: 原所有者用户名 - - `subscription_id: Optional[str]`: 订阅记录ID(有值表示已订阅,None表示未订阅) - - `gmt_subscribed: Optional[datetime]`: 订阅时间(仅在已订阅时有值) - -- **`SharedCollectionList`**: 共享 Collection 列表响应 - - `items: List[SharedCollection]`: 共享的 Collection 列表 - - `total: int`: 总数量(用于分页) - - `page: int`: 当前页码 - - `page_size: int`: 每页大小 - -**4.1.3 修改现有模型:** - -- **`Collection`**: 扩展现有 Collection model(采用语义化设计) - - `is_published: bool`: 是否已发布到市场 - - `published_at: Optional[datetime]`: 发布时间,未发布时为null - -**4.1.4 OpenAPI Schema 组织:** - -所有新增的 model 定义放在 Python / Pydantic schema 中,现有 Collection model 的扩展也在对应 Pydantic model 中添加新字段。 - -#### 4.2. 服务层设计 (Business Logic) - -**4.2.1 新增服务模块: `aperag/service/marketplace_service.py`** - -```python -class MarketplaceService: - """ - Marketplace业务逻辑服务 - 职责: 处理所有与市场和分享相关的业务逻辑 - """ - - async def publish_collection(self, user_id: str, collection_id: str) -> None: - """发布Collection到市场""" - # 验证用户所有权 - # 创建或更新collection_marketplace记录 - # 状态设置为PUBLISHED - - async def unpublish_collection(self, user_id: str, collection_id: str) -> None: - """从市场下架Collection""" - # 验证用户所有权 - # 将collection_marketplace记录状态改为'DRAFT'(设置status='DRAFT', gmt_updated=datetime.utcnow()) - # 应用层查询并软删除相关订阅记录(设置gmt_deleted) - # 注意:需要使用事务确保数据一致性 - - async def get_sharing_status(self, collection_id: str) -> tuple[bool, Optional[datetime]]: - """获取Collection的分享状态""" - # 返回 (is_published, published_at) 元组 - - async def get_raw_sharing_status(self, collection_id: str) -> Optional[CollectionMarketplace]: - """获取原始分享状态(供权限检查使用)""" - - async def list_published_collections(self, user_id: str, page: int, page_size: int) -> SharedCollectionList: - """列出市场中所有已发布的Collection""" - # 查询PUBLISHED状态的Collection - # 计算当前用户的订阅状态 - # 支持分页 - - async def subscribe_collection(self, user_id: str, collection_id: str) -> SharedCollection: - """订阅Collection""" - # 1. 查找Collection对应的已发布marketplace记录 (status = 'PUBLISHED', gmt_deleted IS NULL) - # 2. 验证用户不是Collection所有者 (user_id != collection.user) - # 3. 检查是否已订阅该marketplace实例,防止重复订阅 - # 4. 创建user_collection_subscription记录(关联collection_marketplace_id) - # 异常: 如果用户是所有者,抛出 SelfSubscriptionError("Cannot subscribe to your own collection") - - async def unsubscribe_collection(self, user_id: str, collection_id: str) -> None: - """取消订阅Collection""" - # 验证用户已订阅该Collection - # 软删除订阅记录(设置gmt_deleted) - - async def get_user_subscription(self, user_id: str, collection_id: str) -> Optional[UserCollectionSubscription]: - """获取用户对指定Collection的活跃订阅状态""" - # 通过collection_id查找已发布的marketplace记录,再查找对应的订阅记录 - # 供权限检查函数调用 - # 返回None表示未订阅或已取消订阅 - - async def list_user_subscribed_collections(self, user_id: str, page: int, page_size: int) -> SharedCollectionList: - """获取用户所有活跃订阅的Collection""" - # 查询WHERE gmt_deleted IS NULL - # 关联查询获取Collection详细信息和原所有者信息 - # 支持分页 -``` - -**4.2.2 级联删除处理** - -由于新增了marketplace相关数据,需要在Collection删除时进行级联处理: - -```python -# 在现有的collection删除逻辑中添加 -async def delete_collection(self, user_id: str, collection_id: str): - # ... 现有的删除逻辑 - - # 新增:级联软删除marketplace相关记录 - await marketplace_service.cleanup_collection_marketplace_data(collection_id) - # 该方法会: - # 1. 软删除collection_marketplace记录 (设置gmt_deleted) - # 2. 批量软删除user_collection_subscription记录 (设置gmt_deleted) - # 3. 使用事务确保数据一致性 -``` - -**4.2.3 接口权限策略:** - -采用"专门接口分离"的设计,权限职责明确: - -**现有Collection接口族** (`/api/v1/collections/*`): -- 保持现有权限逻辑不变 -- 只需在Collection model响应中添加 `is_published` 和 `published_at` 字段 - -**新增MarketplaceCollection接口族** (`/api/v1/marketplace/collections/*`): -- **权限策略**: 仅允许有效订阅用户只读访问 -- **权限检查**: 在 `marketplace_collection_service._check_subscription_access` 中统一处理 -- **适用功能**: - - 文档列表查看和预览 - - 知识图谱只读浏览 - - 聊天Bot查询(如果支持) - -**核心优势**: -- ✅ **不影响现有逻辑**: 现有Collection接口保持不变 -- ✅ **职责清晰**: 新增接口专门处理订阅访问 -- ✅ **安全可靠**: 权限检查在接口层面就被分离 -- ✅ **易于维护**: marketplace功能独立,便于后续扩展 - -#### 4.3. API 端点设计 (View 层) - -基于服务层的业务逻辑,设计RESTful API端点,遵循统一的URL命名规范和错误处理模式。 - -**4.3.1 新增 API 端点** - -设计采用混合 URL 模式:marketplace 相关的浏览功能使用 `/marketplace` 路径,而具体 Collection 的分享操作作为 Collection 的子资源管理。 - -- **`GET /api/v1/marketplace/collections`**: 列出市场中所有公开的 Collection - - **功能**: 返回所有状态为 `PUBLISHED` 的 Collection 列表(包括当前用户自己发布的Collection) - - **权限**: 任何已登录用户都可以访问 - - **响应**: `SharedCollectionList` 类型,包含每个 Collection 的基本信息、所有者用户名;`subscription_id` 字段表示当前用户是否已订阅 - - **分页**: 支持 `page` 和 `page_size` 参数 - -- **`POST /api/v1/collections/{collection_id}/sharing`**: 发布一个 Collection 到市场 - - **功能**: 将指定 Collection 的状态设置为 `PUBLISHED` - - **权限**: 仅限 Collection 所有者 - - **行为**: 在 `collection_marketplace` 表中创建记录或更新状态 - - **响应**: 返回 204 No Content - -- **`DELETE /api/v1/collections/{collection_id}/sharing`**: 从市场下架一个 Collection - - **功能**: 将指定 Collection 的状态设置为 `DRAFT`(不删除记录,仅改变状态) - - **权限**: 仅限 Collection 所有者 - - **行为**: 立即停止其他用户对该 Collection 的访问,应用层处理相关订阅失效 - - **响应**: 返回 204 No Content - -- **`GET /api/v1/collections/{collection_id}/sharing`**: 获取指定 Collection 的分享状态 - - **功能**: 返回 Collection 的当前分享状态和相关信息 - - **权限**: 仅限 Collection 所有者 - - **响应**: 简洁的分享状态对象 - ```json - { - "is_published": true, - "published_at": "2024-01-15T10:30:00Z" - } - ``` - -- **`POST /api/v1/marketplace/collections/{collection_id}/subscribe`**: 订阅一个已发布的 Collection - - **功能**: 将指定的已发布 Collection 添加到用户的订阅列表 - - **权限**: 任何已登录用户(除 Collection 所有者外) - - **业务限制**: 用户无法订阅自己是所有者的 Collection - - **行为**: 在 `user_collection_subscription` 表中创建订阅记录 - - **响应**: 返回 `SharedCollection` 信息 - - **错误处理**: - - 如果已订阅则返回 409 Conflict - - 如果尝试订阅自己的 Collection 则返回 400 Bad Request "Cannot subscribe to your own collection" - -- **`DELETE /api/v1/marketplace/collections/{collection_id}/subscribe`**: 取消订阅 Collection - - **功能**: 从用户的订阅列表中移除指定 Collection - - **权限**: 仅限已订阅该 Collection 的用户 - - **行为**: 软删除订阅记录(设置 `gmt_deleted = current_timestamp`) - - **响应**: 返回 204 No Content - -- **`GET /api/v1/marketplace/collections/subscriptions`**: 获取用户订阅的 Collection 列表 (MVP核心API) - - **功能**: 返回当前用户所有活跃订阅的 Collection(`gmt_deleted IS NULL`) - - **权限**: 仅限当前用户(通过认证确定) - - **响应**: Collection 列表,每个item包含订阅信息(订阅时间、原所有者等) - - **分页**: 支持 `page` 和 `page_size` 参数 - - **设计理念**: 资源层级更清晰,subscriptions作为collections的子资源 - -**4.3.2 完整API结构概览** - -采用方案B的RESTful API设计,marketplace作为资源命名空间: - -``` -# Marketplace 相关API(新增) -GET /api/v1/marketplace/collections # 市场列表 -GET /api/v1/marketplace/collections/subscriptions # 用户订阅列表 -POST /api/v1/marketplace/collections/{id}/subscribe # 订阅Collection -DELETE /api/v1/marketplace/collections/{id}/subscribe # 取消订阅 -GET /api/v1/marketplace/collections/{id} # 订阅Collection详情(只读) -GET /api/v1/marketplace/collections/{id}/documents # 订阅Collection的文档列表(只读) -GET /api/v1/marketplace/collections/{id}/documents/{doc_id}/preview # 文档预览(只读) -GET /api/v1/marketplace/collections/{id}/graph # 知识图谱(只读) - -# Collection 管理API(现有,增强) -GET /api/v1/collections # 用户自有Collection列表 -GET /api/v1/collections/{id} # Collection详情(含分享状态) -POST /api/v1/collections/{id}/sharing # 发布到市场 -DELETE /api/v1/collections/{id}/sharing # 从市场下架 -GET /api/v1/collections/{id}/sharing # 查询分享状态 -# ... 其他现有Collection管理接口保持不变 -``` - -**API设计优势**: -- ✅ **命名空间清晰**: `/marketplace/` 明确表示市场功能 -- ✅ **RESTful规范**: 资源层级结构合理 -- ✅ **职责分离**: 管理和浏览功能完全分开 -- ✅ **扩展性好**: 便于后续添加其他marketplace功能 - -**4.3.3 MarketplaceCollection 专用 API 接口** - -为了确保数据隔离和API语义清晰,我们为SharedCollection(订阅的Collection)设计专门的只读API接口。这些接口: -- 只返回订阅者需要的字段,避免敏感信息泄露 -- 提供清晰的只读语义,用户不会意外调用编辑接口 -- 简化权限逻辑,只需验证订阅关系 - -**核心设计原则**: -- **数据隔离**: SharedCollection接口返回的字段与Collection接口不同,隐藏配置、统计等敏感信息 -- **权限简单**: 只需验证用户是否有有效订阅,无需复杂的所有者判断 -- **功能专注**: 只提供内容浏览必需的4个核心接口 - -**MarketplaceCollection 专用接口列表**: - -- **`GET /api/v1/marketplace/collections/{collection_id}`**: 获取MarketplaceCollection详情 - - **功能**: 返回订阅者视角的Collection信息 - - **权限检查**: 验证当前用户是否已订阅该Collection(`user_collection_subscription.gmt_deleted IS NULL`) - - **响应类型**: `SharedCollection` - - **数据隔离**: - - ✅ 包含: `id`, `title`, `description`, `owner_username`, `subscription_id`, `gmt_subscribed` - - ❌ 隐藏: Collection的详细配置、内部统计、所有者ID等敏感信息 - - **错误处理**: - - 未订阅: `403 Forbidden "You need to subscribe to this collection first"` - - Collection不存在: `404 Not Found` - - Collection未发布: `403 Forbidden "This collection is no longer available"` - -- **`GET /api/v1/marketplace/collections/{collection_id}/documents`**: 获取MarketplaceCollection的文档列表 - - **功能**: 返回文档基本信息列表(只读模式) - - **权限检查**: 验证订阅关系 - - **响应格式**: 标准文档列表,但隐藏创建者、编辑历史等内部信息 - - **分页支持**: `page`, `page_size` 参数 - - **过滤支持**: `search`, `file_type` 等基础过滤 - -- **`GET /api/v1/marketplace/collections/{collection_id}/documents/{document_id}/preview`**: 预览MarketplaceCollection中的文档 - - **功能**: 获取文档内容预览(与原接口相同的预览功能) - - **权限检查**: 验证订阅关系 - - **响应格式**: 文档预览数据,格式与原接口相同 - - **支持格式**: 所有原有支持的文档格式(PDF、Word、图片等) - -- **`GET /api/v1/marketplace/collections/{collection_id}/graph`**: 获取MarketplaceCollection的知识图谱 - - **功能**: 返回知识图谱数据(只读模式) - - **权限检查**: 验证订阅关系 - - **响应格式**: 图谱节点和边的数据,与原接口格式相同 - - **查询参数**: 支持 `node_limit`, `depth` 等图谱查询参数 - - **注意**: 不提供图谱编辑相关的接口(如合并建议、节点编辑等) - -**权限验证逻辑**(所有MarketplaceCollection接口通用): - -```python -async def _check_subscription_access(user_id: str, collection_id: str) -> bool: - """检查用户是否有权访问SharedCollection""" - - # 1. 检查Collection是否存在且已发布 - collection_marketplace = await db.get_collection_marketplace_by_collection_id(collection_id) - if not collection_marketplace or collection_marketplace.status != "PUBLISHED": - raise HTTPException(status_code=403, detail="This collection is no longer available") - - # 2. 检查用户订阅状态 - subscription = await db.get_user_subscription(user_id, collection_id) - if not subscription or subscription.gmt_deleted: - raise HTTPException(status_code=403, detail="You need to subscribe to this collection first") - - return True -``` - -**与原有Collection接口的区别**: - -| 方面 | Collection接口 | SharedCollection接口 | -|------|----------------|---------------------| -| **用途** | 用户自有Collection的完整管理 | 订阅Collection的只读访问 | -| **权限** | 验证所有权 | 验证订阅关系 | -| **功能** | 完整CRUD + 高级功能 | 仅内容浏览(4个接口) | -| **数据** | 完整字段,包含配置和统计 | 精简字段,隐藏敏感信息 | -| **路径** | `/api/v1/collections/{id}` | `/api/v1/marketplace/collections/{id}` | - -### 5. 错误处理策略 - -**API 错误处理分层:** - -```python -# 1. 业务逻辑层错误 (Service Layer) -class MarketplaceError(Exception): - """市场相关业务错误基类""" - pass - -class CollectionNotPublishedError(MarketplaceError): - """Collection未发布错误""" - pass - -class AlreadySubscribedError(MarketplaceError): - """重复订阅错误""" - pass - -class SubscriptionNotFoundError(MarketplaceError): - """订阅不存在错误""" - pass - -class SelfSubscriptionError(MarketplaceError): - """尝试订阅自己Collection错误""" - pass - -# 2. API层错误转换 (View Layer) -@router.post("/marketplace/collections/{collection_id}/subscribe") -async def subscribe_collection(collection_id: str, user: User = Depends(current_user)): - try: - result = await marketplace_service.subscribe_collection(user.id, collection_id) - return result - except CollectionNotPublishedError: - raise HTTPException(status_code=400, detail="Collection is not published") - except SelfSubscriptionError: - raise HTTPException(status_code=400, detail="Cannot subscribe to your own collection") - except AlreadySubscribedError: - raise HTTPException(status_code=409, detail="Already subscribed to this collection") - except PermissionError: - raise HTTPException(status_code=403, detail="Permission denied") - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Unexpected error in subscribe_collection: {e}") - raise HTTPException(status_code=500, detail="Internal server error") -``` - -**数据库事务一致性:** - -```python -# marketplace_service.py 中的事务处理 -async def unpublish_collection(self, user_id: str, collection_id: str): - async with self.db_session.begin(): # 事务开始 - try: - # 1. 验证所有权 - collection = await self._verify_ownership(user_id, collection_id) - - # 2. 更新分享状态 - await self._update_sharing_status(collection_id, 'DRAFT') - - # 3. 批量失效订阅 - await self._invalidate_subscriptions(collection_id) - - # 事务自动提交 - return {"message": "Collection unpublished successfully"} - - except Exception as e: - # 事务自动回滚 - logger.error(f"Failed to unpublish collection {collection_id}: {e}") - raise -``` - -**前端错误处理:** - -```typescript -// 前端错误处理中间件 -const handleApiError = (error: any) => { - if (error.response?.status === 403) { - if (error.response.data.detail?.includes('subscribe')) { - return { type: 'SUBSCRIPTION_REQUIRED', message: '请先订阅此知识库' }; - } - return { type: 'PERMISSION_DENIED', message: '权限不足' }; - } - - if (error.response?.status === 409) { - return { type: 'CONFLICT', message: '您已订阅此知识库' }; - } - - return { type: 'UNKNOWN', message: '操作失败,请重试' }; -}; - -// 组件中的错误处理 -const subscribeCollection = async (collectionId: string) => { - try { - await api.subscribeCollection(collectionId); - message.success('订阅成功'); - refresh(); - } catch (error) { - const errorInfo = handleApiError(error); - message.error(errorInfo.message); - } -}; -``` - -### 6. 前端设计 - -#### 6.1. 页面与路由设计 - -**A. 新增市场页面** - -- **路由**: `/marketplace` -- **文件位置**: `frontend/src/pages/marketplace/index.tsx` -- **页面功能**: - - 展示所有已发布的 Collection 卡片列表 - - 支持分页浏览,默认每页显示 12 个卡片 - - 每个卡片包含:Collection 标题、描述、所有者用户名、发布时间 - - 点击卡片跳转到对应的 Collection 详情页(只读模式) -- **UI 设计**: - -**B. Collection列表页面增强 (MVP核心功能)** - -- **路由**: `/collections` (现有页面增强) -- **文件位置**: `frontend/src/pages/collections/index.tsx` -- **API 调用策略**: 同时调用两个专门的API接口 - ```typescript - // 并行调用两个接口 - const [ownedCollections, sharedCollections] = await Promise.all([ - api.getCollections(pagination), // 获取自有Collection,返回Collection[] - api.getMarketplaceCollectionsSubscriptions(pagination) // 获取订阅Collection,返回SharedCollection[] - ]); - ``` -- **设计理念**: 聚焦marketplace核心概念,避免workspace抽象,双接口专职专责 -- **页面功能增强**: - - 前端合并显示用户自有Collection + 订阅的Collection - - 新增Collection类型标签:`我的` / `已订阅` - - 新增筛选器:`全部` / `我的知识库` / `已订阅` (前端筛选实现) - - 订阅Collection显示特殊图标和样式区分 - - 在订阅Collection上提供取消订阅操作 - - **UI 设计增强**: - ```typescript - // Collection 卡片 Props - 支持两种类型 - interface OwnedCollectionCardProps { - type: 'owned'; - collection: Collection; - } - - interface SharedCollectionCardProps { - type: 'shared'; - collection: SharedCollection; - } - - type CollectionCardProps = OwnedCollectionCardProps | SharedCollectionCardProps; - ``` - - **Collection卡片左上角显示类型标签**: - - 自有Collection: 绿色标签 "我的" - - 订阅Collection: 蓝色标签 "已订阅" - - **订阅Collection卡片样式区分**: - - 边框颜色: `#1890ff` (蓝色) - - 卡片背景: `#f6f9ff` (浅蓝色背景) - - 标题前添加订阅图标 `` - - **悬浮信息显示**: - - 自有Collection: 显示创建时间 - - 订阅Collection: 显示 "来自 @{owner_username} • 订阅于 {gmt_subscribed相对时间}" - - **操作菜单差异化**: - - 自有Collection: 编辑、删除、分享设置、查看详情 - - 订阅Collection: 查看详情、取消订阅 - - **筛选器实现**: - ```typescript - const [filter, setFilter] = useState<'all' | 'owned' | 'shared'>('all'); - // 前端合并两种类型的Collection进行筛选 - const allCollections = [ - ...ownedCollections.map(col => ({ type: 'owned' as const, collection: col })), - ...sharedCollections.map(col => ({ type: 'shared' as const, collection: col })) - ]; - const filteredCollections = allCollections.filter(item => { - if (filter === 'owned') return item.type === 'owned'; - if (filter === 'shared') return item.type === 'shared'; - return true; // 'all' - }); - ``` - -**C. Collection详情页面(所有者专用)** - -- **路由**: `/collections/{collection_id}` (保持现有路由) -- **文件位置**: `frontend/src/pages/collections/$collectionId/index.tsx` -- **API调用**: `GET /api/v1/collections/{collection_id}` (仅限所有者) -- **权限检查**: 使用现有的所有者权限验证 -- **功能特性**: - - 显示SharingControl组件(发布/取消发布开关) - - 显示完整的编辑功能(文档管理、设置、删除等) - - 可查看分享状态信息(通过 `is_published` 和 `published_at` 字段) - - 如果Collection已发布,显示订阅用户数量(可选功能) - -**D. MarketplaceCollection详情页面(订阅用户专用)** - -- **路由**: `/marketplace/collections/{collection_id}` (新增专门路由) -- **文件位置**: `frontend/src/pages/marketplace/collections/$collectionId/index.tsx` -- **API调用**: `GET /api/v1/marketplace/collections/{collection_id}` (仅限订阅用户) -- **权限检查**: 后端 `_check_subscription_access()` 验证用户是否已订阅 -- **功能特性**: - - 页面顶部显示ReadOnlyBanner组件 - - 显示订阅信息:"来自 @{owner_username} 的共享知识库" - - 提供取消订阅按钮 - - 隐藏所有编辑按钮:编辑Collection、上传文档、删除文档、重建索引 - - 隐藏设置页面入口 - - 文档列表只显示查看、预览按钮 - - 图谱页面隐藏合并节点等编辑功能 - - 聊天Bot可正常使用(只读查询) - -**路由跳转逻辑**: -```typescript -// 智能路由跳转逻辑 -const handleCollectionClick = (collection: SharedCollection, currentUser: User) => { - if (collection.owner_user_id === currentUser.id) { - // 自己的Collection → 所有者管理页面 - navigate(`/collections/${collection.id}`); - } else if (collection.subscription_id) { - // 已订阅的Collection → Marketplace详情页面 - navigate(`/marketplace/collections/${collection.id}`); - } else { - // 未订阅的Collection → 显示订阅对话框 - showSubscribeModal(collection.id); - } -}; - -// 从Collection列表点击(工作台页面) -const handleCollectionClick = (item: CollectionListItem) => { - if (item.type === 'owned') { - // 自有Collection → 所有者管理页面 - navigate(`/collections/${item.collection.id}`); - } else { - // 订阅Collection → Marketplace详情页面 - navigate(`/marketplace/collections/${item.collection.id}`); - } -}; -``` - -#### 6.2. 组件设计 - -**A. CollectionMarketplaceCard 组件** - -- **文件位置**: `frontend/src/components/CollectionMarketplaceCard.tsx` -- **Props 接口**: - ```typescript - interface CollectionMarketplaceCardProps { - collection: SharedCollection; - onClick: (collectionId: string) => void; - } - ``` -- **UI 元素**: - - Collection 标题(加粗显示) - - Collection 描述(最多显示 150 字符,超出显示省略号) - - 所有者用户名(小号字体,灰色显示) - - 订阅状态显示(根据 `subscription_id` 是否有值判断已订阅/未订阅) - - 悬浮效果和点击交互 - -**B. 只读模式提示 Banner** - -- **文件位置**: `frontend/src/components/ReadOnlyBanner.tsx` -- **显示条件**: 当响应类型为 `SharedCollection` 时显示 -- **Props 接口**: - ```typescript - interface ReadOnlyBannerProps { - ownerUsername: string; - } - ``` -- **UI 设计**: - - 位置:页面顶部,在页面标题下方 - - 样式:使用 Ant Design Alert 组件,type="info" - - 文案:"您正在以只读模式浏览来自 @{ownerUsername} 的共享知识库,无法进行修改操作" - - 图标:信息图标 - - 可关闭:否 - -**C. 分享控制组件** - -- **文件位置**: `frontend/src/components/SharingControl.tsx` -- **显示条件**: 仅当用户是 Collection 所有者时显示 -- **UI 元素**: - - 分享状态开关(Switch 组件) - - 状态标签:"已发布到市场" / "未发布" - - 确认对话框:发布和取消发布操作都需要用户确认 - -#### 6.3. 状态管理 - -**A. Collection Model 扩展** - -- **文件位置**: `frontend/src/models/collection.ts` -- **新增状态字段**: - ```typescript - interface CollectionState { - // 现有字段... - - // 新增字段 - marketplaceCollections: SharedCollection[]; - subscribedCollections: SharedCollection[]; - marketplaceLoading: boolean; - subscribedLoading: boolean; - marketplacePagination: { - current: number; - pageSize: number; - total: number; - }; - subscribedPagination: { - current: number; - pageSize: number; - total: number; - }; - } - ``` -- **新增 Effects**: - - `fetchMarketplaceCollections`: 获取市场 Collection 列表 - - `fetchSubscribedCollections`: 获取用户订阅的 Collection 列表 - - `subscribeCollection`: 订阅 Collection - - `unsubscribeCollection`: 取消订阅 Collection - - `publishCollection`: 发布 Collection 到市场 - - `unpublishCollection`: 从市场下架 Collection - - `fetchSharingStatus`: 获取 Collection 分享状态 - -**B. 全局状态更新** - -- **文件位置**: `frontend/src/models/global.ts` -- **导航菜单**: 新增 "知识库市场" 菜单项,链接到 `/marketplace` - -#### 6.4. UI 交互逻辑 - -**A. 只读模式下的 UI 限制** - -需要在以下组件中根据响应类型(`SharedCollection`)禁用或隐藏相关功能: - -```typescript -// 类型判断逻辑 -const isReadOnly = isSharedCollection(collectionData); - -// 在组件中使用 -if (isReadOnly) { - // 隐藏编辑功能 -} else { - // 显示完整功能 -} -``` - -- **文档管理页面**: - - 隐藏 "上传文档" 按钮 - - 隐藏文档操作菜单(编辑、删除) - - 禁用批量操作功能 -- **Collection 设置页面**: - - 完全隐藏设置页面入口 - - 或显示设置但所有表单字段设为只读 -- **知识图谱页面**: - - 保持正常显示,图谱本身就是只读的 -- **聊天页面**: - - 保持正常功能,允许查询和对话 - -**B. 分享操作的用户体验** - -- **发布确认**: - - 弹出确认对话框 - - 说明发布后其他用户可以访问这个知识库 - - 提供 "发布" 和 "取消" 按钮 -- **取消发布确认**: - - 弹出确认对话框 - - 说明取消发布后其他用户将无法访问 - - 提供 "确认下架" 和 "取消" 按钮 -- **操作反馈**: - - 操作成功后显示成功提示 - - 操作失败后显示错误信息 - - 操作进行中显示加载状态 - -**C. 前端代码示例(简化设计的使用)** - -```typescript -// SharingControl 组件使用示例 -interface SharingControlProps { - collection: Collection; - onToggle: (published: boolean) => Promise; -} - -const SharingControl: React.FC = ({ collection, onToggle }) => { - return ( -
- - - {collection.is_published ? '已发布到市场' : '未发布'} - - {collection.is_published && collection.published_at && ( - - 发布于 {formatRelativeTime(collection.published_at)} - - )} -
- ); -}; - -// 在Collection详情页面中的使用 -const CollectionDetail: React.FC = () => { - const handleTogglePublish = async (shouldPublish: boolean) => { - if (shouldPublish) { - await api.publishCollection(collection.id); - } else { - await api.unpublishCollection(collection.id); - } - // 刷新Collection数据 - refreshCollection(); - }; - - return ( -
- {/* 只有所有者才显示分享控制 */} - {isOwner && ( - - )} -
- ); -}; -``` - -### 7. 详细实施计划 (TODO List) - -#### **Phase 1: 后端 - 数据库与核心服务** - -- [x] **1.1. 数据库模型与迁移** - - [x] 在 `aperag/db/models.py` 中定义数据库模型: - - `CollectionMarketplace` (SQLAlchemy模型):分享状态记录,包含状态和时间字段 - - `UserCollectionSubscription` (SQLAlchemy模型):用户订阅记录,关联collection_marketplace_id,使用 `gmt_deleted` 字段实现软删除 - - `CollectionMarketplaceStatusEnum` (Python enum):分享状态枚举,用于代码逻辑,数据库使用VARCHAR存储 - - 包含所有必要字段、约束和索引(特别注意 `gmt_deleted` 的索引优化) - - 注意:`status` 字段使用 `Column(String(20))` 而非 `EnumColumn`,确保数据库层使用VARCHAR - - 重点:UserCollectionSubscription表关联collection_marketplace_id而不是collection_id - - [x] 运行 `make db-revision` 生成新的数据库迁移脚本 - - [x] 检查生成的迁移脚本(位于 `aperag/migration/versions/`)确保 SQL 语法正确性和索引创建 - - [x] 运行 `make db-migrate` 将数据库 schema 变更应用到开发环境 - - [x] 验证新表创建成功,检查约束和索引是否正确建立 - -- [x] **1.2. OpenAPI Schema 定义** - - [x] 创建 Pydantic marketplace 视图模型: - - `CollectionMarketplaceStatusEnum` - - `SharedCollection` (共享Collection模型,用于市场浏览和订阅访问) - - `SharedCollectionList` (共享Collection列表响应模型) - - `SharingStatusResponse` (简洁的分享状态响应模型,包含is_published和published_at字段) - - [x] 创建 FastAPI marketplace router,定义以下端点: - - `GET /api/v1/marketplace/collections`:获取市场Collection列表 - - `GET /api/v1/marketplace/collections/subscriptions`:获取当前用户订阅的Collection列表 - - `POST /api/v1/marketplace/collections/{collection_id}/subscribe`:订阅Collection - - `DELETE /api/v1/marketplace/collections/{collection_id}/subscribe`:取消订阅Collection - - [x] 修改 FastAPI collections router,添加 sharing 相关端点: - - `GET /api/v1/collections/{collection_id}/sharing` - - `POST /api/v1/collections/{collection_id}/sharing` - - `DELETE /api/v1/collections/{collection_id}/sharing` - - - [x] 修改 Pydantic view model,在 Collection schema 中添加 `is_published` 和 `published_at` 字段 - - [x] 运行 `make openapi-check` 验证 code-first OpenAPI 导出 - - [x] 验证 Pydantic 模型类型注解正确 - -- [x] **1.3. 服务层 - Marketplace Service** - - [x] 创建 `aperag/service/marketplace_service.py` 文件和 MarketplaceService 类 - - [x] 实现 `publish_collection(user_id: str, collection_id: str)` 方法: - - 验证用户是 Collection 所有者 - - 创建或更新 collection_marketplace 记录为 PUBLISHED 状态,手动设置 `gmt_updated = datetime.utcnow()` - - 处理重复发布的情况(如果已经是 PUBLISHED 状态,应返回成功但不执行任何操作) - - [x] 实现 `unpublish_collection(user_id: str, collection_id: str)` 方法: - - 验证用户是 Collection 所有者 - - 将 collection_marketplace 记录状态改为 'DRAFT'(设置 `status='DRAFT', gmt_updated=datetime.utcnow()`) - - 应用层查询并软删除相关订阅记录(设置 `gmt_deleted`) - - 使用数据库事务确保数据一致性 - - [x] 实现 `get_sharing_status(collection_id: str)` 方法: - - 返回指定 Collection 的分享状态信息 - - [x] 实现 `get_raw_sharing_status(collection_id: str)` 内部方法: - - 供权限检查函数调用,不进行额外的权限验证 - - [x] 实现 `list_published_collections(user_id: str, page: int, page_size: int)` 方法: - - 查询所有 PUBLISHED 状态的 Collection - - 支持分页功能 - - 关联查询获取 Collection 基本信息和所有者用户名 - - 计算当前用户的订阅状态(通过subscription_id字段) - - [x] 实现订阅相关方法: - - `subscribe_collection(user_id: str, collection_id: str)` 方法: - - 查找Collection对应的已发布marketplace记录 (status = 'PUBLISHED', gmt_deleted IS NULL) - - 验证用户不是 Collection 所有者,如果是则抛出 SelfSubscriptionError - - 检查是否已订阅该marketplace实例,防止重复订阅 - - 创建用户订阅记录(关联collection_marketplace_id) - - `unsubscribe_collection(user_id: str, collection_id: str)` 方法: - - 验证用户已订阅该 Collection - - 软删除订阅记录(设置 gmt_deleted = current_timestamp) - - `get_user_subscription(user_id: str, collection_id: str)` 方法: - - 通过collection_id查找已发布的marketplace记录,再查找对应的订阅记录 - - 获取用户对指定 Collection 的活跃订阅状态(`WHERE gmt_deleted IS NULL`) - - 供权限检查函数调用,返回 None 表示未订阅或已取消订阅 - - `list_user_subscribed_collections(user_id: str, page: int, page_size: int)` 方法: - - 查询用户所有活跃订阅的 Collection(`WHERE gmt_deleted IS NULL`) - - 关联查询获取 Collection 详细信息和原所有者信息 - - 返回包含订阅信息的 Collection 列表 - - 支持分页功能 - -- [x] **1.4. 服务层 - MarketplaceCollection Service** - - [x] 创建 `aperag/service/marketplace_collection_service.py` 文件和 MarketplaceCollectionService 类 - - [x] 实现 `_check_subscription_access(user_id: str, collection_id: str)` 方法: - - 验证 Collection 是否存在且已发布(status = 'PUBLISHED') - - 验证用户是否已订阅且订阅有效(gmt_deleted IS NULL) - - 返回有效的订阅记录或抛出相应的 HTTPException - - [x] 实现 MarketplaceCollection 专用业务方法: - - `get_marketplace_collection(user_id: str, collection_id: str)` 方法: - - 调用 `_check_subscription_access` 验证权限 - - 返回 SharedCollection 数据(只包含订阅者需要的字段) - - `list_marketplace_collection_documents(user_id: str, collection_id: str, page: int, page_size: int)` 方法: - - 调用 `_check_subscription_access` 验证权限 - - 返回文档列表(隐藏内部信息如创建者、编辑历史等) - - `get_marketplace_collection_document_preview(user_id: str, collection_id: str, document_id: str)` 方法: - - 调用 `_check_subscription_access` 验证权限 - - 返回文档预览数据 - - `get_marketplace_collection_graph(user_id: str, collection_id: str, **params)` 方法: - - 调用 `_check_subscription_access` 验证权限 - - 返回知识图谱数据(只读模式) - - - -#### **Phase 2: 后端 - API 视图与前端集成** - -- [x] **2.1. API 视图层实现** - - [x] 创建 `aperag/views/marketplace.py` 文件: - - 实现 `list_marketplace_collections_view` 函数 - - 处理分页参数验证和默认值设置 - - 调用 `marketplace_service.list_published_collections` - - 返回标准化的分页响应格式 - - [x] 修改 `aperag/views/collections.py`(或相关视图文件)实现 sharing 相关端点: - - `get_collection_sharing_status_view`: 获取分享状态(仅所有者) - - `publish_collection_view`: 发布 Collection 到市场 - - `unpublish_collection_view`: 从市场下架 Collection - - 为每个端点添加用户身份验证、所有权验证和异常错误处理 - - [x] 在 `aperag/app.py` 中注册新的路由: - - 添加 `marketplace` 路由组,tag 设为 "marketplace" - - 添加 `marketplace-collections` 路由组,tag 设为 "marketplace-collections" - - 集成到主应用的路由配置中 - - [x] 创建 `aperag/views/marketplace_collections.py` 文件: - - 实现 `get_marketplace_collection_view`: 获取MarketplaceCollection详情 - - 实现 `list_marketplace_collection_documents_view`: 获取文档列表 - - 实现 `get_marketplace_collection_document_preview_view`: 文档预览 - - 实现 `get_marketplace_collection_graph_view`: 知识图谱 - - 为每个端点添加订阅权限验证和异常错误处理 - -- [x] **2.2. 前端 - typed client 与状态管理** - - [x] 通过 FE typed client / feature adapter 接入 marketplace API - - [x] 验证前端 feature adapter 中的新增内容: - - 检查 marketplace 相关 API 函数 - - 检查新的 TypeScript 接口 - - 验证现有 Collection 接口是否正确更新 - - [ ] 更新前端类型定义: - - 修改 `frontend/src/models/collection.ts` 中的 Collection 接口 - - 在 `frontend/src/types/` 中添加或更新相关类型定义 - - 确保 `SharedCollection` 类型正确 - - 实现类型守卫函数 `isSharedCollection` - -#### **Phase 3: 前端 - UI 实现** - -- [x] **3.1. Marketplace 页面开发** - - [x] 创建页面文件 `frontend/src/pages/marketplace/index.tsx`: - - 实现基础页面结构和布局 - - 添加页面标题 "知识库市场" 和功能说明 - - 集成分页组件和加载状态管理 - - [x] 实现 API 数据获取逻辑: - - 在页面加载时调用 marketplace API - - 处理分页参数和状态更新 - - 实现错误状态处理和重试机制 - - [x] 创建 `CollectionMarketplaceCard` 组件(`frontend/src/components/CollectionMarketplaceCard.tsx`): - - 设计卡片布局(标题、描述、所有者、发布时间) - - 实现悬浮效果和点击交互 - - 处理描述文本截断(最多 150 字符) - - 添加相对时间格式化功能 - - 使用 `SharedCollection` 类型作为 Props - - 实现订阅状态显示逻辑: - - 根据 `subscription_id` 字段是否有值判断订阅状态 - - 如果当前用户是Collection所有者,显示 "我的" 标签 - - 如果当前用户非所有者且未订阅(`subscription_id` 为空),显示 "订阅" 按钮 - - 如果当前用户已订阅(`subscription_id` 有值),显示 "已订阅" 状态 - - [x] 实现网格布局和响应式设计: - - 桌面端:4 列网格布局 - - 平板端:2-3 列网格布局 - - 手机端:1 列布局 - - [x] 添加到导航菜单: - - 在 `frontend/src/layouts/sidebar.tsx` 中添加 "知识库市场" 菜单项 - - 设置市场图标(如ShopOutlined)和路由链接 - -- [ ] **3.2. MarketplaceCollection 专用页面开发** - - [ ] 创建 `frontend/src/pages/marketplace/collections/$collectionId/index.tsx`: - - 实现MarketplaceCollection详情页面基础结构 - - 使用专用的marketplace/collections API接口 - - 显示只读模式Banner和取消订阅功能 - - [ ] 创建 `ReadOnlyBanner` 组件(`frontend/src/components/ReadOnlyBanner.tsx`): - - 使用 Ant Design Alert 组件 - - 设计醒目的提示样式(蓝色信息提示) - - 添加信息图标(InfoCircleOutlined)和提示文案 - - 接收 `ownerUsername` 作为 Props - - 集成"取消订阅"按钮功能 - - [ ] 实现MarketplaceCollection页面功能: - - 文档列表展示(只读模式) - - 文档预览功能 - - 知识图谱查看(只读模式) - - 聊天Bot功能(如果支持) - - [ ] 创建共享组件用于复用: - - `DocumentListReadOnly`: 只读文档列表组件 - - `GraphViewReadOnly`: 只读图谱查看组件 - - `CollectionInfoReadOnly`: 只读基本信息展示 - -- [x] **3.3. Collection 详情页 - 分享功能实现(所有者专用)** - - [x] 创建 `SharingControl` 组件(`frontend/src/components/SharingControl.tsx`): - - 使用 Switch 组件控制发布状态(基于 `collection.is_published`) - - 显示当前分享状态标签和发布时间(使用 `collection.published_at`) - - 仅在Collection详情页面(`/collections/{id}`)中显示 - - [x] 实现分享操作确认对话框: - - 发布确认:说明发布后其他用户可以访问 - - 下架确认:说明下架后其他用户将无法访问 - - 使用 Ant Design Modal 组件 - - [x] 集成分享状态管理: - - 在 Collection model 中添加相关 Effects - - 实现发布/取消发布的 API 调用(调用 `/api/v1/collections/{id}/sharing`) - - 处理操作成功/失败的反馈提示 - - [x] 在 Collection 详情页面中集成 SharingControl: - - 在Collection标题右侧区域展示分享控制组件 - - 确保只有在Collection接口(所有者模式)下才显示 - - 实现状态变更后的页面刷新 - -**注意**:MarketplaceCollection详情页面(`/marketplace/collections/{id}`)不需要SharingControl组件,它们有专门的ReadOnlyBanner和取消订阅功能。 diff --git a/docs/zh-CN/design/graph_curation.md b/docs/zh-CN/design/graph_curation.md deleted file mode 100644 index a3f7588c3..000000000 --- a/docs/zh-CN/design/graph_curation.md +++ /dev/null @@ -1,373 +0,0 @@ -# Graph Curation Module - -## 1. Decision - -### 1.1 Do we need merge-suggestion discovery? - -Yes. - -Current graphindex v2 already keeps the **manual merge primitive** -(`GraphIndexService.merge_entities()`), which means the product still -accepts graph curation as a valid user workflow. What v2 removed was the -**discovery layer** that finds likely duplicate entities and turns them -into reviewable suggestions. - -From first principles: - -- If graph data is only a transient retrieval scaffold, manual merge - should not exist at all. -- If manual merge exists, users need a reliable way to find candidates. -- Therefore, merge-suggestion discovery is a missing product layer, not - an optional old feature to half-revive. - -### 1.2 What should be rebuilt? - -Rebuild a **new** `graph_curation` module. - -Do **not** revive the LightRAG-era pipeline shape: - -- not `top-degree -> LLM grouping -> action` -- not stateless request-time analysis -- not prompt/state/review logic mixed into `graphindex` - -## 2. Goals And Non-Goals - -### 2.1 Goals - -- Provide a simple async workflow that scans one collection and produces - persisted merge suggestions. -- Keep graph truth on a single write path: accept uses the existing - `GraphIndexService.merge_entities()` only. -- Stay viable across PostgreSQL / Neo4j / Nebula graph backends. -- Keep API and interaction surface minimal. -- Keep performance and token cost bounded by deterministic blocking and - explicit caps. - -### 2.2 Non-Goals - -- No full-graph LLM clustering. -- No synchronous analysis in index/query/UI hot paths. -- No second merge implementation in the curation module. -- No backend-specific fuzzy search logic that only works on one graph DB. - -## 3. Module Boundary - -### 3.1 `graphindex` - -Responsibilities remain: - -- graph truth writes -- graph query context -- label/subgraph reads -- structural merge primitive -- graph shadow vector maintenance - -It may expose **read-only helper methods** for curation, but it does not -own: - -- suggestion state -- review workflow -- run orchestration -- invalidation policy - -### 3.2 `graph_curation` - -New module responsibilities: - -- create async analysis runs -- enumerate candidate entities from graph truth -- build cheap candidate pairs -- ask LLM for pairwise adjudication -- aggregate positive pairs into suggestions -- persist suggestion state -- accept/reject/expire/supersede suggestions - -## 4. First-Principles Architecture - -```mermaid -flowchart TD - A[POST merge-suggestions] --> B[create run row] - B --> C[Celery task] - C --> D[list entities from graphindex] - D --> E[deterministic candidate generation] - E --> F[pairwise LLM adjudication] - F --> G[connected-component aggregation] - G --> H[persist suggestions] - H --> I[GET merge-suggestions] - I --> J[human accept/reject] - J --> K[GraphIndexService.merge_entities] - K --> L[supersede overlapping suggestions] -``` - -Key design choice: - -- candidate discovery uses **graph truth + graph shadow vectors** -- not graph-backend-native fuzzy/text search - -This keeps the module portable across PG / Neo4j / Nebula. - -## 5. Data Model - -### 5.1 `graph_curation_runs` - -One row per async scan. - -Fields: - -- `id` -- `user_id` -- `collection_id` -- `status`: `PENDING | RUNNING | COMPLETED | FAILED` -- `config_json`: immutable run config snapshot -- `stats`: counts for analyzed entities, candidate pairs, positive pairs, - final suggestions -- `error_message` -- `gmt_created / gmt_updated / gmt_started / gmt_finished` - -### 5.2 `graph_curation_suggestions` - -One row per reviewable suggestion. - -Fields: - -- `id` -- `run_id` -- `user_id` -- `collection_id` -- `status`: `PENDING | ACCEPTED | REJECTED | EXPIRED | SUPERSEDED` -- `entity_ids`: all entities in the suggestion -- `entity_snapshots`: display payload captured at generation time -- `target_entity_id`: deterministic target chosen from existing nodes -- `confidence_score` -- `reason` -- `evidence`: structured trace of pairwise signals / adjudications -- `resolution_note` -- `operated_by` -- `gmt_created / gmt_updated / gmt_operated` - -No separate suggestion-items table is required in v1 of this module: - -- entity membership is already naturally represented as JSON array -- the workflow is simple and collection-scoped -- write/read paths stay cheaper and easier to reason about - -If later UI/product needs per-item moderation or cross-suggestion joins, -that is a future schema migration, not a reason to over-model now. - -## 6. Candidate Generation - -### 6.1 Input Set - -The module scans up to a bounded number of entities per collection -(`max_entities`). - -Enumeration comes from `GraphIndexService.list_entities_for_curation()`, -which is implemented as a bounded read over the existing graph truth. - -### 6.2 Candidate Signals - -Generate pair candidates only within the same `entity_type`, then score -them with cheap deterministic signals: - -- normalized-name exact match -- normalized-name containment -- acronym match -- token overlap -- description token overlap -- shared source chunk overlap -- optional graph shadow vector nearest neighbors - -This gives three important properties: - -- bounded compute -- explainable candidate provenance -- backend independence - -### 6.3 Why use graph shadow vectors? - -Entity vectors are already written to the existing vector store as -`indexer=graph_entity`. - -Using those shadows means: - -- no duplicate vector infrastructure -- no graph-backend-specific text indexing -- same candidate discovery logic works across PG / Neo4j / Nebula - -## 7. LLM Adjudication - -### 7.1 Unit Of Judgement - -Use **pairwise** adjudication. - -Input: - -- entity A snapshot -- entity B snapshot -- deterministic candidate signals - -Output JSON only: - -- `same_entity: bool` -- `confidence: float` -- `reason: str` -- `recommended_target_entity_id: str | null` - -Constraint: - -- `recommended_target_entity_id` must be one of the two existing ids -- the model cannot invent a new canonical node - -### 7.2 Why pairwise instead of cluster-level? - -Because pairwise is: - -- easier to prompt -- easier to test -- easier to bound in cost -- easier to aggregate deterministically - -Multi-entity suggestions are formed **after** pairwise positives are -known. - -## 8. Suggestion Aggregation - -Positive pair edges form a graph over entity ids. - -The module builds connected components: - -- size `< 2` -> ignored -- size `>= 2` -> one suggestion - -Target selection is deterministic: - -1. pairwise recommendation votes -2. larger supporting chunk count -3. lexical tie-break on entity id - -This preserves a single graph identity rule without delegating target -creation to the LLM. - -## 9. API Contract - -Keep the public API narrow. - -### 9.1 Start Run - -`POST /collections/{collection_id}/graphs/merge-suggestions` - -Behavior: - -- if a run is already `PENDING/RUNNING`, return that run -- otherwise create a new run and enqueue Celery work - -### 9.2 Read Latest Suggestions - -`GET /collections/{collection_id}/graphs/merge-suggestions` - -Behavior: - -- return the latest run summary -- return suggestions from that run - -### 9.3 Act On A Suggestion - -`POST /collections/{collection_id}/graphs/merge-suggestions/{suggestion_id}/action` - -Body: - -- `{"action": "accept"}` or `{"action": "reject"}` - -Accept: - -- call existing `GraphIndexService.merge_entities()` -- mark accepted suggestion as `ACCEPTED` -- mark overlapping pending suggestions as `SUPERSEDED` - -Reject: - -- mark suggestion as `REJECTED` - -No target override API is exposed. The point of this module is simple, -low-support review, not a second complex merge editor. - -## 10. Invalidation Rules - -Freshness is conservative by design. - -### 10.1 Expire Pending Suggestions On - -- document re-index -- document delete -- manual merge - -### 10.2 Purge Everything On - -- collection delete - -### 10.3 Supersede Pending Suggestions On - -- successful completion of a newer run -- accepting one suggestion that overlaps their entity set - -The module prefers **expire + rerun** over trying to incrementally patch -old suggestions in place. - -## 11. Performance And Cost - -Hard caps are server-side, not UI-driven: - -- max entities analyzed per run -- max candidate pairs kept after blocking -- max vector neighbors per entity -- max concurrent LLM adjudications -- max final suggestions persisted - -This keeps the feature predictable in three dimensions: - -- DB cost -- vector search cost -- LLM token cost - -## 12. Backend Viability - -### 12.1 PostgreSQL - -Works through existing graphindex tables and vector shadows. - -### 12.2 Neo4j - -Works because the curation module does not rely on Neo4j-specific text -or full-text behavior. It reads entities through graphindex and uses the -same vector shadow path. - -### 12.3 Nebula - -Works for the same reason: graph truth comes from graphindex storage -calls, not Nebula-specific fuzzy search. This avoids Nebula's async -schema/index readiness problems from becoming curation-specific product -bugs. - -## 13. Historical Cleanup - -The implementation removes stale v1/v2 residuals: - -- old `410` merge-suggestion routes -- old stateless request/response schemas -- stale OpenAPI contract that still described high-degree LightRAG scan - -Historical code and analysis docs remain only as archaeology input, not -as active runtime contract. - -## 14. Implementation Notes - -This rollout intentionally keeps one truth path: - -- discover in `graph_curation` -- merge in `graphindex` - -That gives us a simple rule for future maintenance: - -> if code changes graph truth directly, it belongs in `graphindex`; -> if code proposes or reviews graph edits, it belongs in -> `graph_curation`. diff --git a/docs/zh-CN/design/graph_index_creation.md b/docs/zh-CN/design/graph_index_creation.md deleted file mode 100644 index 255f6433a..000000000 --- a/docs/zh-CN/design/graph_index_creation.md +++ /dev/null @@ -1,1084 +0,0 @@ ---- -title: 图索引构建流程 -description: ApeRAG 知识图谱索引构建的完整流程与核心技术 -keywords: 知识图谱, Graph Index, 实体提取, 关系抽取, 并发优化 -position: 2 ---- - -# 图索引构建流程 - -## 1. 什么是图索引 - -图索引(Graph Index)是 ApeRAG 的核心特色功能,它能从非结构化文本中自动提取出结构化的知识图谱。 - -### 1.1 一个简单的例子 - -想象一下,你有一份关于公司组织架构的文档,里面提到: - -> "张三是数据库团队的负责人,他擅长 PostgreSQL 和 MySQL。李四在前端团队工作,经常和张三的团队协作开发后台管理系统。" - -**从文档到知识图谱的转换**: - -```mermaid -flowchart LR - subgraph Input[📄 输入文档] - Doc["张三是数据库团队的负责人,
他擅长 PostgreSQL 和 MySQL。
李四在前端团队工作..."] - end - - subgraph Process[🔄 图索引处理] - Extract[提取实体和关系] - end - - subgraph Output[🕸️ 知识图谱] - direction TB - A[张三
人物] -->|负责| B[数据库团队
组织] - A -->|擅长| C[PostgreSQL
技术] - A -->|擅长| D[MySQL
技术] - E[李四
人物] -->|属于| F[前端团队
组织] - E -->|协作| A - end - - Input --> Process - Process --> Output - - style Input fill:#e3f2fd - style Process fill:#fff59d - style Output fill:#c8e6c9 -``` - -传统的向量检索只能找到"语义相似"的段落,但无法回答这些问题: -- 张三负责什么? -- 张三和李四是什么关系? -- 数据库团队都有哪些技术栈? - -**图索引能做到**:精确回答这些需要理解"关系"的问题,因为它把隐藏在文本中的知识关系显性化了。 - -### 1.2 核心价值 - -与传统检索方式相比,图索引提供了独特的能力: - -| 能力 | 向量检索 | 全文检索 | 图索引 | -|------|---------|---------|--------| -| 语义相似搜索 | ✅ 强 | ❌ 弱 | ✅ 强 | -| 精确关键词匹配 | ❌ 弱 | ✅ 强 | ✅ 中 | -| 关系查询 | ❌ 不支持 | ❌ 不支持 | ✅ 强 | -| 多跳推理 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 | -| 适用问题 | "如何优化性能" | "PostgreSQL 配置" | "张三和李四的关系" | - -**核心优势**:图索引让 AI 能够"理解"知识之间的关联,而不仅仅是文本的相似度。 - -## 2. 图索引能解决什么问题 - -图索引特别擅长处理那些需要"理解关系"的场景。让我们看看它在实际工作中的应用。 - -### 2.1 企业知识管理 - -**场景**:公司有大量文档,包括组织架构、项目资料、技术文档等。 - -**图索引的价值**: - -- 📊 **组织关系**:"张三的团队有哪些人?" → 快速找到团队成员 -- 🔗 **协作关系**:"谁和张三合作过?" → 发现工作网络 -- 🛠️ **技能图谱**:"谁擅长 PostgreSQL?" → 定位技术专家 -- 📁 **项目历史**:"张三参与过哪些项目?" → 追溯项目经验 - -**实际效果**: - -``` -问:"数据库团队负责人是谁?" -传统检索:返回包含"数据库团队"和"负责人"的所有段落(可能几十条) -图索引:直接返回"张三" + 相关背景信息 -``` - -### 2.2 研究与学习 - -**场景**:分析学术论文、技术文档,理解知识脉络。 - -**图索引的价值**: - -- 👥 **作者网络**:"这个作者和谁合作过?" → 发现研究团队 -- 📖 **引用关系**:"这篇论文引用了哪些文献?" → 追溯研究脉络 -- 🔬 **技术演进**:"这个技术是如何发展的?" → 理解技术历史 -- 💡 **概念关联**:"A 技术和 B 技术有什么关系?" → 连接知识点 - -### 2.3 产品与服务 - -**场景**:产品文档、用户手册、API 文档等。 - -**图索引的价值**: - -- ⚙️ **功能依赖**:"启用 A 功能需要先配置什么?" → 理解依赖关系 -- 🔧 **配置关联**:"这个配置项会影响哪些功能?" → 避免误操作 -- 🐛 **问题诊断**:"出现 X 错误可能是什么原因?" → 快速定位 -- 📚 **API 关系**:"这个 API 通常和哪些 API 一起使用?" → 学习最佳实践 - -### 2.4 对比:什么时候用图索引 - -不同的问题适合不同的检索方式: - -| 问题类型 | 示例 | 最佳方案 | -|---------|------|---------| -| **概念理解** | "什么是 RAG?" | 向量检索 | -| **精确查找** | "PostgreSQL 配置文件路径" | 全文检索 | -| **关系查询** | "张三和李四什么关系?" | 图索引 ✨ | -| **多跳推理** | "张三团队用的技术栈" | 图索引 ✨ | -| **知识追溯** | "这个功能依赖哪些模块?" | 图索引 ✨ | - -**最佳实践**:ApeRAG 同时支持向量检索、全文检索和图索引,可以根据问题类型智能选择或组合使用。 - -## 3. 构建流程概览 - -当你上传一个文档并启用图索引后,ApeRAG 会自动完成以下步骤。这里先给出一个简单的概览,具体细节在后面章节详细介绍。 - -### 3.1 五个关键步骤 - -```mermaid -flowchart TB - subgraph Step1["1️⃣ 文档分块"] - A1[原始文档] --> A2[智能分块] - A2 --> A3[生成 Chunks] - end - - subgraph Step2["2️⃣ 实体关系提取"] - B1[Chunks] --> B2[调用 LLM] - B2 --> B3[识别实体] - B2 --> B4[识别关系] - end - - subgraph Step3["3️⃣ 连通分量分析"] - C1[实体关系网络] --> C2[BFS 算法] - C2 --> C3[分组] - end - - subgraph Step4["4️⃣ 并发合并"] - D1[分组 1] --> D2[实体去重] - D3[分组 2] --> D4[实体去重] - D5[分组 N] --> D6[实体去重] - D2 --> D7[关系聚合] - D4 --> D7 - D6 --> D7 - end - - subgraph Step5["5️⃣ 多存储写入"] - E1[图数据库] - E2[向量数据库] - E3[文本存储] - end - - A3 --> B1 - B3 --> C1 - B4 --> C1 - C3 --> D1 - C3 --> D3 - C3 --> D5 - D7 --> E1 - D7 --> E2 - A3 --> E3 - - style Step1 fill:#e3f2fd - style Step2 fill:#fff3e0 - style Step3 fill:#f3e5f5 - style Step4 fill:#e8f5e9 - style Step5 fill:#fce4ec -``` - -**简单来说**,就是:文档分块 → 提取实体关系 → 智能分组 → 并发合并 → 写入存储。 - -整个过程完全自动化,你只需要上传文档,系统会自动完成所有工作。 - -### 3.2 处理时间参考 - -不同规模的文档,处理时间大致如下: - -| 文档大小 | 实体数量 | 处理时间 | 说明 | -|---------|---------|---------|------| -| 小型(< 5 页) | ~50 个 | 10-30 秒 | 公司通知、会议纪要 | -| 中型(10-50 页) | ~200 个 | 1-3 分钟 | 技术文档、产品手册 | -| 大型(100+ 页) | ~1000 个 | 5-15 分钟 | 研究报告、书籍 | - -**影响因素**: -- LLM 响应速度(主要瓶颈) -- 文档复杂度(表格、图片多会慢一些) -- 并发设置(可以通过配置提速) - -> 💡 **提示**:处理是异步的,你可以上传多个文档,系统会并行处理。 - -### 3.3 实时进度查看 - -你可以随时查看文档的处理进度: - -``` -文档状态:处理中 -- ✅ 文档解析:完成 -- ✅ 文档分块:完成(生成 25 个 chunks) -- 🔄 实体提取:进行中(15/25) -- ⏳ 关系提取:等待中 -- ⏳ 图谱构建:等待中 -``` - -处理完成后,文档状态会变为"活跃",此时就可以进行图谱查询了。 - -## 4. 详细构建流程 - -前面介绍了图索引能做什么以及整体流程概览。如果你想了解更多技术细节,这一章会详细介绍每个步骤的具体实现。 - -> 💡 **阅读建议**:如果你只是想了解图索引的基本概念和用法,可以跳过这一章,直接看第 8 章的实际应用场景。 - -### 4.1 文档分块 - -第一步是把长文档切成合适大小的块(chunks)。 - -**为什么要分块?** -- LLM 有输入长度限制(通常几千到几万 tokens) -- 块太大:提取质量下降,LLM 容易"遗漏"信息 -- 块太小:丢失上下文,无法理解完整语义 - -**智能分块策略**: - -```mermaid -flowchart LR - Doc[长文档] --> Check{检查大小} - Check -->|小于 1200 tokens| Keep[保持完整] - Check -->|大于 1200 tokens| Split[智能分割] - - Split --> By1[按段落分] - By1 --> Check2{还是太大?} - Check2 -->|是| By2[按句子分] - Check2 -->|否| Done[完成] - By2 --> Check3{还是太大?} - Check3 -->|是| By3[按字符分] - Check3 -->|否| Done - By3 --> Done - - style Doc fill:#e1f5ff - style Split fill:#ffccbc - style Done fill:#c5e1a5 -``` - -**分块参数**: -- 默认大小:1200 tokens(约 800-1000 个中文字) -- 重叠大小:100 tokens(保证上下文连续) -- 优先级:段落 > 句子 > 字符 - -### 4.2 实体关系提取 - -使用 LLM 从每个 chunk 中识别实体和关系。 - -**提取过程**: - -```mermaid -sequenceDiagram - participant C as Chunk - participant L as LLM - participant R as 结果 - - C->>L: "张三是数据库团队负责人..." - L->>R: 实体: [张三(人物), 数据库团队(组织)] - L->>R: 关系: [张三-负责->数据库团队] - - C->>L: "张三擅长 PostgreSQL..." - L->>R: 实体: [张三(人物), PostgreSQL(技术)] - L->>R: 关系: [张三-擅长->PostgreSQL] -``` - -**并发优化**:多个 chunks 可以同时调用 LLM,默认并发 20 个请求。 - -### 4.3 连通分量分析 - -把实体关系网络分成独立的子图,实现并行处理。 - -**为什么需要这一步?** - -技术团队的实体和财务部门的实体之间没有连接,可以完全并行处理! - -```mermaid -graph LR - subgraph 分量1[连通分量 1 - 技术团队] - A1[张三] -->|负责| A2[数据库团队] - A1 -->|擅长| A3[PostgreSQL] - A4[李四] -->|协作| A1 - end - - subgraph 分量2[连通分量 2 - 财务部门] - B1[王五] -->|属于| B2[财务部] - B3[赵六] -->|协作| B1 - end - - style 分量1 fill:#bbdefb - style 分量2 fill:#c5e1a5 -``` - -**性能提升**:3 个独立分量 = 3 倍加速! - -### 4.4 并发合并 - -同名实体需要去重,相同关系需要聚合。 - -```mermaid -flowchart TD - subgraph Before["合并前"] - A1["张三
数据库负责人"] - A2["张三
擅长 PostgreSQL"] - A3["张三
带领团队"] - end - - Merge[智能合并] - - subgraph After["合并后"] - B1["张三
数据库团队负责人,
擅长 PostgreSQL,
带领团队完成多个项目"] - end - - A1 --> Merge - A2 --> Merge - A3 --> Merge - Merge --> B1 - - style Before fill:#ffccbc - style After fill:#c5e1a5 -``` - -**细粒度锁**:只锁定正在合并的实体,其他实体可以并发处理。 - -### 4.5 多存储写入 - -知识图谱写入三个存储系统: - -```mermaid -flowchart LR - KG[知识图谱] --> G[图数据库
图查询] - KG --> V[向量数据库
语义搜索] - KG --> T[文本存储
全文检索] - - style KG fill:#e1f5ff - style G fill:#bbdefb - style V fill:#c5e1a5 - style T fill:#ffccbc -``` - -不同存储支持不同类型的查询,互相补充。 - -## 5. 核心技术设计 - -这一章介绍 ApeRAG 图索引的核心技术设计,包括数据隔离、并发控制等。 - -> 💡 **阅读建议**:这些是系统架构和实现细节,主要面向开发者和技术决策者。 - -### 5.1 workspace 数据隔离 - -每个 Collection 拥有独立的命名空间,实现完全的数据隔离。 - -**命名规范**: - -```python -# 实体命名 -entity:{entity_name}:{workspace} -# 示例 -entity:张三:collection_abc123 - -# 关系命名 -relationship:{source}:{target}:{workspace} -# 示例 -relationship:张三:数据库团队:collection_abc123 -``` - -**隔离效果**: - -```mermaid -graph TB - subgraph Collection_A[Collection A - 公司文档] - A1[entity:张三:A] --> A2[entity:数据库团队:A] - end - - subgraph Collection_B[Collection B - 学校文档] - B1[entity:张三:B] --> B2[entity:计算机系:B] - end - - style Collection_A fill:#bbdefb - style Collection_B fill:#c5e1a5 -``` - -两个 Collection 中的"张三"完全独立,互不干扰! - -### 5.2 无状态实例管理 - -每个处理任务创建独立的图索引实例,处理完成后销毁。 - -**生命周期管理**: - -```mermaid -sequenceDiagram - participant C as Celery Task - participant M as Manager - participant R as Graph Index Instance - participant S as Storage - - C->>M: process_document() - M->>R: create_instance() - R->>S: 初始化存储连接 - R->>R: 处理文档 - R->>S: 写入数据 - R-->>M: 返回结果 - M-->>C: 任务完成 - Note over R: 实例被销毁,资源释放 -``` - -**优势**: - -- ✅ 零状态污染:每个任务独立,不会互相干扰 -- ✅ 自动资源管理:实例销毁时自动释放资源 -- ✅ 易于扩展:可以同时运行多个 Worker - -### 5.3 连通分量并发优化 - -通过图拓扑分析,实现智能并发处理。 - -**算法原理**: - -```mermaid -graph TB - subgraph Input[输入:实体关系网络] - I1[实体 1] --> I2[实体 2] - I2 --> I3[实体 3] - - I4[实体 4] --> I5[实体 5] - - I6[实体 6] - end - - Algorithm[BFS 算法] - - subgraph Output[输出:3 个连通分量] - O1[分量 1
3 个实体] - O2[分量 2
2 个实体] - O3[分量 3
1 个实体] - end - - Input --> Algorithm - Algorithm --> Output - - style Input fill:#ffccbc - style Algorithm fill:#fff59d - style Output fill:#c5e1a5 -``` - -**性能提升**:3 个分量并发处理 = 3 倍加速! - -### 5.4 细粒度并发控制 - -实现实体级别的精确锁定: - -**锁的层次**: - -```mermaid -graph TD - A[全局锁 - 传统方案] -->|太粗| B[所有实体串行处理] - - C[实体锁 - ApeRAG] -->|刚好| D[只锁定需要合并的实体] - - style A fill:#ffccbc - style B fill:#ffccbc - style C fill:#c5e1a5 - style D fill:#c5e1a5 -``` - -**锁策略**: -1. 提取阶段无锁:完全并行 -2. 合并阶段加锁:只锁需要的实体 -3. 排序获取锁:避免死锁 - -### 5.5 智能摘要生成 - -自动压缩过长的描述内容: - -```python -if len(description) > 2000 tokens: - summary = await llm_summarize(description) -else: - summary = description -``` - -**效果**:2500 tokens 压缩到 200 tokens,保留核心信息。 - -### 5.6 多存储后端支持 - -ApeRAG 支持两种图数据库:Neo4j 和 PostgreSQL。 - -**如何选择?** - -| 场景 | 推荐方案 | 原因 | -|------|---------|------| -| **小规模**(< 10万实体) | PostgreSQL | 运维简单,成本低 | -| **中等规模**(10-100万) | PostgreSQL 或 Neo4j | 根据查询复杂度选择 | -| **大规模**(> 100万) | Neo4j | 图查询性能更好 | -| **预算有限** | PostgreSQL | 无需额外部署 | -| **复杂图算法** | Neo4j | 内置图算法支持 | - -**切换方式**: - -```bash -# 使用 PostgreSQL(默认) -export GRAPH_INDEX_GRAPH_STORAGE=PGOpsSyncGraphStorage - -# 使用 Neo4j -export GRAPH_INDEX_GRAPH_STORAGE=Neo4JSyncStorage -``` - -**性能对比**: - -| 操作 | PostgreSQL | Neo4j | -|------|-----------|-------| -| **简单查询**(1-2跳) | 快 | 快 | -| **复杂查询**(3+跳) | 中 | 快 | -| **批量写入** | 快 | 中 | -| **图算法** | 需要自己实现 | 内置支持 | - -## 6. 完整数据流 - -整个图索引构建过程是一个数据转换流水线,从非结构化文本到结构化知识图谱: - -```mermaid -flowchart TD - A[原始文档] --> B[清理预处理] - B --> C[智能分块] - C --> D[Chunks] - - D --> E[LLM 并发提取] - E --> F[原始实体列表] - E --> G[原始关系列表] - - F --> H[构建邻接图] - G --> H - H --> I[BFS 发现连通分量] - I --> J[分组并发处理] - - J --> K[实体去重合并] - J --> L[关系聚合] - - K --> M{描述长度检查} - M -->|过长| N[LLM 摘要] - M -->|适中| O[保留原文] - N --> P[最终实体] - O --> P - - L --> Q{描述长度检查} - Q -->|过长| R[LLM 摘要] - Q -->|适中| S[保留原文] - R --> T[最终关系] - S --> T - - P --> U[图数据库] - P --> V[向量数据库] - T --> U - T --> V - D --> W[文本存储] - - U --> X[知识图谱完成] - V --> X - W --> X - - style A fill:#e1f5ff - style E fill:#fff59d - style I fill:#f3e5f5 - style J fill:#c5e1a5 - style X fill:#c8e6c9 -``` - -### 数据转换示例 - -让我们用一个具体例子,看看数据是如何一步步转换的: - -**输入文档**: - -```text -张三是数据库团队的负责人,他擅长 PostgreSQL 和 MySQL。 -李四在前端团队工作,经常和张三的团队协作开发后台管理系统。 -王五是财务部的会计,负责公司的财务报表。 -``` - -**Step 1: 分块** - -```json -[ - { - "chunk_id": "chunk-001", - "content": "张三是数据库团队的负责人,他擅长 PostgreSQL 和 MySQL。", - "tokens": 25 - }, - { - "chunk_id": "chunk-002", - "content": "李四在前端团队工作,经常和张三的团队协作开发后台管理系统。", - "tokens": 28 - }, - { - "chunk_id": "chunk-003", - "content": "王五是财务部的会计,负责公司的财务报表。", - "tokens": 20 - } -] -``` - -**Step 2: 实体关系提取** - -```json -{ - "entities": [ - {"name": "张三", "type": "人物", "source": "chunk-001"}, - {"name": "数据库团队", "type": "组织", "source": "chunk-001"}, - {"name": "PostgreSQL", "type": "技术", "source": "chunk-001"}, - {"name": "MySQL", "type": "技术", "source": "chunk-001"}, - {"name": "李四", "type": "人物", "source": "chunk-002"}, - {"name": "前端团队", "type": "组织", "source": "chunk-002"}, - {"name": "王五", "type": "人物", "source": "chunk-003"}, - {"name": "财务部", "type": "组织", "source": "chunk-003"} - ], - "relationships": [ - {"source": "张三", "target": "数据库团队", "relation": "负责"}, - {"source": "张三", "target": "PostgreSQL", "relation": "擅长"}, - {"source": "张三", "target": "MySQL", "relation": "擅长"}, - {"source": "李四", "target": "前端团队", "relation": "属于"}, - {"source": "李四", "target": "张三", "relation": "协作"}, - {"source": "王五", "target": "财务部", "relation": "属于"} - ] -} -``` - -**Step 3: 连通分量分析** - -``` -连通分量 1(技术部门): -- 实体:张三、李四、数据库团队、前端团队、PostgreSQL、MySQL -- 关系:6 条 - -连通分量 2(财务部门): -- 实体:王五、财务部 -- 关系:1 条 -``` - -**Step 4: 并发合并** - -两个分量可以并行处理! - -**Step 5: 最终知识图谱** - -```mermaid -graph LR - subgraph 技术部门 - 张三 -->|负责| 数据库团队 - 张三 -->|擅长| PostgreSQL - 张三 -->|擅长| MySQL - 李四 -->|属于| 前端团队 - 李四 -->|协作| 张三 - end - - subgraph 财务部门 - 王五 -->|属于| 财务部 - end - - style 技术部门 fill:#bbdefb - style 财务部门 fill:#c5e1a5 -``` - -### 性能优化特性 - -1. **细粒度并发控制** - - 实体级别的锁:`entity:张三:collection_abc` - - 只在合并时加锁,提取时完全并行 - -2. **连通分量并发** - - 技术部门和财务部门可以并行处理 - - 零锁竞争,充分利用多核 CPU - -3. **智能摘要** - - 描述 < 2000 tokens:保留原文 - - 描述 > 2000 tokens:LLM 摘要压缩 - -## 7. 性能优化策略 - -### 7.1 并发度控制 - -图索引构建涉及大量的 LLM 调用和数据库操作,需要合理控制并发度。 - -**并发层次**: - -```mermaid -graph TB - A[文档级并发] --> B[Chunk 级并发] - B --> C[连通分量级并发] - C --> D[实体级并发] - - A1[Celery Workers
多个文档同时处理] --> A - B1[LLM 并发调用
多个 chunks 同时提取] --> B - C1[分量并行合并
多个分量同时处理] --> C - D1[实体并发合并
不同实体同时合并] --> D - - style A fill:#e3f2fd - style B fill:#fff3e0 - style C fill:#f3e5f5 - style D fill:#e8f5e9 -``` - -**并发参数配置**: - -| 参数 | 默认值 | 说明 | -|------|--------|------| -| `llm_model_max_async` | 20 | LLM 并发调用数 | -| `embedding_func_max_async` | 16 | Embedding 并发调用数 | -| `max_batch_size` | 32 | 批量处理大小 | - -**调优建议**: - -```python -# 场景 1:LLM API 限流严格 -llm_model_max_async = 5 # 降低并发,避免触发限流 - -# 场景 2:性能充足,想提速 -llm_model_max_async = 50 # 提高并发,加快处理速度 - -# 场景 3:内存有限 -max_batch_size = 16 # 减小批量大小,降低内存占用 -``` - -### 7.2 LLM 调用优化 - -LLM 调用是最耗时的环节,主要优化策略: - -1. **并发调用**:多个 chunks 同时提取(默认并发 20 个) -2. **批量处理**:减少 LLM 调用次数 -3. **缓存复用**:相似描述复用摘要结果 - -**性能提升**:并发调用比串行快 4 倍。 - -### 7.3 存储优化 - -批量写入可以显著提升性能: - -| 方式 | 100 个实体写入时间 | -|------|------------------| -| 逐个写入 | ~10 秒 | -| 批量写入(32 个/批) | ~1 秒 | - -**优化效果**:10 倍速度提升! - -### 7.4 内存优化 - -大文档处理的内存管理策略: - -- 流式分块:不一次性加载整个文档 -- 及时释放:处理完立即释放内存 -- 分批处理:控制内存峰值 - -### 7.5 性能监控 - -系统会输出详细的性能统计: - -``` -图索引构建完成: -✓ 文档分块:10 个 chunks,耗时 0.5 秒 -✓ 实体提取:120 个实体,耗时 25 秒 -✓ 关系提取:85 个关系,耗时 25 秒 -✓ 并发合并:耗时 15 秒 -✓ 存储写入:耗时 2 秒 -━━━━━━━━━━━━━━━━━━━━━━━━━ -总耗时:42.7 秒 -``` - -**瓶颈分析**:实体/关系提取占 60% 时间,可通过提高 LLM 并发度优化。 - -## 8. 配置参数 - -### 8.1 核心配置 - -图索引构建可以通过以下参数进行调优: - -**分块参数**: - -```python -# 分块大小(tokens) -CHUNK_TOKEN_SIZE = 1200 - -# 重叠大小(tokens) -CHUNK_OVERLAP_TOKEN_SIZE = 100 -``` - -**调优建议**: -- 小文档(< 5000 tokens):`CHUNK_TOKEN_SIZE = 800` -- 大文档(> 50000 tokens):`CHUNK_TOKEN_SIZE = 1500` -- 需要更多上下文:增加 `CHUNK_OVERLAP_TOKEN_SIZE` - -**并发参数**: - -```python -# LLM 并发调用数 -LLM_MODEL_MAX_ASYNC = 20 - -# Embedding 并发调用数 -EMBEDDING_FUNC_MAX_ASYNC = 16 - -# 批量处理大小 -MAX_BATCH_SIZE = 32 -``` - -**调优建议**: -- LLM API 限流严格:降低 `LLM_MODEL_MAX_ASYNC` 到 5-10 -- 性能充足想提速:提高到 50-100 -- 内存有限:降低 `MAX_BATCH_SIZE` 到 16 - -**实体提取参数**: - -```python -# 实体提取重试次数(0 = 只提取 1 次) -ENTITY_EXTRACT_MAX_GLEANING = 0 - -# 摘要最大 token 数 -SUMMARY_TO_MAX_TOKENS = 2000 - -# 强制摘要的描述片段数 -FORCE_LLM_SUMMARY_ON_MERGE = 10 -``` - -**调优建议**: -- 提取质量重要:`ENTITY_EXTRACT_MAX_GLEANING = 1`(多提取一次) -- 追求速度:`ENTITY_EXTRACT_MAX_GLEANING = 0` -- 描述经常很长:降低 `SUMMARY_TO_MAX_TOKENS` 到 1000 - -### 8.2 知识图谱配置 - -在 Collection 配置中可以设置: - -```json -{ - "knowledge_graph_config": { - "language": "simplified chinese", - "entity_types": [ - "organization", - "person", - "geo", - "event", - "product", - "technology", - "date", - "category" - ] - } -} -``` - -**参数说明**: - -- **language**:提取语言,影响 LLM 提示词 - - `simplified chinese`:简体中文 - - `English`:英文 - - `traditional chinese`:繁体中文 - -- **entity_types**:要提取的实体类型 - - 默认:8 种类型(组织、人物、地点、事件、产品、技术、日期、类别) - - 可自定义:比如只提取人物和组织 - -### 8.3 存储配置 - -通过环境变量配置存储后端: - -```bash -# KV 存储(键值对) -export GRAPH_INDEX_KV_STORAGE=PGOpsSyncKVStorage - -# 向量存储 -export GRAPH_INDEX_VECTOR_STORAGE=PGOpsSyncVectorStorage - -# 图存储 -export GRAPH_INDEX_GRAPH_STORAGE=Neo4JSyncStorage -# 或者使用 PostgreSQL -export GRAPH_INDEX_GRAPH_STORAGE=PGOpsSyncGraphStorage -``` - -**存储选择建议**: - -| 场景 | KV 存储 | 向量存储 | 图存储 | -|------|---------|---------|--------| -| **默认** | PostgreSQL | PostgreSQL | PostgreSQL | -| **高性能向量搜索** | PostgreSQL | Qdrant | Neo4j | -| **大规模图谱** | PostgreSQL | Qdrant | Neo4j | -| **简单部署** | PostgreSQL | PostgreSQL | PostgreSQL | - -### 8.4 完整配置示例 - -```bash -# 分块配置 -export CHUNK_TOKEN_SIZE=1200 -export CHUNK_OVERLAP_TOKEN_SIZE=100 - -# 并发配置 -export LLM_MODEL_MAX_ASYNC=20 -export MAX_BATCH_SIZE=32 - -# 提取配置 -export ENTITY_EXTRACT_MAX_GLEANING=0 -export SUMMARY_TO_MAX_TOKENS=2000 - -# 存储配置 -export GRAPH_INDEX_KV_STORAGE=PGOpsSyncKVStorage -export GRAPH_INDEX_VECTOR_STORAGE=PGOpsSyncVectorStorage -export GRAPH_INDEX_GRAPH_STORAGE=PGOpsSyncGraphStorage - -# 数据库连接(PostgreSQL) -export POSTGRES_HOST=127.0.0.1 -export POSTGRES_PORT=5432 -export POSTGRES_DB=aperag -export POSTGRES_USER=postgres -export POSTGRES_PASSWORD=your_password - -# 数据库连接(Neo4j,可选) -export NEO4J_HOST=127.0.0.1 -export NEO4J_PORT=7687 -export NEO4J_USERNAME=neo4j -export NEO4J_PASSWORD=your_password -``` - -## 9. 实际应用场景 - -图索引特别适合以下场景: - -### 9.1 企业知识库 - -**场景描述**:公司有大量的技术文档、组织架构、项目资料。 - -**图索引的价值**: - -- ✅ 理解人员关系:谁和谁在一起工作过 -- ✅ 追溯项目历史:哪些人参与了哪些项目 -- ✅ 技术栈分析:哪个团队用什么技术 -- ✅ 知识传承:某个领域的专家是谁 - -**查询示例**: - -``` -用户:"张三参与过哪些项目?" -图索引:查询 张三 --参与--> 项目 的关系 -结果:项目 A、项目 B、项目 C - -用户:"数据库团队都有哪些人?" -图索引:查询 人物 --属于--> 数据库团队 的关系 -结果:张三、李四、王五 -``` - -### 8.2 研究论文分析 - -**场景描述**:分析大量学术论文,理解研究脉络。 - -**图索引的价值**: - -- ✅ 作者合作网络:谁和谁合作过 -- ✅ 引用关系:哪些论文互相引用 -- ✅ 研究主题:某个领域的核心概念 -- ✅ 技术演进:技术如何发展的 - -**查询示例**: - -``` -用户:"Graph RAG 相关的研究有哪些?" -图索引:查询 论文 --研究--> Graph RAG 的关系 -结果:论文 A、论文 B、论文 C - -用户:"某作者和谁合作过?" -图索引:查询 作者 --合作--> 其他作者 的关系 -结果:合作者列表及合作项目 -``` - -### 8.3 产品文档 - -**场景描述**:软件产品的用户手册、API 文档。 - -**图索引的价值**: - -- ✅ 功能依赖:某个功能依赖哪些其他功能 -- ✅ API 关联:哪些 API 经常一起使用 -- ✅ 配置关系:某个配置项影响哪些功能 -- ✅ 问题诊断:出现某个错误可能是什么原因 - -**查询示例**: - -``` -用户:"如何配置图索引?" -图索引:查询 配置项 --影响--> 图索引 的关系 -结果:GRAPH_INDEX_GRAPH_STORAGE、knowledge_graph_config - -用户:"Neo4j 和 PostgreSQL 有什么区别?" -图索引:查询 Neo4j、PostgreSQL 的属性和关系 -结果:性能对比、适用场景、配置方式 -``` - -### 8.4 对话场景对比 - -让我们看看不同检索方式在实际对话中的表现: - -**问题:"张三和李四是什么关系?"** - -| 检索方式 | 能否回答 | 回答质量 | -|---------|---------|---------| -| **纯向量检索** | ⚠️ 部分 | 找到提到两人的段落,但不清楚关系 | -| **纯全文检索** | ⚠️ 部分 | 找到包含"张三"和"李四"的段落 | -| **图索引** | ✅ 可以 | 直接返回:张三和李四是协作关系 | - -**问题:"PostgreSQL 配置文件在哪?"** - -| 检索方式 | 能否回答 | 回答质量 | -|---------|---------|---------| -| **纯向量检索** | ✅ 可以 | 找到相关配置段落 | -| **纯全文检索** | ✅ 可以 | 精确匹配"PostgreSQL"和"配置" | -| **图索引** | ✅ 可以 | 找到 PostgreSQL --配置--> 文件 的关系 | - -**问题:"如何提升系统性能?"** - -| 检索方式 | 能否回答 | 回答质量 | -|---------|---------|---------| -| **纯向量检索** | ✅ 强 | 找到所有性能优化相关内容 | -| **纯全文检索** | ⚠️ 中 | 需要精确关键词"性能"、"优化" | -| **图索引** | ✅ 强 | 找到 优化方法 --提升--> 性能 的关系 | - -**最佳实践**:结合使用多种检索方式! - -## 10. 总结 - -ApeRAG 的图索引提供了生产级的知识图谱构建能力,具有高性能、高可靠性和易扩展的特点。 - -### 关键特性 - -1. **workspace 数据隔离**:每个 Collection 完全独立,支持真正的多租户 -2. **无状态架构**:每个任务独立实例,零状态污染 -3. **连通分量并发**:智能并发策略,性能提升 2-3 倍 -4. **细粒度锁管理**:实体级别的锁,最大化并发度 -5. **智能摘要**:自动压缩过长描述,节省存储和提升检索效率 -6. **多存储支持**:灵活选择 Neo4j 或 PostgreSQL - -### 适用场景 - -- ✅ **企业知识库**:理解组织结构、人员关系、项目历史 -- ✅ **研究论文分析**:作者合作网络、引用关系、研究脉络 -- ✅ **产品文档**:功能依赖、配置关系、问题诊断 -- ✅ **任何需要理解"关系"的场景** - -### 性能表现 - -- 处理 10,000 个实体:约 2-5 分钟(取决于 LLM 速度) -- 连通分量并发:性能提升 2-3 倍 -- 内存占用:约 400 MB(10,000 个实体) -- 存储空间:约 100 MB(10,000 个实体) - -### 下一步 - -图索引构建完成后,就可以进行图谱检索了。ApeRAG 支持三种图谱查询模式: - -- **Local 模式**:查询某个实体的局部信息 -- **Global 模式**:查询整体关系和模式 -- **Hybrid 模式**:综合性查询 - -详细的检索流程请参考 [系统架构文档](./architecture.md#42-知识图谱查询)。 - ---- - -## 相关文档 - -- 📋 [系统架构](./architecture.md) - ApeRAG 整体架构设计 -- 📖 [实体提取与合并机制](./lightrag_entity_extraction_and_merging.md) - 核心算法详解 -- 🔗 [连通分量优化](./connected_components_optimization.md) - 并发优化原理 -- 🌐 [索引链路架构](./indexing_architecture.md) - 完整索引流程 diff --git a/docs/zh-CN/design/indexing_architecture.md b/docs/zh-CN/design/indexing_architecture.md deleted file mode 100644 index 48846d9a8..000000000 --- a/docs/zh-CN/design/indexing_architecture.md +++ /dev/null @@ -1,556 +0,0 @@ -# ApeRAG 索引链路架构设计文档 - -## 概述 - -ApeRAG的索引链路架构采用双链路设计模式,将索引管理分为前端链路(Frontend Chain)和后端链路(Backend Chain),通过状态驱动的调谐机制实现文档索引的异步处理。前端链路负责快速响应用户操作并设置索引状态,后端链路通过定时调谐器检测状态变化并调度异步任务执行实际的索引操作。 - -> 🚀 **深入阅读**:了解 Graph Index 的详细创建流程,请继续阅读 [Graph Index 创建流程技术文档](./graph_index_creation_zh.md) - -## 架构概览 - -```mermaid -graph TB - subgraph "Frontend Chain (同步快速响应)" - A[API请求] --> B[IndexManager] - B --> C[写入DocumentIndex表] - C --> D[设置status=PENDING, version++] - end - - subgraph "Backend Chain (异步任务处理)" - E[定时任务reconcile_indexes_task] --> F[IndexReconciler.reconcile_all] - F --> G[检测版本不匹配或状态变更需求] - G --> H[TaskScheduler调度异步任务] - end - - subgraph "Celery任务执行层" - H --> I[create_document_indexes_workflow] - I --> J[parse_document_task] - J --> K[trigger_create_indexes_workflow] - K --> L[group并行执行] - L --> M[create_index_task.VECTOR] - L --> N[create_index_task.FULLTEXT] - L --> O[create_index_task.GRAPH] - M --> P[chord回调] - N --> P - O --> P - P --> Q[notify_workflow_complete] - end - - subgraph "状态反馈" - Q --> R[IndexTaskCallbacks] - R --> S[更新status=ACTIVE, observed_version] - S --> T[下次调谐检查] - end - - D -.-> E - T -.-> E -``` - -## 核心设计思路 - -### 1. 双链路分离 - -**前端链路(Frontend Chain)**: -- **目标**:快速响应用户操作,不阻塞API请求 -- **实现**:只操作数据库表,设置期望状态,立即返回 -- **代码**:`aperag/index/manager.py` 中的 `IndexManager` - -**后端链路(Backend Chain)**: -- **目标**:异步执行耗时的索引操作,支持重试和错误恢复 -- **实现**:通过定时任务持续扫描状态变化,调度异步任务 -- **代码**:`aperag/index/reconciler.py` 中的 `IndexReconciler` - -### 2. 单一状态驱动调谐 - -通过数据库表`DocumentIndex`记录每个文档索引的状态和版本: - -```python -class DocumentIndex(BaseModel): - document_id: str - index_type: DocumentIndexType # VECTOR/FULLTEXT/GRAPH - status: DocumentIndexStatus # PENDING/CREATING/ACTIVE/DELETING/DELETION_IN_PROGRESS/FAILED - version: int # 版本号,递增触发重建 - observed_version: int # 上次处理的版本 -``` - -关键状态含义: -- **PENDING**:等待处理(需要创建/更新) -- **CREATING**:任务已认领,创建/更新进行中 -- **ACTIVE**:索引已是最新状态,可用于搜索 -- **DELETING**:已请求删除 -- **DELETION_IN_PROGRESS**:任务已认领,删除进行中 -- **FAILED**:上次操作失败 - -调谐器定时扫描所有记录,基于以下条件触发相应操作: -- 版本不匹配:`observed_version < version` 表示需要更新 -- 版本 = 1 且 observed_version = 0:表示需要初始创建 -- 状态 = DELETING:表示需要删除 - -### 3. TaskScheduler抽象层设计 - -**设计优势**: -- **业务逻辑与任务系统解耦**:Reconciler只关心"需要执行什么操作",不关心"用什么系统执行" -- **多调度器支持**:可以在Celery、Prefect/Airflow等工作流引擎之间切换 -- **测试友好**:测试时可以使用不同的调度器,便于调试 - -```python -# 抽象接口 -class TaskScheduler(ABC): - def schedule_create_index(self, document_id: str, index_types: List[str], context: dict = None) -> str - def schedule_update_index(self, document_id: str, index_types: List[str], context: dict = None) -> str - def schedule_delete_index(self, document_id: str, index_types: List[str]) -> str - -# Reconciler使用抽象接口 -class IndexReconciler: - def __init__(self, scheduler_type: str = "celery"): - self.task_scheduler = create_task_scheduler(scheduler_type) - - def _reconcile_document_operations(self, document_id: str, claimed_indexes: List[dict]): - # 只调用抽象接口,不关心具体实现 - if create_types: - self.task_scheduler.schedule_create_index(document_id, create_types, context) - if update_types: - self.task_scheduler.schedule_update_index(document_id, update_types, context) -``` - -**Celery任务入口与业务代码分离**: -- Celery任务函数(`config/celery_tasks.py`):负责任务调度、参数序列化、错误重试 -- 业务逻辑(`aperag/tasks/document.py`):负责具体的索引创建逻辑 -- 这种分离使得业务逻辑可以独立测试,也便于在不同任务系统间迁移 - -### 4. 创建与更新操作区分 - -系统明确区分创建和更新操作: - -**创建操作** (version = 1, observed_version = 0): -- 针对新文档或新索引类型 -- 使用 `schedule_create_index` 和 `create_index_task` -- 从零开始创建索引 - -**更新操作** (version > 1, observed_version < version): -- 针对需要重建的现有索引 -- 使用 `schedule_update_index` 和 `update_index_task` -- 使用新内容更新现有索引 - -这种区分允许对每种操作类型采用不同的处理策略和优化。 - -## 异步任务体系 - -### 当前异步任务列表 - -ApeRAG当前定义了以下异步任务,每个任务都有明确的职责分工: - -| 任务名称 | 功能 | 重试次数 | 位置 | -|---------|------|---------|------| -| `parse_document_task` | 解析文档内容,提取文本和元数据 | 3次 | config/celery_tasks.py | -| `create_index_task` | 创建单个类型的索引(VECTOR/FULLTEXT/GRAPH) | 3次 | config/celery_tasks.py | -| `update_index_task` | 更新单个类型的索引 | 3次 | config/celery_tasks.py | -| `delete_index_task` | 删除单个类型的索引 | 3次 | config/celery_tasks.py | -| `trigger_create_indexes_workflow` | 动态扇出创建索引任务 | 无重试 | config/celery_tasks.py | -| `trigger_update_indexes_workflow` | 动态扇出更新索引任务 | 无重试 | config/celery_tasks.py | -| `trigger_delete_indexes_workflow` | 动态扇出删除索引任务 | 无重试 | config/celery_tasks.py | -| `notify_workflow_complete` | 聚合工作流结果并通知完成 | 无重试 | config/celery_tasks.py | -| `reconcile_indexes_task` | 定时调谐器任务 | 无重试 | config/celery_tasks.py | - -### 任务设计原则 - -1. **细粒度任务**:每个索引类型(VECTOR/FULLTEXT/GRAPH)都是独立的任务,支持单独重试 -2. **动态编排**:通过trigger任务在运行时决定要执行哪些索引任务 -3. **分层重试**:业务任务支持重试,编排任务不重试 -4. **状态回调**:每个任务完成后都会回调更新数据库状态 -5. **版本验证**:任务验证版本号以防止过期操作 - -### 并发执行设计 - -#### Celery Group + Chord模式 - -使用Celery的`group`实现并行执行,`chord`实现结果聚合: - -```python -# Group:使用context并行执行多个索引任务 -parallel_index_tasks = group([ - create_index_task.s(document_id, index_type, parsed_data_dict, context) - for index_type in index_types -]) - -# Chord:等待所有并行任务完成后执行回调 -workflow_chord = chord( - parallel_index_tasks, - notify_workflow_complete.s(document_id, "create", index_types) -) -``` - -#### 任务串联机制 - -通过Celery的`chain`实现任务串联,通过`signature`传递参数: - -```python -# 串联执行:解析 -> 动态扇出并传递context -workflow_chain = chain( - parse_document_task.s(document_id), - trigger_create_indexes_workflow.s(document_id, index_types, context) -) -``` - -#### 参数传递和上下文流 - -```python -# context包含每个索引类型的版本信息 -context = { - "VECTOR_version": 2, - "FULLTEXT_version": 1, - "GRAPH_version": 3 -} - -# 每个索引任务从context中提取其特定版本 -def create_index_task(document_id, index_type, parsed_data_dict, context): - target_version = context.get(f'{index_type}_version') - # 处理前验证版本 -``` - -## 具体执行链路示例 - -### 创建索引执行链路 - -以用户上传文档触发索引创建为例: - -```python -# 1. 前端链路(同步,毫秒级) -API调用 -> IndexManager.create_indexes() - ↓ -写入DocumentIndex表记录: -{ - document_id: "doc123", - index_type: "VECTOR", - status: "PENDING", - version: 1, - observed_version: 0 -} - ↓ -API立即返回200 - -# 2. 后端链路(异步,分钟级) -定时任务reconcile_indexes_task(每30秒执行) - ↓ -IndexReconciler.reconcile_all() - ↓ -检测到version=1, observed_version=0(需要创建操作) - ↓ -CeleryTaskScheduler.schedule_create_index(doc123, ["VECTOR", "FULLTEXT", "GRAPH"], context) - ↓ -create_document_indexes_workflow.delay() - -# 3. Celery任务执行(异步,分钟到小时级) -parse_document_task("doc123") -├── 下载文档文件到本地临时目录 -├── 调用docparser解析文档内容 -├── 返回ParsedDocumentData.to_dict() -└── 更新status="CREATING" - ↓ -trigger_create_indexes_workflow(parsed_data, "doc123", ["VECTOR", "FULLTEXT", "GRAPH"], context) -├── 创建group并行任务并传递版本context -└── 启动chord等待 - ↓ -并行执行: -├── create_index_task("doc123", "VECTOR", parsed_data, context) -│ ├── 从context提取VECTOR_version -│ ├── 验证版本仍与数据库匹配 -│ ├── 调用vector_indexer.create_index() -│ ├── 生成embedding并存入向量数据库 -│ └── 回调IndexTaskCallbacks.on_index_created(target_version) -├── create_index_task("doc123", "FULLTEXT", parsed_data, context) -│ ├── 从context提取FULLTEXT_version -│ ├── 验证版本仍与数据库匹配 -│ ├── 调用fulltext_indexer.create_index() -│ ├── 建立全文搜索索引 -│ └── 回调IndexTaskCallbacks.on_index_created(target_version) -└── create_index_task("doc123", "GRAPH", parsed_data, context) - ├── 从context提取GRAPH_version - ├── 验证版本仍与数据库匹配 - ├── 调用graph_indexer.create_index() - ├── 构建知识图谱 - └── 回调IndexTaskCallbacks.on_index_created(target_version) - ↓ -notify_workflow_complete([result1, result2, result3], "doc123", "create", ["VECTOR", "FULLTEXT", "GRAPH"]) -├── 聚合所有索引任务结果 -├── 记录工作流完成日志 -└── 返回WorkflowResult -``` - -### 更新索引执行链路 - -用户修改文档内容触发索引更新: - -```python -# 1. 前端链路 -API调用 -> IndexManager.rebuild_indexes() - ↓ -所有现有索引记录version字段+1: -version: 1 -> 2 (触发重建) - ↓ -API立即返回 - -# 2. 后端链路 -reconcile_indexes_task检测到版本不匹配 - ↓ -version=2, observed_version=1, version > 1(需要更新操作) - ↓ -schedule_update_index() -> update_document_indexes_workflow() - -# 3. 任务执行(与创建类似但使用更新任务) -parse_document_task -> trigger_update_indexes_workflow -> 并行update_index_task -``` - -### 删除索引执行链路 - -用户删除文档触发索引删除: - -```python -# 1. 前端链路 -API调用 -> IndexManager.delete_indexes() - ↓ -设置status="DELETING" - ↓ -API立即返回 - -# 2. 后端链路 -检测到status=DELETING - ↓ -schedule_delete_index() -> delete_document_indexes_workflow() - -# 3. 任务执行(无需解析) -trigger_delete_indexes_workflow -> 并行delete_index_task -├── 从向量数据库删除embeddings -├── 从全文搜索引擎删除文档 -└── 从知识图谱删除节点和关系 -``` - -## 异常处理机制 - -### 任务级别异常处理 - -每个Celery任务都配置了自动重试: - -```python -@current_app.task(bind=True, autoretry_for=(Exception,), retry_kwargs={'max_retries': 3, 'countdown': 60}) -def create_index_task(self, document_id: str, index_type: str, parsed_data_dict: dict, context: dict = None): - try: - # 从context提取并验证版本 - target_version = context.get(f'{index_type}_version') if context else None - - # 处理前双重检查版本仍与数据库匹配 - # ... 版本验证逻辑 ... - - # 业务逻辑 - result = document_index_task.create_index(document_id, index_type, parsed_data) - if result.success: - self._handle_index_success(document_id, index_type, target_version, result.data) - else: - # 业务逻辑失败但不抛异常,避免无意义重试 - if self.request.retries >= self.max_retries: - self._handle_index_failure(document_id, index_type, result.error) - return result.to_dict() - except Exception as e: - # 只有在重试次数用完后才标记失败 - if self.request.retries >= self.max_retries: - self._handle_index_failure(document_id, index_type, str(e)) - raise # 继续抛出异常触发重试 -``` - -### 工作流级别异常处理 - -通过`notify_workflow_complete`聚合错误: - -```python -def notify_workflow_complete(self, index_results: List[dict], document_id: str, operation: str, index_types: List[str]): - successful_tasks = [] - failed_tasks = [] - - for result_dict in index_results: - result = IndexTaskResult.from_dict(result_dict) - if result.success: - successful_tasks.append(result.index_type) - else: - failed_tasks.append(f"{result.index_type}: {result.error}") - - # 确定整体状态 - if not failed_tasks: - status = TaskStatus.SUCCESS # 全部成功 - elif successful_tasks: - status = TaskStatus.PARTIAL_SUCCESS # 部分成功 - else: - status = TaskStatus.FAILED # 全部失败 -``` - -### 状态管理和错误恢复 - -通过数据库状态和版本验证追踪错误: - -```python -class IndexTaskCallbacks: - @staticmethod - def on_index_created(document_id: str, index_type: str, target_version: int, index_data: str = None): - """带版本验证的任务成功回调""" - # 使用带版本验证的原子更新 - update_stmt = ( - update(DocumentIndex) - .where( - and_( - DocumentIndex.document_id == document_id, - DocumentIndex.index_type == DocumentIndexType(index_type), - DocumentIndex.status == DocumentIndexStatus.CREATING, - DocumentIndex.version == target_version, # 关键:验证版本 - ) - ) - .values( - status=DocumentIndexStatus.ACTIVE, - observed_version=target_version, # 标记此版本已处理 - index_data=index_data, - error_message=None, - ) - ) -``` - -### 错误恢复策略 - -1. **自动重试**:任务级别的3次自动重试,解决临时网络或资源问题 -2. **版本验证**:通过在任务执行时检查版本来防止过期操作 -3. **状态重置**:用户可以手动重置失败状态,触发重新执行 -4. **部分重试**:只重试失败的索引类型,不影响已成功的索引 -5. **降级处理**:某些索引失败不影响文档的可搜索性(如graph索引失败但vector索引成功) - -## 代码组织结构 - -### 目录结构 - -``` -aperag/ -├── index/ # 索引管理核心模块 -│ ├── manager.py # 索引管理器(前端操作) -│ ├── reconciler.py # 后端调谐器 -│ ├── base.py # 索引器基类定义 -│ ├── vector_index.py # 向量索引实现 -│ ├── fulltext_index.py # 全文索引实现 -│ └── graph_index.py # 图索引实现 -├── tasks/ # 任务相关模块 -│ ├── models.py # 任务数据结构定义 -│ ├── scheduler.py # 任务调度器抽象层 -│ ├── document.py # 文档处理业务逻辑 -│ └── utils.py # 任务工具函数 -└── db/ - └── models.py # 数据库模型定义 - -config/ -└── celery_tasks.py # Celery任务定义 -``` - -### 核心接口设计 - -#### 索引管理接口 -```python -# aperag/index/manager.py -class IndexManager: - def create_indexes(self, document_id, index_types, created_by, session) - def rebuild_indexes(self, document_id, index_types, created_by, session) - def delete_indexes(self, document_id, index_types, session) -``` - -#### 调谐器接口 -```python -# aperag/index/reconciler.py -class IndexReconciler: - def reconcile_all(self) # 主调谐循环 - def _get_indexes_needing_reconciliation(self, session) # 获取需要调谐的索引 - def _reconcile_single_document(self, document_id, operations) # 处理单个文档 -``` - -#### 索引器接口 -```python -# aperag/index/base.py -class BaseIndexer(ABC): - def create_index(self, document_id, content, doc_parts, collection, **kwargs) - def update_index(self, document_id, content, doc_parts, collection, **kwargs) - def delete_index(self, document_id, collection, **kwargs) - def is_enabled(self, collection) -``` - -### 数据流设计 - -#### 任务数据结构 -```python -# aperag/tasks/models.py -@dataclass -class ParsedDocumentData: - """文档解析结果,承载所有索引任务所需的数据""" - document_id: str - collection_id: str - content: str # 解析后的文本内容 - doc_parts: List[Any] # 分块后的文档片段 - file_path: str # 本地文件路径 - local_doc_info: LocalDocumentInfo # 临时文件信息 - -@dataclass -class IndexTaskResult: - """单个索引任务的执行结果""" - status: TaskStatus - index_type: str - document_id: str - success: bool - data: Optional[Dict[str, Any]] # 索引元数据(如向量数量等) - error: Optional[str] - -@dataclass -class WorkflowResult: - """整个工作流的执行结果""" - workflow_id: str - document_id: str - operation: str # create/update/delete - status: TaskStatus - successful_indexes: List[str] - failed_indexes: List[str] - index_results: List[IndexTaskResult] -``` - -## 当前实现状态 - -### 简化架构特性 - -1. **移除分布式锁**:当前实现专注于通过版本验证确保正确性,而非分布式锁处理外部资源并发 -2. **单一状态模型**:从双状态(期望/实际)简化为带版本跟踪的单一状态 -3. **明确操作分离**:显式区分创建(v=1)和更新(v>1)操作 -4. **基于版本的验证**:通过在任务执行时检查版本来防止过期操作 - -### 未来考虑 - -1. **并发控制**:虽然为简化而移除了分布式锁,但未来实现可能需要解决外部系统(向量数据库、搜索引擎等)的并发操作问题 -2. **性能优化**:当前架构优先考虑正确性和简洁性而非最大性能 -3. **监控增强**:随着系统扩展,可能会添加额外的监控和告警功能 - -## 总结 - -ApeRAG的索引链路架构通过以下技术设计实现了高效的文档索引处理: - -### 核心优势 - -1. **响应速度快**:前端链路只操作数据库,API响应时间控制在毫秒级 -2. **处理能力强**:后端异步处理支持大批量文档索引,通过并行任务提升吞吐量 -3. **错误恢复好**:多层次的重试机制和基于版本的状态管理,支持部分失败场景的优雅处理 -4. **系统解耦强**:TaskScheduler抽象层使得业务逻辑与具体任务系统解耦 -5. **版本一致性**:版本验证防止过期操作并确保数据一致性 - -### 技术特点 - -1. **状态驱动**:通过检测版本不匹配和状态变化实现最终一致性 -2. **动态编排**:运行时根据文档解析结果动态创建索引任务,避免静态工作流的局限性 -3. **批量优化**:同一文档的多个索引任务共享解析结果,减少重复计算 -4. **分层设计**:任务调度、业务逻辑、索引实现分层解耦,便于测试和维护 -5. **操作区分**:明确分离创建与更新操作,允许优化的处理策略 - -这个架构在保证系统可靠性和可维护性的同时,为高并发的文档索引场景提供了良好的性能和扩展性支持。 - ---- - -## 相关文档 - -- 🚀 [Graph Index 创建流程技术文档](./graph_index_creation_zh.md) - 深入了解图索引的详细构建流程 -- 📋 [Indexing Architecture Design](./indexing_architecture.md) - English Version \ No newline at end of file diff --git a/docs/zh-CN/design/lightrag_entity_extraction_and_merging.md b/docs/zh-CN/design/lightrag_entity_extraction_and_merging.md deleted file mode 100644 index fc12272e0..000000000 --- a/docs/zh-CN/design/lightrag_entity_extraction_and_merging.md +++ /dev/null @@ -1,696 +0,0 @@ -# LightRAG 实体提取与合并机制详解 - -> 📖 **补充阅读**:本文档是 [索引链路架构设计](./indexing_architecture_zh.md) 的技术细节补充,专注于 Graph Index 创建过程中的实体提取与合并机制。 - -## 概述 - -本文档详细介绍 LightRAG 系统中两个核心函数的工作原理: -- `extract_entities`: 从文本块中提取实体和关系 -- `merge_nodes_and_edges`: 合并提取结果并更新知识图谱 - -这两个函数构成了 [Graph Index 创建流程](./graph_index_creation_zh.md) 的核心环节,负责将非结构化文本转换为结构化的知识图谱。 - -## 实体提取机制 (extract_entities) - -### 核心工作流程 - -#### 1. 并发处理策略 -```python -# 使用信号量控制并发数,避免LLM服务过载 -semaphore = asyncio.Semaphore(llm_model_max_async) - -# 为每个文本块创建异步任务 -tasks = [ - asyncio.create_task(_process_single_content(chunk, context)) - for chunk in ordered_chunks -] - -# 等待所有任务完成,支持异常处理 -done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) -``` - -**并发任务输入格式**: -```python -# 单个处理任务的输入 -chunk_input = { - "content": "要处理的文本内容", - "chunk_key": "chunk_unique_identifier", - "file_path": "source_file_path", - "context": { - "entity_extraction_prompt": "实体提取提示词", - "continue_extraction_prompt": "继续提取提示词", - "extraction_config": {...} - } -} -``` - -**单任务输出格式**: -```python -# _process_single_content 函数的返回值 -task_result = ( - maybe_nodes, # Dict[str, List[Dict]] - 候选实体 - maybe_edges # Dict[Tuple[str, str], List[Dict]] - 候选关系 -) - -# 示例输出结构 -maybe_nodes = { - "张三": [{ - "entity_name": "张三", - "entity_type": "人物", - "description": "公司技术总监", - "source_id": "chunk_001", - "file_path": "/docs/company.txt" - }], - "ABC公司": [{ - "entity_name": "ABC公司", - "entity_type": "组织", - "description": "科技公司", - "source_id": "chunk_001", - "file_path": "/docs/company.txt" - }] -} - -maybe_edges = { - ("张三", "ABC公司"): [{ - "src_id": "张三", - "tgt_id": "ABC公司", - "weight": 1.0, - "description": "张三是ABC公司的技术总监", - "keywords": "工作, 职位, 领导", - "source_id": "chunk_001", - "file_path": "/docs/company.txt" - }] -} -``` - -#### 2. 多轮提取机制 (Gleaning) -LightRAG 采用多轮提取策略提高实体识别的完整性: - -1. **初始提取**:使用实体提取提示词进行首次提取 -2. **补充提取**:通过"继续提取"提示词发现遗漏的实体 -3. **停止判断**:LLM 自主判断是否需要继续提取 - -```python -for glean_index in range(entity_extract_max_gleaning): - # 补充提取:只接受新的实体名称 - glean_result = await use_llm_func(continue_prompt, history_messages=history) - - # 合并结果(去重) - for entity_name, entities in glean_nodes.items(): - if entity_name not in maybe_nodes: # 只接受新实体 - maybe_nodes[entity_name].extend(entities) - - # 判断是否继续 - if_continue = await use_llm_func(if_loop_prompt, history_messages=history) - if if_continue.strip().lower() != "yes": - break -``` - -**初始提取阶段产物**: -```python -# 第一轮提取的原始结果 -initial_extraction = { - "entities": [ - { - "entity_name": "张三", - "entity_type": "人物", - "description": "技术总监" - }, - { - "entity_name": "ABC公司", - "entity_type": "组织", - "description": "科技公司" - } - ], - "relationships": [ - { - "src_id": "张三", - "tgt_id": "ABC公司", - "description": "工作关系", - "keywords": "员工, 公司" - } - ] -} -``` - -**补充提取阶段产物**: -```python -# 每轮补充提取的增量结果 -glean_extraction = { - "round": 2, # 提取轮次 - "new_entities": [ - { - "entity_name": "产品部", # 必须是全新的实体名称 - "entity_type": "部门", - "description": "ABC公司的产品开发部门" - } - ], - "new_relationships": [ - { - "src_id": "张三", - "tgt_id": "产品部", # 必须是全新的关系对 - "description": "管理关系", - "keywords": "负责, 管理" - } - ], - "continue_extraction": "no" # LLM判断是否继续 -} - -# 关键限制:gleaning阶段只接受新发现的实体和关系 -# - 已存在的实体名称会被忽略:if entity_name not in maybe_nodes -# - 已存在的关系对会被忽略:if edge_key not in maybe_edges -# - 不会对已有实体进行描述补充或合并 -``` - -**多轮提取后的Chunk结果**: -```python -# 单个chunk经过多轮提取后的完整结果(gleaning只添加新实体,不合并) -final_chunk_result = { - "chunk_id": "chunk_001", - "total_rounds": 2, - "maybe_nodes": { - "张三": [{ # 初始提取的实体 - "entity_name": "张三", - "entity_type": "人物", - "description": "技术总监", - "source_id": "chunk_001", - "file_path": "/docs/company.txt" - }], - "产品部": [{ # gleaning阶段新发现的实体 - "entity_name": "产品部", - "entity_type": "部门", - "description": "ABC公司的产品开发部门", - "source_id": "chunk_001", - "file_path": "/docs/company.txt" - }] - # 注意:gleaning不会合并张三的描述,只添加新实体 - }, - "maybe_edges": { - ("张三", "ABC公司"): [{ # 初始提取的关系 - "src_id": "张三", - "tgt_id": "ABC公司", - "weight": 1.0, - "description": "张三是ABC公司的技术总监", - "source_id": "chunk_001" - }], - ("张三", "产品部"): [{ # gleaning阶段新发现的关系 - "src_id": "张三", - "tgt_id": "产品部", - "weight": 1.0, - "description": "张三负责管理产品部", - "source_id": "chunk_001" - }] - } -} - -# 重要说明:gleaning阶段的合并规则 -# - 只接受新的实体名称:if entity_name not in maybe_nodes -# - 不会合并已有实体的多个描述片段 -# - 真正的描述合并和权重累加发生在merge阶段 -``` - -#### 3. 提取结果格式 - -**实体格式**: -```python -{ - "entity_name": "标准化实体名称", - "entity_type": "实体类型", - "description": "实体描述", - "source_id": "chunk_key", - "file_path": "文件路径" -} -``` - -**关系格式**: -```python -{ - "src_id": "源实体", - "tgt_id": "目标实体", - "weight": 1.0, # 关系权重,详见下方说明 - "description": "关系描述", - "keywords": "关键词", - "source_id": "chunk_key", - "file_path": "文件路径" -} -``` - -#### 关系权重 (weight) 机制详解 - -**权重的作用**: -- 🎯 **关系强度指标**:数值越大表示两实体间关系越重要或越频繁 -- 📊 **图查询优化**:检索时优先返回高权重关系,提升结果质量 -- 🔍 **路径计算**:图遍历算法中用作边的重要性权重 -- 📈 **知识演化**:追踪关系在不同文档中的重复出现程度 - -**初始权重计算**: -```python -# 每个新提取的关系默认权重为 1.0 -initial_weight = 1.0 - -# 特殊情况:LLM 可能输出带权重的关系 -if "weight" in extracted_relation: - initial_weight = float(extracted_relation["weight"]) -else: - initial_weight = 1.0 # 默认基础权重 -``` - -**权重累积规则**: -- ✅ **同一文档内重复**:相同关系在同一文档的不同chunk中出现,权重累加 -- 🔄 **跨文档强化**:相同关系在不同文档中出现,权重持续累积 -- 📊 **频次反映**:最终权重 = 该关系在所有文档中的总出现次数 - -**权重计算示例**: -```python -# 假设关系 "张三" -> "工作于" -> "ABC公司" 在以下情况出现: -# 文档1, chunk1: weight = 1.0 -# 文档1, chunk3: weight = 1.0 -# 文档2, chunk1: weight = 1.0 -# 最终权重: 1.0 + 1.0 + 1.0 = 3.0 - -final_weight = sum([edge["weight"] for edge in same_relation_edges]) -``` - -### 关键设计特点 - -#### 分块独立处理 -每个文本块独立提取,返回结果为: -```python -chunk_results = [ - (chunk1_nodes, chunk1_edges), # 第一个chunk的提取结果 - (chunk2_nodes, chunk2_edges), # 第二个chunk的提取结果 - # ... 更多chunk结果 -] -``` - -**设计优势**: -- 🚀 **并发效率**:文本块可以完全并行处理 -- 💾 **内存友好**:避免构建巨大的中间合并结果 -- 🛡️ **错误隔离**:单个块失败不影响其他块 -- 🔧 **处理灵活**:可对不同块应用不同策略 - -**数据特点**: -- ⚠️ **存在重复**:同一实体可能在多个chunk中重复提取 -- 📊 **分散数据**:完整的实体信息分散在不同chunk中 - -#### Gleaning vs Merge 阶段的区别 - -**Gleaning 阶段(chunk 内部)**: -- 🎯 **目标**:在单个chunk内发现更多实体和关系 -- 🔍 **策略**:只添加新发现的实体名称和关系对 -- ❌ **不进行合并**:不会合并已有实体的描述或累加关系权重 -- 📝 **代码逻辑**:`if entity_name not in maybe_nodes` - -**Merge 阶段(跨chunk)**: -- 🎯 **目标**:将所有chunk的结果合并成最终知识图谱 -- 🔍 **策略**:合并同名实体的所有描述片段,累加关系权重 -- ✅ **完整合并**:描述连接、权重累加、智能摘要 -- 📝 **代码逻辑**:`all_nodes[entity_name].extend(entities)` - -## 实体合并机制 (merge_nodes_and_edges) - -### 核心合并策略 - -#### 1. 跨Chunk数据收集 -```python -# 收集所有同名实体和关系 -all_nodes = defaultdict(list) # {entity_name: [entity1, entity2, ...]} -all_edges = defaultdict(list) # {(src, tgt): [edge1, edge2, ...]} - -for maybe_nodes, maybe_edges in chunk_results: - # 合并同名实体 - for entity_name, entities in maybe_nodes.items(): - all_nodes[entity_name].extend(entities) - - # 合并同向关系 - for edge_key, edges in maybe_edges.items(): - sorted_key = tuple(sorted(edge_key)) # 统一方向 - all_edges[sorted_key].extend(edges) -``` - -**数据收集阶段输入格式**: -```python -# 来自多个chunk的提取结果集合 -chunk_results = [ - # Chunk 1 的结果 - (chunk1_maybe_nodes, chunk1_maybe_edges), - # Chunk 2 的结果 - (chunk2_maybe_nodes, chunk2_maybe_edges), - # ... 更多chunk结果 -] - -# 单个chunk结果示例 -chunk1_maybe_nodes = { - "张三": [{ - "entity_name": "张三", - "entity_type": "人物", - "description": "技术总监", - "source_id": "chunk_001" - }] -} - -chunk2_maybe_nodes = { - "张三": [{ # 同一实体在不同chunk中重复出现 - "entity_name": "张三", - "entity_type": "人物", - "description": "产品负责人", - "source_id": "chunk_002" - }] -} -``` - -**数据收集阶段产物格式**: -```python -# 跨chunk收集后的聚合数据 -all_nodes = { - "张三": [ - { - "entity_name": "张三", - "entity_type": "人物", - "description": "技术总监", - "source_id": "chunk_001", - "file_path": "/docs/company.txt" - }, - { - "entity_name": "张三", - "entity_type": "人物", - "description": "产品负责人", - "source_id": "chunk_002", - "file_path": "/docs/company.txt" - } - # 同一实体的多个描述片段等待合并 - ], - "ABC公司": [ - { - "entity_name": "ABC公司", - "entity_type": "组织", - "description": "科技公司", - "source_id": "chunk_001" - } - ] -} - -all_edges = { - ("ABC公司", "张三"): [ # key已排序统一方向 - { - "src_id": "张三", - "tgt_id": "ABC公司", - "weight": 1.0, - "description": "工作关系", - "source_id": "chunk_001" - }, - { - "src_id": "张三", - "tgt_id": "ABC公司", - "weight": 1.0, - "description": "管理关系", - "source_id": "chunk_002" - } - # 同一关系的多次出现等待权重累加 - ] -} -``` - -#### 2. 实体合并规则 - -**类型选择**:选择最频繁出现的实体类型 -```python -entity_type = Counter([ - entity["entity_type"] for entity in entities -]).most_common(1)[0][0] -``` - -**描述合并**:使用分隔符连接,去重排序 -```python -descriptions = [entity["description"] for entity in entities] -if existing_entity: - descriptions.extend(existing_entity["description"].split(GRAPH_FIELD_SEP)) - -merged_description = GRAPH_FIELD_SEP.join(sorted(set(descriptions))) -``` - -**智能摘要**:当描述片段过多时自动生成摘要 -```python -fragment_count = merged_description.count(GRAPH_FIELD_SEP) + 1 - -if fragment_count >= force_llm_summary_threshold: - # 使用LLM生成摘要,压缩长描述 - merged_description = await llm_summarize( - entity_name, merged_description, max_tokens - ) -``` - -#### 3. 关系合并规则 - -**权重累加**:反映关系强度的增强 -```python -total_weight = sum([edge["weight"] for edge in edges]) -if existing_edge: - total_weight += existing_edge["weight"] -``` - -**描述聚合**:类似实体描述的合并策略 -```python -# 关系描述合并示例 -edge_descriptions = [edge["description"] for edge in edges] -if existing_edge: - edge_descriptions.extend(existing_edge["description"].split(GRAPH_FIELD_SEP)) - -merged_description = GRAPH_FIELD_SEP.join(sorted(set(edge_descriptions))) -``` - -**关键词去重**:提取并合并所有关键词 -```python -# 关键词合并示例 -all_keywords = [] -for edge in edges: - if edge.get("keywords"): - all_keywords.extend(edge["keywords"].split(", ")) - -merged_keywords = ", ".join(sorted(set(all_keywords))) -``` - -**合并规则最终产物格式**: - -**实体合并产物**: -```python -# 经过合并规则处理后的最终实体格式 -merged_entity = { - "entity_name": "张三", - "entity_type": "人物", # 基于出现频次选择的类型 - "description": "技术总监§产品负责人§项目经理", # 使用§分隔符连接的描述 - "source_chunks": ["chunk_001", "chunk_002", "chunk_003"], # 源chunk列表 - "file_paths": ["/docs/company.txt", "/docs/team.txt"], # 源文件列表 - "mention_count": 3, # 在多少个chunk中被提及 - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T12:00:00Z" -} -``` - -**关系合并产物**: -```python -# 经过合并规则处理后的最终关系格式 -merged_relationship = { - "src_id": "张三", - "tgt_id": "ABC公司", - "weight": 3.0, # 累加后的权重 (1.0 + 1.0 + 1.0) - "description": "工作关系§管理关系§领导关系", # 使用§分隔符连接 - "keywords": "员工, 公司, 管理, 负责, 领导", # 去重合并的关键词 - "source_chunks": ["chunk_001", "chunk_002"], # 关系出现的chunk - "file_paths": ["/docs/company.txt"], # 关系出现的文件 - "mention_count": 2, # 关系被提及的次数 - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T12:00:00Z" -} -``` - -#### 4. 数据库更新流程 - -```mermaid -graph LR - A[合并实体数据] --> B[更新图数据库] - B --> C[生成向量表示] - C --> D[更新向量数据库] - - E[合并关系数据] --> F[确保端点存在] - F --> G[更新图数据库] - G --> H[生成向量表示] - H --> I[更新向量数据库] - - style B fill:#e8f5e8 - style D fill:#e1f5fe - style G fill:#e8f5e8 - style I fill:#e1f5fe -``` - -**数据库存储最终格式**: - -**图数据库实体存储格式**: -```python -# 存储在图数据库中的实体节点 -graph_entity_node = { - "id": "张三", # 实体名称作为节点ID - "entity_type": "人物", - "description": "技术总监§产品负责人§项目经理", - "source_chunks": ["chunk_001", "chunk_002", "chunk_003"], - "file_paths": ["/docs/company.txt", "/docs/team.txt"], - "mention_count": 3, - "workspace": "collection_12345", # 工作空间隔离 - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T12:00:00Z" -} -``` - -**图数据库关系存储格式**: -```python -# 存储在图数据库中的关系边 -graph_relationship_edge = { - "source": "张三", # 源节点ID - "target": "ABC公司", # 目标节点ID - "weight": 3.0, - "description": "工作关系§管理关系§领导关系", - "keywords": "员工, 公司, 管理, 负责, 领导", - "source_chunks": ["chunk_001", "chunk_002"], - "file_paths": ["/docs/company.txt"], - "mention_count": 2, - "workspace": "collection_12345", - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T12:00:00Z" -} -``` - -**向量数据库存储格式**: -```python -# 存储在向量数据库中的实体向量 -vector_entity_record = { - "id": "entity_张三_collection_12345", # 向量记录唯一ID - "entity_name": "张三", - "content": "张三是一位人物,担任技术总监、产品负责人和项目经理的职务", # 用于向量化的文本 - "content_vector": [0.1, 0.2, ..., 0.9], # 1024维向量表示 - "workspace": "collection_12345", - "storage_type": "entity", # 区分实体/关系向量 - "metadata": { - "entity_type": "人物", - "mention_count": 3, - "file_paths": ["/docs/company.txt", "/docs/team.txt"] - } -} - -# 存储在向量数据库中的关系向量 -vector_relationship_record = { - "id": "relation_张三_ABC公司_collection_12345", - "relationship": "张三 -> ABC公司", - "content": "张三与ABC公司之间存在工作关系、管理关系、领导关系", - "content_vector": [0.3, 0.4, ..., 0.8], - "workspace": "collection_12345", - "storage_type": "relationship", - "metadata": { - "weight": 3.0, - "keywords": "员工, 公司, 管理, 负责, 领导", - "mention_count": 2 - } -} -``` - -### 并发控制与一致性 - -#### 工作空间隔离 -```python -# 使用工作空间实现多租户隔离 -lock_manager = get_lock_manager() -entity_lock = f"entity:{entity_name}:{workspace}" -relation_lock = f"relation:{src_id}:{tgt_id}:{workspace}" - -async with lock_manager.lock(entity_lock): - # 原子性的读取-合并-写入操作 - existing = await graph_db.get_node(entity_name) - merged_entity = merge_entity_data(existing, new_entities) - await graph_db.upsert_node(entity_name, merged_entity) -``` - -#### 锁粒度优化 -- **实体级锁定**:每个实体独立加锁,避免全局竞争 -- **关系级锁定**:每个关系对独立处理 -- **排序锁获取**:防止死锁,确保一致的锁获取顺序 - -## 性能优化特性 - -### 1. 连通分量并发 -基于图拓扑分析的智能分组: -- 🧠 **拓扑分析**:使用BFS算法发现独立的实体群组 -- ⚡ **并行处理**:不同连通分量完全并行合并 -- 🔒 **零锁竞争**:组件间无共享实体,避免锁冲突 - -### 2. 内存与I/O优化 -- 📦 **分批处理**:按连通分量分批,控制内存峰值 -- 🔄 **连接复用**:数据库连接池减少建连开销 -- 📊 **批量操作**:尽可能使用批量数据库操作 - -### 3. 智能摘要策略 -- 🎯 **阈值控制**:只在必要时调用LLM生成摘要 -- ⚖️ **性能平衡**:避免频繁LLM调用影响性能 -- 💡 **信息保全**:摘要过程中保留关键信息 - -## 数据流总览 - -```mermaid -graph TD - A[文档分块] --> B[并发提取] - B --> C[实体/关系识别] - C --> D[多轮补充提取] - D --> E[分块提取结果] - - E --> F[跨块数据收集] - F --> G[同名实体合并] - G --> H{描述长度检查} - H -->|超过阈值| I[LLM智能摘要] - H -->|正常长度| J[直接合并] - - I --> K[更新图数据库] - J --> K - K --> L[生成向量] - L --> M[更新向量库] - - style B fill:#e3f2fd - style G fill:#fff3e0 - style I fill:#f3e5f5 - style K fill:#e8f5e8 - style M fill:#e1f5fe -``` - -## 关键技术特点 - -### 1. 增量更新设计 -- ✅ **非破坏性合并**:新信息增强而非替换现有数据 -- 📈 **权重累积**:关系强度随重复出现而增强 -- 🔍 **信息聚合**:多源描述提供更全面的实体画像 - -### 2. 容错与恢复 -- 🛡️ **异常隔离**:单个任务失败不影响整体流程 -- 🔄 **自动补全**:自动创建缺失的关系端点实体 -- ✔️ **数据验证**:严格的格式和内容验证机制 - -### 3. 扩展性支持 -- 🏗️ **模块化设计**:提取和合并逻辑完全解耦 -- 🔌 **接口标准**:支持不同的图数据库和向量存储 -- 📊 **监控友好**:完整的日志记录和性能指标 - -## 总结 - -LightRAG 的实体提取与合并机制通过以下创新实现了高效的知识图谱构建: - -1. **🚀 高并发提取**:分块并行处理 + 多轮补充提取,确保准确性和效率 -2. **🧠 智能合并**:基于连通分量的并发优化,最大化并行处理能力 -3. **📊 增量更新**:非破坏性数据合并,支持知识图谱的持续演化 -4. **🔒 并发安全**:细粒度锁机制 + 工作空间隔离,确保多租户数据安全 -5. **⚡ 性能优化**:智能摘要 + 批量操作,平衡准确性和处理速度 - -这些技术特性使得 LightRAG 能够在保证数据质量的同时,实现大规模文档的高效知识图谱构建。 - ---- - -## 相关文档 - -- 📋 [索引链路架构设计](./indexing_architecture_zh.md) - 整体架构设计 -- 🏗️ [Graph Index 创建流程](./graph_index_creation_zh.md) - 详细的图索引构建流程 -- 📖 [Entity Extraction and Merging Mechanism](./lightrag_entity_extraction_and_merging.md) - English Version \ No newline at end of file diff --git a/docs/zh-CN/design/quota-system-design.md b/docs/zh-CN/design/quota-system-design.md deleted file mode 100644 index f18a819c1..000000000 --- a/docs/zh-CN/design/quota-system-design.md +++ /dev/null @@ -1,482 +0,0 @@ -# ApeRAG 配额系统设计文档 - -## 目录 -- [概述](#概述) -- [架构设计](#架构设计) -- [数据模型](#数据模型) -- [API 设计](#api-设计) -- [服务层](#服务层) -- [前端实现](#前端实现) -- [安全与授权](#安全与授权) -- [错误处理](#错误处理) -- [使用模式](#使用模式) -- [未来增强](#未来增强) - -## 概述 - -ApeRAG 配额系统是一个全面的资源管理解决方案,旨在控制和监控平台上的用户资源消耗。它提供对各种资源类型(包括文档集合、文档和机器人)的细粒度控制,确保公平使用并防止系统滥用。 - -### 核心特性 - -- **多资源配额管理**:支持不同配额类型(文档集合、文档、机器人) -- **实时使用量跟踪**:自动跟踪当前资源使用情况 -- **系统默认配置**:为新用户提供集中式默认配额设置 -- **管理控制**:仅限管理员的配额管理和用户搜索功能 -- **原子操作**:线程安全的配额消费和释放操作 -- **灵活的 API 设计**:支持单个和批量操作的 RESTful API - -### 支持的配额类型 - -1. **max_collection_count**:用户可以创建的最大文档集合数量 -2. **max_document_count**:所有文档集合中文档的总最大数量 -3. **max_document_count_per_collection**:单个文档集合中的最大文档数量 -4. **max_bot_count**:用户可以创建的最大机器人数量(不包括系统默认机器人) - -## 架构设计 - -配额系统采用分层架构模式: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 前端层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │ -│ │ 用户配额 │ │ 管理面板 │ │ 系统配置 │ │ -│ │ 视图 │ │ 视图 │ │ 视图 │ │ -│ └─────────────────┘ └─────────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ API 层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │ -│ │ 配额 API │ │ 管理 API │ │ 系统 API │ │ -│ │ (quotas.yaml) │ │ │ │ │ │ -│ └─────────────────┘ └─────────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ 服务层 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ QuotaService │ │ -│ │ • get_user_quotas() │ │ -│ │ • check_and_consume_quota() │ │ -│ │ • release_quota() │ │ -│ │ • recalculate_user_usage() │ │ -│ │ • update_user_quota() │ │ -│ │ • get/update_system_default_quotas() │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ 数据层 │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │ -│ │ UserQuota │ │ ConfigModel │ │ 相关表 │ │ -│ │ 表 │ │ 表 │ │ │ │ -│ └─────────────────┘ └─────────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 数据模型 - -### UserQuota 表 - -存储用户配额信息的核心表: - -```sql -CREATE TABLE user_quota ( - user VARCHAR(256) NOT NULL, -- 用户标识符 - key VARCHAR(256) NOT NULL, -- 配额类型键 - quota_limit INTEGER NOT NULL DEFAULT 0, -- 最大允许使用量 - current_usage INTEGER NOT NULL DEFAULT 0, -- 当前使用量 - gmt_created TIMESTAMP WITH TIME ZONE NOT NULL, - gmt_updated TIMESTAMP WITH TIME ZONE NOT NULL, - gmt_deleted TIMESTAMP WITH TIME ZONE, - PRIMARY KEY (user, key) -); -``` - -**关键字段:** -- `user`:用户标识符(用户表的外键) -- `key`:配额类型(如 'max_collection_count'、'max_document_count') -- `quota_limit`:此配额类型的最大允许使用量 -- `current_usage`:当前使用量的实时跟踪 -- 复合主键确保每个用户每种配额类型只有一条记录 - -### ConfigModel 表 - -存储系统级配置,包括默认配额: - -```sql -CREATE TABLE config ( - key VARCHAR(256) PRIMARY KEY, -- 配置键 - value TEXT NOT NULL, -- JSON 配置值 - gmt_created TIMESTAMP WITH TIME ZONE NOT NULL, - gmt_updated TIMESTAMP WITH TIME ZONE NOT NULL, - gmt_deleted TIMESTAMP WITH TIME ZONE -); -``` - -**系统默认配额配置:** -```json -{ - "max_collection_count": 10, - "max_document_count": 1000, - "max_document_count_per_collection": 100, - "max_bot_count": 5 -} -``` - -### 相关表 - -配额系统与多个其他表集成以进行使用量计算: - -- **Collection**:用于计算用户文档集合数量(status != 'DELETED') -- **Document**:用于计算跨文档集合的文档数量 -- **Bot**:用于计算用户机器人数量(不包括系统默认机器人) - -## API 设计 - -### RESTful 端点 - -#### 1. 获取用户配额 -```http -GET /quotas?user_id={user_id}&search={search_term} -``` - -**功能:** -- 当前用户配额(无参数) -- 仅限管理员按 ID、用户名或邮箱搜索用户 -- 支持多个搜索结果 - -**响应类型:** -- `UserQuotaInfo`:单个用户配额信息 -- `UserQuotaList`:多个用户(搜索结果) - -#### 2. 更新用户配额 -```http -PUT /quotas/{user_id} -``` - -**功能:** -- 仅限管理员操作 -- 支持单个和批量配额更新 -- 原子事务处理 - -**请求体:** -```json -{ - "max_collection_count": 20, - "max_document_count": 2000, - "max_bot_count": 10 -} -``` - -#### 3. 重新计算使用量 -```http -POST /quotas/{user_id}/recalculate -``` - -**功能:** -- 仅限管理员操作 -- 从数据库重新计算实际使用量 -- 原子更新 current_usage 字段 - -#### 4. 系统默认配额 -```http -GET /system/default-quotas -PUT /system/default-quotas -``` - -**功能:** -- 仅限管理员操作 -- 集中式默认配额管理 -- 应用于新用户初始化 - -### OpenAPI 规范 - -API 遵循 OpenAPI 3.0 规范,包含: -- 全面的模式定义 -- 详细的错误响应映射 -- 参数验证规则 -- 安全要求(仅限管理员操作) - -## 服务层 - -### QuotaService 类 - -`QuotaService` 类提供配额管理的核心业务逻辑: - -#### 关键方法 - -**1. 配额消费(线程安全)** -```python -async def check_and_consume_quota( - self, - user_id: str, - quota_type: str, - amount: int = 1, - session=None -) -> None: - """ - 使用 SELECT FOR UPDATE 原子地检查和消费配额 - 如果配额将被超出则抛出 QuotaExceededException - """ -``` - -**2. 配额释放** -```python -async def release_quota( - self, - user_id: str, - quota_type: str, - amount: int = 1, - session=None -) -> None: - """ - 释放配额(减少使用量)并保证事务安全 - """ -``` - -**3. 使用量重新计算** -```python -async def recalculate_user_usage(self, user_id: str) -> Dict[str, int]: - """ - 从相关表重新计算实际使用量: - - 文档集合:COUNT(*) WHERE user=? AND status!='DELETED' - - 文档:通过与文档集合的 JOIN 进行 COUNT(*) - - 机器人:COUNT(*) WHERE user=? AND title!='Default Agent Bot' - """ -``` - -**4. 用户初始化** -```python -async def initialize_user_quotas(self, user_id: str) -> None: - """ - 从系统配置为新用户初始化默认配额 - """ -``` - -#### 事务管理 - -- **数据库操作**:所有配额操作都使用异步数据库事务 -- **原子更新**:SELECT FOR UPDATE 防止竞态条件 -- **会话处理**:支持独立和嵌套事务 - -## 前端实现 - -### React 组件架构 - -前端配额管理实现为一个全面的 React 组件,具有: - -#### 关键功能 - -**1. 多标签界面(仅限管理员)** -- 用户配额管理 -- 系统默认配置 - -**2. 用户搜索与选择** -- 按用户名、邮箱或用户 ID 实时搜索 -- 多结果处理与选择界面 -- 清除搜索功能 - -**3. 内联表格编辑** -- 配额限制的编辑模式切换 -- 实时验证 -- 批量保存操作 -- 取消/恢复功能 - -**4. 进度可视化** -- 使用率进度条 -- 颜色编码状态(正常/警告/危险) -- 百分比计算 - -**5. 管理操作** -- 配额重新计算 -- 系统默认配额管理 -- 用户特定配额更新 - -#### 组件结构 - -```typescript -interface QuotaInfo { - quota_type: string; - quota_limit: number; - current_usage: number; - remaining: number; -} - -interface UserQuotaInfo { - user_id: string; - username: string; - email?: string; - role: string; - quotas: QuotaInfo[]; -} -``` - -#### 状态管理 - -- **本地状态**:UI 交互的组件级状态 -- **API 集成**:带有加载状态的直接 API 调用 -- **错误处理**:带有国际化的用户友好错误消息 - -## 安全与授权 - -### 基于角色的访问控制 - -**普通用户:** -- 仅查看自己的配额 -- 只读访问 -- 无管理功能 - -**管理员用户:** -- 完整的配额管理功能 -- 用户搜索和选择 -- 系统配置访问权限 -- 配额重新计算权限 - -### API 安全 - -- **身份验证**:所有配额端点都需要身份验证 -- **授权**:管理操作的管理员角色验证 -- **输入验证**:全面的参数验证 -- **速率限制**:通过配额系统本身隐式实现 - -## 错误处理 - -### 异常层次结构 - -```python -class QuotaExceededException(BusinessException): - """ - 当配额消费将超出限制时抛出 - 映射到适当的 HTTP 状态码: - - 文档集合配额:403 Forbidden - - 文档配额:403 Forbidden - - 机器人配额:403 Forbidden - """ -``` - -### 错误响应格式 - -```json -{ - "error_code": "COLLECTION_QUOTA_EXCEEDED", - "code": 1103, - "message": "已达到max_collection_count的配额限制。当前使用量: 10/10", - "details": { - "quota_type": "max_collection_count", - "quota_limit": 10, - "current_usage": 10, - "quota_exceeded": true - } -} -``` - -### 前端错误处理 - -- **国际化消息**:多语言错误显示 -- **用户友好反馈**:清晰的操作指导 -- **优雅降级**:回退 UI 状态 - -## 使用模式 - -### 资源创建流程 - -```python -# 示例:创建新的文档集合 -async def create_collection(user_id: str, collection_data: dict): - async with database_transaction() as session: - # 1. 原子地检查和消费配额 - await quota_service.check_and_consume_quota( - user_id, - 'max_collection_count', - session=session - ) - - # 2. 创建资源 - collection = Collection(**collection_data) - session.add(collection) - - # 3. 提交事务(配额和资源一起) - await session.commit() -``` - -### 资源删除流程 - -```python -# 示例:删除文档集合 -async def delete_collection(user_id: str, collection_id: str): - async with database_transaction() as session: - # 1. 标记资源为已删除 - collection.status = 'DELETED' - collection.gmt_deleted = utc_now() - - # 2. 释放配额 - await quota_service.release_quota( - user_id, - 'max_collection_count', - session=session - ) - - # 3. 提交事务 - await session.commit() -``` - -### 管理操作 - -```python -# 示例:批量配额更新 -await quota_service.update_user_quota( - user_id="user123", - quota_updates={ - "max_collection_count": 20, - "max_document_count": 2000, - "max_bot_count": 10 - } -) -``` - -## 未来增强 - -### 计划功能 - -1. **配额历史跟踪** - - 历史配额变更 - - 使用分析 - - 趋势分析 - -2. **动态配额调整** - - 基于使用量的自动调整 - - 临时配额增加 - - 基于时间的配额 - -3. **高级监控** - - 实时配额警报 - - 使用预测 - - 容量规划 - -4. **集成增强** - - 外部配额提供商 - - 多租户支持 - - API 速率限制集成 - -### 技术改进 - -1. **性能优化** - - 配额缓存策略 - - 批量操作 - - 数据库索引改进 - -2. **可扩展性** - - 分布式配额管理 - - 微服务架构 - - 事件驱动更新 - -3. **可观测性** - - 详细指标收集 - - 配额操作跟踪 - - 性能监控 - ---- - -*本文档提供了 ApeRAG 配额系统设计和实现的全面概述。有关具体实现细节,请参考相应模块中的源代码。* diff --git a/docs/zh-CN/design/search_flow_design.md b/docs/zh-CN/design/search_flow_design.md deleted file mode 100644 index 29c399b67..000000000 --- a/docs/zh-CN/design/search_flow_design.md +++ /dev/null @@ -1,654 +0,0 @@ ---- -title: 检索流程设计 -description: ApeRAG 检索流程的完整设计文档,涵盖 MCP 入口、Flow 执行引擎与各检索类型的核心实现 -keywords: 检索, Flow Engine, DAG, 向量检索, 全文检索, 图检索, MCP -position: 3 ---- - -# ApeRAG 检索流程设计 - -## 概述 - -ApeRAG 的检索流程采用 **Flow 执行引擎**驱动的多路并行检索架构。用户(或 AI Agent)通过 MCP 工具发起检索请求,请求到达服务端后被转化为一个有向无环图(DAG)描述的检索 Flow,由 Flow 引擎按拓扑顺序并行执行各检索节点,最终将多路结果合并重排后返回。 - -```mermaid -graph LR - A[AI Agent / 用户] -->|MCP 工具调用| B[MCP Server] - B -->|"POST /api/v1/collections//searches"| C[FastAPI 路由] - C --> D[CollectionService.create_search] - D --> E[execute_search_flow\n动态构建 DAG] - E --> F[FlowEngine.execute_flow\nDAG 拓扑排序 + 并行执行] - - F --> G1[vector_search\n向量检索] - F --> G2[fulltext_search\n全文检索] - F --> G3[graph_search\n图谱检索] - F --> G4[summary_search\n摘要检索] - F --> G5[vision_search\n视觉检索] - - G1 --> H[merge\n多路结果合并] - G2 --> H - G3 --> H - G4 --> H - G5 --> H - - H --> I[rerank\n结果重排] - I --> J[SearchResult 返回给调用方] -``` - -图中 REST 路径里的 `` 表示路径参数,与 OpenAPI 写法 `/collections/{collection_id}/searches` 含义相同(Mermaid 中花括号 `{}` 为语法保留字符,故图中用尖括号表示占位符)。 - ---- - -## 第一层:MCP 入口 - -[MCP(Model Context Protocol)](https://modelcontextprotocol.io/) 是 ApeRAG 面向 AI Agent 暴露能力的标准接口。Agent 无需直接调用 REST API,只需调用 MCP 工具即可完成检索。 - -### MCP 挂载位置 - -MCP Server 以 Stateless HTTP 模式挂载在 FastAPI 应用的 `/mcp` 路径下: - -```python -# aperag/app.py -mcp_app = mcp_server.http_app(path="/", stateless_http=True) -app.mount("/mcp", mcp_app) -``` - -### `search_collection` 工具 - -最核心的检索工具是 `search_collection`,它封装了对 REST API 的调用: - -```python -# aperag/mcp/server.py -@mcp_server.tool -async def search_collection( - collection_id: str, - query: str, - use_vector_index: bool = True, - use_fulltext_index: bool = True, - use_graph_index: bool = True, - use_summary_index: bool = True, - use_vision_index: bool = True, - rerank: bool = True, - topk: int = 5, - query_keywords: list[str] = None, -) -> Dict[str, Any]: - """Search for knowledge in a persistent collection/knowledge base""" - ... - async with httpx.AsyncClient(timeout=120.0) as client: - response = await client.post( - f"{API_BASE_URL}/api/v1/collections/{collection_id}/searches", - headers={"Authorization": f"Bearer {api_key}"}, - json=search_data, - ) -``` - -工具会将参数(启用哪些检索类型、topk、关键词等)组装成 JSON,以 Bearer Token 的方式调用内部 REST API。 - -> **注意**:`API_BASE_URL` 默认为 `http://localhost:8000`,与 API 进程同机部署时合理。若 MCP 与 API 分离部署,需要通过环境变量或配置覆盖此地址。 - ---- - -## 第二层:REST API 端点 - -MCP 工具调用最终落到以下 FastAPI 路由: - -```python -# aperag/views/collections.py -@router.post("/collections/{collection_id}/searches", tags=["search"]) -@audit(resource_type="search", api_name="CreateSearch") -async def create_search_view( - request: Request, - collection_id: str, - data: view_models.SearchRequest, - user: User = Depends(required_user), -) -> view_models.SearchResult: - return await collection_service.create_search(str(user.id), collection_id, data) -``` - -### SearchRequest 结构 - -请求体 `SearchRequest` 对应 OpenAPI schema,字段如下: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `query` | string | 用户查询语句 | -| `vector_search` | object | 向量检索参数(`topk`、`similarity`) | -| `fulltext_search` | object | 全文检索参数(`topk`、`keywords`) | -| `graph_search` | object | 图谱检索参数(`topk`) | -| `summary_search` | object | 摘要检索参数(`topk`、`similarity`) | -| `vision_search` | object | 视觉检索参数(`topk`、`similarity`) | -| `rerank` | boolean | 是否对结果重排 | -| `save_to_history` | boolean | 是否保存到搜索历史 | - -某个检索类型的字段为 `null`/缺失时,表示本次检索**不启用**该类型。 - ---- - -## 第三层:`execute_search_flow` —— 动态构建检索 DAG - -`create_search` 校验权限后,调用 `execute_search_flow` 动态构建并执行检索 Flow: - -```python -# aperag/service/collection_service.py -async def execute_search_flow( - self, - data: view_models.SearchRequest, - collection_id: str, - search_user_id: str, - chat_id: Optional[str] = None, - flow_name: str = "search", - flow_title: str = "Search", -) -> Tuple[List[SearchResultItem], str]: -``` - -### 构建过程 - -这个方法根据 `SearchRequest` 的内容,**动态**决定要创建哪些节点,再把它们连接成 DAG: - -```python -nodes = {} -edges = [] -merge_node_id = "merge" - -# 按需添加各检索节点 -if data.vector_search: - nodes["vector_search"] = NodeInstance(type="vector_search", ...) - edges.append(Edge(source="vector_search", target="merge")) - -if data.fulltext_search: - nodes["fulltext_search"] = NodeInstance(type="fulltext_search", ...) - edges.append(Edge(source="fulltext_search", target="merge")) - -if data.graph_search: - nodes["graph_search"] = NodeInstance(type="graph_search", ...) - edges.append(Edge(source="graph_search", target="merge")) - -# ... summary_search, vision_search 同理 - -# merge 节点始终存在 -nodes["merge"] = NodeInstance(type="merge", ...) - -# rerank 节点始终在 merge 之后 -nodes["rerank"] = NodeInstance(type="rerank", ...) -edges.append(Edge(source="merge", target="rerank")) -``` - -构建完成后,将 `FlowInstance` 交给 `FlowEngine` 执行: - -```python -flow = FlowInstance(name=flow_name, nodes=nodes, edges=edges) -engine = FlowEngine() -result, _ = await engine.execute_flow(flow, initial_data={"query": query, "user": search_user_id}) -``` - -### 典型 DAG 示意(全部检索类型启用) - -``` -vector_search ──┐ -fulltext_search ─┤ -graph_search ────┼──→ merge ──→ rerank ──→ 返回结果 -summary_search ──┤ -vision_search ───┘ -``` - -检索节点之间**没有依赖关系**,全部指向 `merge`;`merge` 完成后,结果流向 `rerank`。 - ---- - -## 第四层:Flow 执行引擎 - -Flow 引擎(`aperag/flow/engine.py`)是整个检索流程的核心调度组件,负责解析 DAG、拓扑排序、并行执行。 - -### 核心数据模型 - -```python -# aperag/flow/base/models.py - -class FlowInstance(BaseModel): - name: str - title: str - nodes: Dict[str, NodeInstance] # node_id -> NodeInstance - edges: List[Edge] # 有向边列表 - -class NodeInstance(BaseModel): - id: str - type: str # 节点类型,对应已注册的 Runner - input_values: dict # 节点输入参数(支持 Jinja 模板引用其他节点输出) - -class Edge(BaseModel): - source: str # 源节点 id - target: str # 目标节点 id(target 依赖 source) -``` - -### 执行流程 - -`FlowEngine.execute_flow` 的完整执行步骤: - -```mermaid -flowchart TB - A[execute_flow 入口] --> B[写入 initial_data 到 ExecutionContext\nquery / user / chat_id 等] - B --> C[_topological_sort\nKahn 算法拓扑排序] - C --> D{是否有环?} - D -- 有环 --> E[抛出 CycleError] - D -- 无环 --> F[_find_parallel_groups\n按层分组] - F --> G[逐层执行 _execute_node_group] - G --> H{当前层节点数} - H -- 1个 --> I[顺序执行单个节点] - H -- 多个 --> J[asyncio.gather 并行执行] - I --> K[写入节点输出到 context.outputs] - J --> K - K --> L{还有下一层?} - L -- 是 --> G - L -- 否 --> M[返回 context.outputs] -``` - -### 拓扑排序:Kahn 算法 - -```python -def _topological_sort(self, flow: FlowInstance) -> List[str]: - # 统计每个节点的入度(有多少条边指向它) - in_degree = {node_id: 0 for node_id in flow.nodes} - for edge in flow.edges: - in_degree[edge.target] += 1 - - # 从入度为 0 的节点开始(无依赖的节点) - queue = deque([node_id for node_id, degree in in_degree.items() if degree == 0]) - - sorted_nodes = [] - while queue: - node_id = queue.popleft() - sorted_nodes.append(node_id) - # 处理完当前节点后,更新后继节点的入度 - for edge in flow.edges: - if edge.source == node_id: - in_degree[edge.target] -= 1 - if in_degree[edge.target] == 0: - queue.append(edge.target) - - # 若处理节点数不足,说明图中存在环 - if len(sorted_nodes) != len(flow.nodes): - raise CycleError("Flow contains cycles") - - return sorted_nodes -``` - -### 并行分层执行 - -拓扑排序后,引擎进一步将节点分成**可以并行的层(Level)**: - -```python -def _find_parallel_groups(self, flow, sorted_nodes) -> List[Set[str]]: - """将拓扑序的节点按可并行的层分组""" - in_degree = {node_id: 0 for node_id in flow.nodes} - for edge in flow.edges: - in_degree[edge.target] += 1 - - processed = set() - groups = [] - - while len(processed) < len(sorted_nodes): - # 找出当前所有入度为 0 且未处理的节点 → 可以并行 - current_group = { - node_id for node_id in sorted_nodes - if in_degree[node_id] == 0 and node_id not in processed - } - groups.append(current_group) - for node_id in current_group: - processed.add(node_id) - for edge in flow.edges: - if edge.source == node_id: - in_degree[edge.target] -= 1 - - return groups # 每个元素是一个 Set,同组内可并行 -``` - -对于默认检索 Flow,分层结果如下: - -| 层 | 节点(可并行) | -|----|--------------| -| 第 1 层 | `vector_search`、`fulltext_search`、`graph_search`、`summary_search`、`vision_search` | -| 第 2 层 | `merge`(等待第 1 层全部完成) | -| 第 3 层 | `rerank`(等待 merge 完成) | - -同一层内的节点通过 `asyncio.gather` **并发执行**,显著降低多路检索的总延迟。 - -### 节点间数据传递:Jinja 模板 - -节点的 `input_values` 支持 Jinja2 模板语法,用于引用其他节点的输出: - -```python -# merge 节点引用各检索节点的输出 -merge_node_values = { - "vector_search_docs": "{{ nodes.vector_search.output.docs }}", - "fulltext_search_docs": "{{ nodes.fulltext_search.output.docs }}", - "graph_search_docs": "{{ nodes.graph_search.output.docs }}", - ... -} - -# rerank 节点引用 merge 节点的输出 -rerank_input_values = { - "docs": "{{ nodes.merge.output.docs }}", - ... -} -``` - -引擎在执行节点前会先解析这些模板,将前序节点的实际输出填充进来。 - -### NodeRunner 注册机制 - -每种节点类型通过装饰器注册到全局注册表 `NODE_RUNNER_REGISTRY`: - -```python -# aperag/flow/base/models.py -NODE_RUNNER_REGISTRY = {} - -def register_node_runner(node_type, input_model, output_model): - def decorator(cls): - NODE_RUNNER_REGISTRY[node_type] = { - "runner": cls(), - "input_model": input_model, - "output_model": output_model, - } - return cls - return decorator -``` - -节点 Runner 示例: - -```python -# aperag/flow/runners/vector_search.py -@register_node_runner( - "vector_search", - input_model=VectorSearchInput, - output_model=VectorSearchOutput, -) -class VectorSearchNodeRunner(BaseNodeRunner): - async def run(self, ui: VectorSearchInput, si: SystemInput) -> Tuple[VectorSearchOutput, dict]: - ... -``` - -`import aperag.flow.runners` 时,所有 Runner 模块被加载,完成注册(见 `engine.py` 第 23 行的 `import` 语句)。 - ---- - -## 第五层:各检索类型详解 - -### 1. 向量检索(`vector_search`) - -**原理**:将用户查询通过 Embedding 模型转为向量,在向量数据库中做近似最近邻搜索,找出语义最相似的文档片段。 - -**适用场景**:语义理解类查询,例如"有没有关于性能优化的内容"。 - -**核心代码**(`aperag/flow/runners/vector_search.py`): - -```python -# 1. 生成查询向量 -vector = embedding_model.embed_query(query) - -# 2. 在向量数据库中查询 -results = context_manager.query( - query, - score_threshold=similarity_threshold, - topk=top_k, - vector=vector, - index_types=["vector"], - chat_id=chat_id, -) - -# 3. 标记召回类型 -for item in results: - item.metadata["recall_type"] = "vector_search" -``` - -**输入参数**: - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `top_k` | 返回结果数量上限 | 5 | -| `similarity_threshold` | 相似度阈值,低于此值的结果过滤掉 | 0.2 | -| `collection_ids` | 检索的知识库 ID 列表 | — | -| `chat_id` | 会话 ID(会话文件检索时用于过滤) | null | - ---- - -### 2. 全文检索(`fulltext_search`) - -**原理**:基于关键词的倒排索引检索,支持精确词匹配和布尔查询。 - -**适用场景**:精确词语查询,例如"找出包含'PostgreSQL'的段落"。 - -**核心代码**(`aperag/flow/runners/fulltext_search.py`): - -```python -# 支持自定义关键词,或从查询中自动提取 -if not keywords: - keywords = extract_keywords(query) - -results = await fulltext_indexer.search( - index=index_name, - query=query, - keywords=keywords, - topk=top_k, - chat_id=chat_id, -) -``` - -**输入参数**: - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `top_k` | 返回结果数量上限 | 5 | -| `keywords` | 自定义关键词列表(可选,不传则从 query 自动提取) | [] | -| `collection_ids` | 检索的知识库 ID 列表 | — | -| `chat_id` | 会话 ID | null | - ---- - -### 3. 图谱检索(`graph_search`) - -**原理**:基于知识图谱(Knowledge Graph)的检索。文档在建索引时,LightRAG 会从中提取实体和关系,构建图谱。检索时,在图谱中做 hybrid 模式(向量 + 关键词)的子图查询,返回相关的实体上下文。 - -**适用场景**:需要多跳推理的查询,例如"张三负责的团队用到了哪些技术栈"。 - -**前提条件**:知识库必须启用 `enable_knowledge_graph` 选项。 - -**核心代码**(`aperag/flow/runners/graph_search.py`): - -```python -# 需要集合开启了知识图谱 -if not config.enable_knowledge_graph: - return [] - -# 创建 LightRAG 实例并查询 -rag = await lightrag_manager.create_lightrag_instance(collection) -param = QueryParam( - mode="hybrid", # 向量 + 关键词混合查询 - only_need_context=True, # 只需要图谱上下文,不需要 LLM 生成 - top_k=top_k, -) -context = await rag.aquery_context(query, param=param) -``` - -**输入参数**: - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `top_k` | 返回结果数量上限 | 5 | -| `collection_ids` | 检索的知识库 ID 列表 | — | - ---- - -### 4. 摘要检索(`summary_search`) - -**原理**:每个文档在建索引时会生成文档级别的摘要向量。检索时对摘要向量做近似最近邻搜索,以文档整体粒度召回,适合"找和这个主题相关的文档"类查询。 - -**适用场景**:需要文档级别召回(而非段落级别)的场景。 - -**核心代码**(`aperag/flow/runners/summary_search.py`): - -```python -results = context_manager.query( - query, - score_threshold=similarity_threshold, - topk=top_k, - vector=vector, - index_types=["summary"], # 只查摘要索引 - chat_id=chat_id, -) -for item in results: - item.metadata["recall_type"] = "summary_search" -``` - -**输入参数**: - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `top_k` | 返回结果数量上限 | 5 | -| `similarity` | 相似度阈值 | 0.2 | -| `collection_ids` | 检索的知识库 ID 列表 | — | - ---- - -### 5. 视觉检索(`vision_search`) - -**原理**:基于多模态 Embedding 对图像内容做向量检索。文档中的图片在建索引时会生成视觉向量,支持用自然语言描述来检索相关图片。 - -**适用场景**:包含大量图表、截图的知识库,例如"找一张关于系统架构的图"。 - -**输入参数**: - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `top_k` | 返回结果数量上限 | 5 | -| `similarity` | 相似度阈值 | 0.2 | -| `collection_ids` | 检索的知识库 ID 列表 | — | - ---- - -### 6. 合并节点(`merge`) - -所有检索节点的结果汇聚到 `merge` 节点: - -```python -# aperag/flow/runners/merge.py -@register_node_runner("merge", input_model=MergeInput, output_model=MergeOutput) -class MergeNodeRunner(BaseNodeRunner): - async def run(self, ui: MergeInput, si: SystemInput): - # 合并所有检索路的 docs - all_docs = ( - ui.vector_search_docs - + ui.fulltext_search_docs - + ui.graph_search_docs - + ui.summary_search_docs - + ui.vision_search_docs - ) - - # 按文本内容去重 - if ui.deduplicate: - seen = set() - unique_docs = [] - for doc in all_docs: - if doc.text not in seen: - seen.add(doc.text) - unique_docs.append(doc) - return MergeOutput(docs=unique_docs), {} - - return MergeOutput(docs=all_docs), {} -``` - -Merge 策略: -- **合并策略(`merge_strategy`)**:目前为 `union`(取并集) -- **去重(`deduplicate`)**:默认开启,按文本内容去重,避免同一段落被多个检索路召回 - ---- - -### 7. 重排节点(`rerank`) - -Rerank 节点对合并后的文档列表按与查询的相关性重新排序,过滤噪声,提升最终结果质量: - -```python -# aperag/flow/runners/rerank.py -@register_node_runner("rerank", input_model=RerankInput, output_model=RerankOutput) -class RerankNodeRunner(BaseNodeRunner): - async def run(self, ui: RerankInput, si: SystemInput): - if ui.use_rerank_service: - # 调用专用 Rerank 模型服务(如 Jina Reranker、Cohere 等) - rerank_service = RerankService(...) - docs = await rerank_service.rerank(si.query, ui.docs) - else: - # 降级策略:按原始召回分数排序 - docs = sorted(ui.docs, key=lambda d: d.score, reverse=True) - return RerankOutput(docs=docs), {} -``` - -Rerank 行为由 `SearchRequest.rerank` 字段控制: -- `rerank=true`:尝试调用用户配置的 Rerank 模型服务;若未配置,降级为按分数排序 -- `rerank=false`:直接按合并后的召回分数排序 - ---- - -## 检索结果 - -流程执行完成后,`rerank` 节点的输出 `docs` 被转换为 `SearchResultItem` 列表: - -```python -for idx, doc in enumerate(docs): - items.append(SearchResultItem( - rank=idx + 1, - score=doc.score, - content=doc.text, - source=doc.metadata.get("source", ""), - recall_type=doc.metadata.get("recall_type", ""), # 标明来自哪种检索 - metadata=doc.metadata, - )) -``` - -`recall_type` 字段枚举值: - -| 值 | 含义 | -|----|------| -| `vector_search` | 来自向量检索 | -| `fulltext_search` | 来自全文检索 | -| `graph_search` | 来自图谱检索 | -| `summary_search` | 来自摘要检索 | -| `vision_search` | 来自视觉检索 | - ---- - -## 会话文件检索 - -`execute_search_flow` 也被复用于会话(Chat)内的临时文件检索,入口为: - -``` -POST /api/v1/chats/{chat_id}/search -``` - -区别在于传入了 `chat_id` 参数,各检索节点会利用该参数过滤,只检索属于该会话的上传文件,不会混入知识库的全局文档。 - ---- - -## 关键文件索引 - -| 文件 | 职责 | -|------|------| -| `aperag/mcp/server.py` | MCP 工具定义,`search_collection` 入口 | -| `aperag/app.py` | FastAPI 应用,MCP 挂载点 | -| `aperag/views/collections.py` | REST API 路由 `/collections/{id}/searches` | -| `aperag/service/collection_service.py` | `create_search` 和 `execute_search_flow` 实现 | -| `aperag/flow/engine.py` | Flow 执行引擎,拓扑排序与并行调度 | -| `aperag/flow/base/models.py` | DAG 数据模型,NodeRunner 注册机制 | -| `aperag/flow/runners/vector_search.py` | 向量检索 Runner | -| `aperag/flow/runners/fulltext_search.py` | 全文检索 Runner | -| `aperag/flow/runners/graph_search.py` | 图谱检索 Runner(基于 LightRAG) | -| `aperag/flow/runners/summary_search.py` | 摘要检索 Runner | -| `aperag/flow/runners/vision_search.py` | 视觉检索 Runner | -| `aperag/flow/runners/merge.py` | 多路结果合并 Runner | -| `aperag/flow/runners/rerank.py` | 结果重排 Runner | - ---- - -## 相关文档 - -- [索引链路架构设计](./indexing_architecture.md):了解各检索类型的索引是如何构建的 -- [图索引构建流程](./graph_index_creation.md):深入了解知识图谱的构建过程 -- [MCP API 集成指南](../integration/mcp-api.md):如何在 Agent 中接入 ApeRAG 的 MCP 工具 diff --git a/docs/zh-CN/design/vector_db_abstraction.md b/docs/zh-CN/design/vector_db_abstraction.md deleted file mode 100644 index 13cd07a68..000000000 --- a/docs/zh-CN/design/vector_db_abstraction.md +++ /dev/null @@ -1,435 +0,0 @@ -# 向量数据库抽象层设计分析(ApeRAG) - -> Status: **M2 + M3 已落地**(本文从分析文档升级成"文档即实现")。两个 -> 后端(Qdrant、pgvector)共存、同一抽象,可通过 `VECTOR_DB_TYPE` 一行 -> env 切换。 - -## 变更记录 - -| 日期 | 内容 | -|---|---| -| 2026-04-20 | 初稿:分层、DSL、三后端草图、路线图 | -| 2026-04-21 | M2 落地:`VectorFilter` DSL、Qdrant translator、`VectorPoint`、client pool、`retrieve()` 入基类 | -| 2026-04-22 | M3 落地:pgvector 后端、DTO 全套(`TenantRef`/`VectorShape`/`QueryRequest`/`SearchHit`)、`upsert` 进入契约、去 LlamaIndex 写依赖 | - -## 1. 背景与目标 - -当前 ApeRAG 只有一个向量后端:Qdrant。在 -[`qdrant_memory_optimization.md`](./qdrant_memory_optimization.md) 的改造后, -Qdrant 连接器已经承担了三件相互耦合的事情: - -1. **物理布局路由**:按 `(vector_size, distance)` 选全局 collection。 -2. **多租户语义**:payload `collection_id` + `is_tenant` 索引。 -3. **存储与索引调优**:INT8 量化、HNSW on_disk、segment 数、memmap 阈值等。 - -未来我们希望: - -- 线上大集群保留 Qdrant(性能/隔离/量化成熟); -- 中小规模或需要单一 PostgreSQL 技术栈的部署可以走 **pgvector**(少一个组件); -- 未来潜在需求(超大规模、GPU 索引)可切换 **Milvus**; -- 对于"快速验证"或 CI/本地开发,期望一个纯内存实现(`:memory:` 或 in-proc)。 - -所以需要一个**向上/向下双向稳定**的抽象层。本文给出选型、接口草案与迁移策略。 - ---- - -## 2. 当前代码实际的抽象现状(事实核查) - -| 层 | 文件 | 角色 | 健康度 | -|---|---|---|---| -| 抽象基类 | `aperag/vectorstore/base.py`:`VectorStoreConnector` | `search / delete / create_collection / delete_collection` | 不完整:`retrieve()` 只在 Qdrant 实现里有 | -| 适配器 | `aperag/vectorstore/connector.py`:`VectorStoreConnectorAdaptor` | `match vector_store_type` 分发 | 只支持 qdrant 分支 | -| 具体实现 | `aperag/vectorstore/qdrant_connector.py` | Qdrant | 承载多租户 + 优化所有细节 | -| 上层过滤器 | `aperag/context/context.py`:`ContextManager._create_combined_filter` | 业务过滤 | **直接 import `qdrant_client.models`**,抽象破口 | -| 上层入口 | 索引写入 `aperag/index/*.py`、检索 `aperag/service/search_pipeline_service.py` | 每次按需构造 `VectorStoreConnectorAdaptor`、`ContextManager` | 每次请求重建连接 | -| 分片路由 | `aperag/config.py`:`build_vector_db_context`、`get_vector_db_connector` | 注入 `multitenant/quantization/...` ctx | Qdrant 专属字段混在 Config 里 | - -**当前抽象的 4 个真实缺口**(2026-04-20 分析;M2 + M3 之后全部关闭): - -| # | 缺口 | 状态 | -|---|---|---| -| 1 | `retrieve(ids)` 只在 Qdrant 连接器上存在 | ✅ 已进基类,返回 `VectorPoint` | -| 2 | `ContextManager` 硬编码 `qdrant_client.models` | ✅ 已迁到 `VectorFilter` DSL | -| 3 | `LlamaIndex.QdrantVectorStore` 被直接暴露给业务层 | ✅ M3:新增 `upsert(points)`,业务写路径彻底不经 LlamaIndex | -| 4 | 每次 search 重建 `QdrantClient` | ✅ 已进程级复用 | - -LlamaIndex 在代码库里的实际地位:仅剩的存在形式是 `TextNode` 作为 -chunker / embedder 的**中间**数据结构。在 `embedding_utils.py` 和 -`vision_index.py` 里,`BaseNode` 经过 `nodes_to_vector_points()`(见 -`aperag/vectorstore/llama_index_adapter.py`)一次性转成 -backend-neutral 的 `VectorPoint`;连接器层(Qdrant / pgvector)对 -LlamaIndex 完全无感。**读路径**的 `_node_content` 反序列化逻辑被抽到 -`flatten_node_payload()`(`aperag/vectorstore/dto.py`),只为了向后 -兼容 M2 之前由 `LlamaIndex.QdrantVectorStore.add()` 写入的老数据;新 -写入的数据采用扁平 `{text, metadata}`,读路径两种都支持。 - ---- - -## 3. 三个候选后端的能力矩阵 - -| 能力 | Qdrant ≥1.11 | pgvector (pg 16 + pgvector 0.7+) | Milvus 2.4+ | -|---|---|---|---| -| 原生 payload 过滤(keyword match/IN/OR) | ✅ | ✅ (`WHERE`) | ✅ (expr) | -| 向量量化(int8 / binary) | ✅ 内置 scalar+binary | `halfvec`(fp16)、`bit`、需手工 | ✅ IVF/HNSW+PQ/SQ | -| HNSW on_disk / mmap | ✅ | HNSW 常驻内存,不支持 on_disk;表/toast 走 OS page cache | ✅(DiskANN / MMAP) | -| 多租户 defragmentation | ✅ `is_tenant` payload | 需手工(按租户建 partitioned table) | ✅ partition/collection-per-tenant | -| 单机 in-proc 测试 | ✅ `:memory:` | 需起 pg | ✅ `milvus-lite` | -| Hybrid search(BM25 + 向量) | ✅(sparse vector + fusion) | `tsvector + vector` 手工 | ✅(sparse vector) | -| 操作学习成本 | 中(一套独立 API) | 低(就是 SQL) | 高(大量配置项) | -| 与现有技术栈耦合 | 独立服务 | ApeRAG 已有 PG,**省一个组件** | 独立服务 | - -**结论**: -- Qdrant 仍然是**生产默认**(已对量化 + 多租户做过深度优化)。 -- pgvector 适合作为**"少组件"部署模式**的一等公民(比如 ApeRAG-Lite、CI、自托管用户)。 -- Milvus 作为**长期备选**(大规模 / GPU 加速场景),短期不投入实现。 - ---- - -## 4. 抽象层分层设计 - -建议抽象分成三层: - -### 4.1 Transport / 连接管理层(L0) - -- 责任:客户端生命周期、连接池、池化单例、健康探测。 -- **不涉及任何数据语义**。 -- 代码位置建议:`aperag/vectorstore/_client.py`(新增)。 - -```python -class VectorClientFactory: - def get(self, backend: str, dsn_ctx: dict) -> Any: - """Return a cached, process-level client. Thread-safe, idempotent.""" -``` - -目的:修掉第 2.4 条"每次 search 重建 client"的性能缺口。 - -### 4.2 能力抽象层(L1) - -- 责任:**业务侧能理解的向量操作**,与具体后端无关。 -- 代码位置:`aperag/vectorstore/base.py`(扩展现有接口)。 - -建议的接口草案: - -```python -class VectorStoreConnector(ABC): - # ---- 集合 / 租户管理 ---- - @abstractmethod - def ensure_tenant(self, tenant: TenantRef, shape: VectorShape) -> None: ... - @abstractmethod - def drop_tenant(self, tenant: TenantRef, *, purge_all_shapes: bool = False) -> None: ... - - # ---- 写入 ---- - @abstractmethod - def upsert(self, tenant: TenantRef, points: Iterable[VectorPoint]) -> list[str]: ... - @abstractmethod - def delete_by_ids(self, tenant: TenantRef, ids: Sequence[str]) -> None: ... - @abstractmethod - def delete_by_filter(self, tenant: TenantRef, flt: VectorFilter) -> None: ... - - # ---- 查询 ---- - @abstractmethod - def search( - self, tenant: TenantRef, query: QueryWithEmbedding, - *, flt: Optional[VectorFilter] = None, - score_threshold: float | None = None, - search_opts: SearchOptions | None = None, - ) -> QueryResult: ... - - @abstractmethod - def retrieve(self, tenant: TenantRef, ids: Sequence[str], *, - with_vectors: bool = False) -> list[VectorPoint]: ... -``` - -四个配套 DTO(统一跨后端的数据模型): - -```python -@dataclass(frozen=True) -class TenantRef: - id: str # ApeRAG collection id (or None if embed-only mode) - shape: VectorShape | None = None # optional hint - -@dataclass(frozen=True) -class VectorShape: - size: int - distance: Literal["cosine", "dot", "euclid"] - -@dataclass -class VectorPoint: - id: str - vector: list[float] - payload: dict[str, Any] - # on read: score optional - score: float | None = None - -class VectorFilter: - """Backend-neutral filter tree. See §4.3.""" -``` - -这一层要**严格禁止**暴露 LlamaIndex / Qdrant / psycopg 类型。 - -### 4.3 业务过滤器 DSL(L1 子模块) - -**最痛的一处**:当前 `ContextManager._create_combined_filter` 直接构造 -`qdrant_client.models.Filter`。pgvector/Milvus 无法复用。 - -建议一个极小的、**面向 RAG 使用场景**的过滤 DSL: - -```python -@dataclass(frozen=True) -class Eq: key: str; value: str | int | float -@dataclass(frozen=True) -class In: key: str; values: Sequence[str | int | float] -@dataclass(frozen=True) -class IsEmpty: key: str -@dataclass(frozen=True) -class And: parts: Sequence["VectorFilter"] -@dataclass(frozen=True) -class Or: parts: Sequence["VectorFilter"] -@dataclass(frozen=True) -class Not: inner: "VectorFilter" - -VectorFilter = Union[Eq, In, IsEmpty, And, Or, Not] -``` - -每个后端有自己的 **translator**: - -| DSL 节点 | Qdrant | pgvector | Milvus | -|---|---|---|---| -| `Eq(k, v)` | `FieldCondition(key=k, match=MatchValue(v))` | `payload->>'k' = :v` | `k == v` | -| `In(k, V)` | `FieldCondition(key=k, match=MatchAny(V))` | `payload->>'k' = ANY(:V)` | `k in [...]` | -| `IsEmpty(k)` | `IsEmptyCondition(PayloadField(k))` | `NOT (payload ? 'k')` | `not (exists k)` | -| `And` | `Filter(must=[...])` | `AND` | `&&` | -| `Or` | `Filter(should=[...])` | `OR` | `\|\|` | -| `Not` | `Filter(must_not=[...])` | `NOT` | `!` | - -**多租户守卫自然内建**:Connector 在 `search / delete_by_filter` 时自动 `And(Eq("collection_id", tenant.id), user_filter)`;业务层完全无感。 - -### 4.4 存储/索引调优层(L2) - -每个后端独有。不对外暴露。 - -- Qdrant: 当前的 `_hnsw_config / _optimizers_config / _quantization_config` 保留在该后端的 `*_connector.py` 中,通过 `SearchOptions.hints` 透传。 -- pgvector: `CREATE INDEX ... USING hnsw WITH (m=16, ef_construction=64)`,量化用 `halfvec`。 -- Milvus: `IVF_FLAT/HNSW/DISKANN` + PQ/SQ 参数。 - -L2 的配置**不**暴露给 L1 调用方;只暴露一个能力类的 `SearchOptions`: - -```python -@dataclass -class SearchOptions: - top_k: int - consistency: Literal["eventual", "majority", "strong"] = "majority" - hints: dict[str, Any] = field(default_factory=dict) # 后端白名单参数 -``` - ---- - -## 5. 三种后端的实现草图 - -### 5.1 Qdrant(现有 → 重构成 L1) - -- `ensure_tenant` → 现有 `_ensure_collection` + `_ensure_tenant_payload_index`。 -- `upsert` → 包装 `self.client.upsert`(不再经由 LlamaIndex `store.add`,从而解除 `_node_content` 依赖;或 keep-as-is 并在读路径做兼容)。 -- 过滤器 translator:`_to_qdrant_filter(vf: VectorFilter) -> rest.Filter`。 -- SearchOptions.hints 支持 `hnsw_ef`、`exact`。 - -### 5.2 pgvector(新增,新增包 `aperag/vectorstore/pgvector_connector.py`) - -**Schema 设计**: - -```sql -CREATE EXTENSION IF NOT EXISTS vector; - --- 每个 (size, distance) 一张表,类比 Qdrant 的多租户 collection -CREATE TABLE aperag_vectors_1024_cosine ( - id UUID PRIMARY KEY, - tenant_id TEXT NOT NULL, -- = ApeRAG collection id - embedding vector(1024) NOT NULL, - payload JSONB NOT NULL -); - --- 租户过滤 + HNSW 共用。分区表可选(pg 14+)。 -CREATE INDEX ON aperag_vectors_1024_cosine (tenant_id); -CREATE INDEX ON aperag_vectors_1024_cosine USING hnsw (embedding vector_cosine_ops) - WITH (m=16, ef_construction=64); --- JSONB 字段上的 GIN 索引供 payload 过滤 -CREATE INDEX ON aperag_vectors_1024_cosine USING GIN (payload); -``` - -**搜索**: - -```sql -SELECT id, payload, embedding <=> :q AS score - FROM aperag_vectors_1024_cosine - WHERE tenant_id = :tenant - AND payload @> :payload_filter -- 由 VectorFilter translator 生成 - ORDER BY embedding <=> :q - LIMIT :k; -``` - -**量化**:用 `halfvec(1024)` 列替代 `vector(1024)`,磁盘/内存减半;搜索时 -`HALFVEC_L2_OPS` / `HALFVEC_COSINE_OPS`。`bit(dim)` 也可用作更激进量化。 - -**多租户 defrag**:pg 14+ 可按 `tenant_id` 做 **list partitioning**,每个租户一张物理表,HNSW 索引天然按分区切分。初期不做,租户数量到万级再评估。 - -### 5.3 Milvus(未来) - -- `DataType.FLOAT_VECTOR` + `IVF_SQ8 / HNSW`; -- payload 字段走 scalar field(`VarChar(tenant_id)`); -- 每个 `(vector_size, distance)` 一个 collection,与 Qdrant 对齐; -- partition key = `tenant_id`(近似 Qdrant 的 `is_tenant`)。 - -只在需要时实现,目前不动。 - ---- - -## 6. 路线图(建议) - -分成 4 个 milestone,每个都能**独立上线、独立回滚**: - -### M1(本 PR 已落地的改造不变) - -- Qdrant 多租户 + 量化 + embedding 锁定。 -- 本文档纳入 repo,作为后续决策的依据。 - -### M2:抽象层最小可行(✅ **已落地**) - -实际落地的范围(比最初草案更克制,符合 "零答疑" 原则): - -- ✅ `VectorFilter` DSL(`aperag/vectorstore/filters.py`):`Eq / In / - IsEmpty / And / Or / Not` + `all_of / any_of` 短路 helper。 -- ✅ Qdrant translator(`_translate_filter` / `_normalize_filter_input` - 在 `qdrant_connector.py`)。兼容旧的 `rest.Filter` 直传(迁移脚本在用)。 -- ✅ `ContextManager._create_combined_filter` 改为产出 DSL,**不再** import - `qdrant_client.models`。 -- ✅ `VectorStoreConnector` 基类补 `retrieve()` 抽象方法。 -- ✅ 只引入一个最小 DTO `VectorPoint(id, payload, vector?)` 给 `retrieve()` - 用;没有引入 `TenantRef / VectorShape / QueryResult / SearchOptions`, - 避免"只有一个实现时多 DTO 的人生思考题"。 -- ✅ `QdrantClient` 按 endpoint 进程级复用 - (`_get_or_create_client` + 双检锁);`:memory:` 显式绕开缓存以保护测试隔离。 -- **没做** 的事:解耦 LlamaIndex(`store.add(nodes)` 和 - `node_to_metadata_dict` 在当前是 Qdrant 后端的实现细节,等真做 - pgvector 的时候再一起换);没引入统一的 `upsert(tenant, points)` - 写接口——目前的 `store.add` + `delete(ids)` 已经够用,M3 再说。 - -实际代码规模:5 个新文件 + 3 个现有文件重构;测试新增 `test_filters.py` / -`test_qdrant_filter_translation.py` / `test_qdrant_client_cache.py` / -`test_context_manager_filter.py`。 - -### M3:pgvector 实现(✅ **已落地**) - -实际落地范围(比草案更激进,把 M2 时故意推迟的"DTO 全套 + 去 LlamaIndex -写依赖"都一并做掉了): - -- ✅ `aperag/vectorstore/pgvector_connector.py`:动态 `CREATE TABLE IF - NOT EXISTS aperag_vectors__`(与 Qdrant 的物理分片命名 - 对齐),HNSW + tenant_id + GIN(payload) 三个索引一并建好;`CREATE - EXTENSION IF NOT EXISTS vector` 由连接器发起。 -- ✅ 不走 alembic 模板:表按 shape 动态创建,与 Qdrant 的 `ensure_collection` - 一致的幂等语义;进程级 `_ENSURED_TABLES` 缓存避免重复 DDL。 -- ✅ SQL filter translator(`_SqlFilter.translate`)把 `VectorFilter` - 编译成参数化 `WHERE` 片段 + bind dict。**所有值都走 bind 参数**, - key 走白名单校验,彻底屏蔽注入面。 -- ✅ SQLAlchemy `Engine` 按 `database_url` 进程级共享,与 Qdrant - `_get_or_create_client` 的模式保持一致。 -- ✅ 默认复用 ApeRAG 主 Postgres(共享 `DATABASE_URL`),"零新组件" - 部署;若 vector 规模压垮主 DB,设 `PGVECTOR_DATABASE_URL` 独立出去, - 一个 env 搞定。 -- ✅ embedding lock(前端 + service)和 Qdrant 完全共用,无需改动。 -- ✅ 端到端测试 `test_pgvector_end_to_end.py`:10 个用例,gated by - `APERAG_TEST_PGVECTOR_URL` env(本地 `apecloud/pgvector:pg16` + 主 - DB 起在 docker-compose 时默认可跑)。 - -### M2 的补完(本次 PR 一起做) - -M2 当时"保留尾巴"的三件事,M3 实现 pgvector 时**一次性补齐**: - -- ✅ **DTO 全套**:`TenantRef(id)` / `VectorShape(size, distance)` / - `VectorPoint(id, vector, payload)` / `QueryRequest(embedding, top_k, - flt, score_threshold, with_vectors, hints)` / `SearchHit(id, score, - payload, vector?)`,全部 frozen dataclass。`dto.py` 严禁 import 任何 - 后端 SDK。 -- ✅ **`connector.upsert(points)` 抽象**:`VectorStoreConnector` 基类 - 新增抽象方法;Qdrant / pgvector 都原生实现;`embedding_utils.py` 和 - `vision_index.py`(两处)迁过去,不再用 `store.add(nodes)`。 -- ✅ **`delete_by_filter(flt)` 抽象**:基类方法,两后端都支持;空/None - 过滤器被显式 raise 而不是"全删"。 -- ✅ **`drop_tenant(purge_all_shards)`** 替代历史的 `delete_collection`: - 命名更准确(多租户语义下它从不物理 drop 一个 collection); - `purge_all_shards=True` 的语义两后端完全一致——按 `aperag_vectors_*` - 前缀扫分片、按 tenant_id 删行。 -- ✅ **`base.py::VectorStoreConnector` 不再要求 `self.store`**:LlamaIndex - 的 `VectorStore` 类型从基类契约里拿掉;后端各自决定是否持有。 -- ✅ **`flatten_node_payload()` 抽到 DTO 模块**:所有读路径(Qdrant - `search` 结果、`document_service` 的 chunk 预览)统一走它,把 - "`_node_content` 反序列化 + relationships 派生 source" 的历史逻辑 - 收到一个地方,两个后端共用。 - -### M4:生产切换策略(按需) - -- 配置:`VECTOR_DB_TYPE=qdrant|pgvector`。 -- 混合部署期间支持"读双写单"的数据迁移模式(类似本 PR 的 - `scripts/migrate_qdrant_multitenancy.py`)。 -- Milvus 留作后续调研,不进本 roadmap。 - ---- - -## 7. 风险与取舍 - -| 风险 | 说明 | 缓解 | -|---|---|---| -| 抽象稀释 Qdrant 独有能力 | 如 sparse vector / hybrid search / HNSW ef_construct | `SearchOptions.hints` 留后门;新能力进抽象需要 ≥2 后端支持 | -| pgvector 高 QPS 表现不如 Qdrant | pg 进程级锁、HNSW 全量常驻内存 | M3 benchmark 门槛;低 QPS 场景优先 | -| 过滤 DSL 表达不完备 | 早期只支持 Eq/In/IsEmpty/And/Or/Not,没有 range/geo | 按需扩展,保持单一职责 | -| LlamaIndex 直接耦合 | `node_to_metadata_dict` 在 payload 里塞大量 `_node_content` 等字段 | M2 中 Qdrant 改为直接走 `client.upsert`,不再经 `LlamaIndex.QdrantVectorStore.add`;读路径做兼容 | -| embedding 模型切换 vs 抽象层 | 不同 backend 对重建索引成本不同 | 本次 embedding 锁定(见 §8)规避了该问题,抽象层之后也无需处理 | - ---- - -## 8. 与"embedding 模型锁定"的关系 - -本 PR 引入的 embedding 锁定(`CollectionService._reject_embedding_change`) -是抽象层设计的**前置条件**: - -- 没锁定:每个后端都要实现"向量维度变化时的数据迁移"——pgvector 要 alter - column、Milvus 要 drop+recreate collection、Qdrant 要 scroll+upsert。 - 每种都是大手术。 -- 锁定后:向量维度 = collection 生命周期常量,后端只要专注"同维度高效检索"。 - -因此抽象层假定 `VectorShape` 在 `ensure_tenant` 之后不可变。 - ---- - -## 9. 开放问题 - -1. **是否把 fulltext/knowledge graph 也抽象到同一层?** 目前 ApeRAG 有 - - vector: Qdrant - - fulltext: Elasticsearch - - graph: LightRAG (存在 Postgres/Neo4j) - 三者 indexing 流程在 `aperag/index/` 已有 `BaseIndexer`,各自独立——**不建议** - 把它们塞进向量抽象层,保持单一职责。 - -2. **是否与 LlamaIndex 解耦?** LlamaIndex 带来了:chunking、node schema、 - retrieval pipeline。短期我们只想解 `QdrantVectorStore` 这一处。长期如果 - 想支持多种 chunker(比如 unstructured、langchain),才值得把 LlamaIndex - 也抽象掉。 - -3. **是否支持"多向量字段"?** 如 dense + sparse 混合检索。当前架构 - `VectorShape(size, distance)` 只支持单向量;未来要扩展成 - `VectorShape(fields: Mapping[str, VectorField])`。 - ---- - -## 10. 附:何时**不**应该做这个抽象 - -如果短期内只跑 Qdrant,不会切换,做这层抽象就是**过度设计**。 - -要启动 M2+ 的触发条件(满足任一即可): - -- 用户明确要求某个部署不能依赖 Qdrant。 -- 我们要做 "ApeRAG-Lite"(只依赖 PG + Redis 的极简部署包)。 -- Qdrant 成本/运维成为增长瓶颈。 - -当前三者都还没触发——这份文档的作用是**提前想清楚路径**,等触发发生时 -不用从零开始设计。 diff --git a/docs/zh-CN/design/vision_index_creation.md b/docs/zh-CN/design/vision_index_creation.md deleted file mode 100644 index 90face672..000000000 --- a/docs/zh-CN/design/vision_index_creation.md +++ /dev/null @@ -1,261 +0,0 @@ -# ApeRAG Vision Index 创建流程技术文档 - -## 1. 概述 - -本文档旨在为 ApeRAG 系统设计一套全新的 "Vision Index" 方案。随着多媒体数据的日益增多,为图像等视觉内容建立高效的索引和召回机制变得至关重要。Vision Index 将作为对现有 Vector Index、FullText Index 和 Graph Index 的补充,专门处理图像等多媒体文档,增强系统对视觉信息的理解和检索能力。 - -### 1.1 设计目标 - -- **无缝集成**: Vision Index 将遵循现有的 `indexing_architecture` 双链路设计,作为一种新的 `DocumentIndexType` 无缝集成到系统中。 -- **双路径索引**: 提供两种核心的图像索引方式: - 1. **纯向量路径 (Pure Vision Embedding)**: 直接通过多模态模型获取图像的向量表示,用于相似性召回。 - 2. **视觉转文本路径 (Vision-to-Text)**: 通过视觉语言模型(VLM)对图像进行描述、总结和光学字符识别(OCR),将生成的文本内容进行二次索引(如文本向量化),利用文本相似性进行召ow。 -- **可扩展性**: 架构设计应支持未来轻松扩展,以兼容新的多模态模型、VLM 和图像处理技术。 -- **性能考量**: 异步处理图像,并考虑计算资源的消耗,确保不影响系统整体性能。 - -### 1.2 与现有架构的融合 - -Vision Index 将作为 `create_index_task` 的一个新类型并行执行,与 `VECTOR`, `FULLTEXT`, `GRAPH` 索引的创建流程保持一致。 - -```mermaid -graph TB - subgraph "Celery任务执行层 (扩展)" - K[trigger_create_indexes_workflow] --> L[group并行执行] - L --> M[create_index_task.VECTOR] - L --> N[create_index_task.FULLTEXT] - L --> O[create_index_task.GRAPH] - L --> P_NEW((create_index_task.VISION)) - - M --> Q[chord回调] - N --> Q - O --> Q - P_NEW --> Q - end - - style P_NEW fill:#cde4ff,stroke:#5a96e6,stroke-width:2px,font-weight:bold -``` - -## 2. 核心设计思路 - -### 2.1 数据库与模型服务扩展 - -#### 2.1.1 数据库模型扩展 -为了支持 Vision Index,`DocumentIndex` 模型需要扩展。 - -**`aperag/db/models.py`** -```python -class DocumentIndexType(str, Enum): - VECTOR = "VECTOR" - FULLTEXT = "FULLTEXT" - GRAPH = "GRAPH" - VISION = "VISION" # 新增索引类型 -``` - -#### 2.1.2 Model Provider 扩展 -系统中的 `Model Provider` 模块需要扩展,以识别和管理用于 Vision Index 的模型。 - -- **模型类型**: 新增 `MULTIMODAL_EMBEDDING` 和 `VLM` 两种模型类型。 -- **模型注册**: 在模型配置文件中(如 `alibaba_bailian_models_completion.json`),需要为支持的模型添加相应的类型标注。 - -#### 2.1.3 Collection (数据集) 配置 -在用户创建 Collection 时,UI层面需提供以下配置选项: -- **启用 Vision Index**: 一个开关,决定是否为该 Collection 创建视觉索引。 -- **选择多模态嵌入模型**: 如果启用,用户可以从已注册的 `MULTIMODAL_EMBEDDING` 模型中选择一个,用于“纯视觉向量”路径。 -- **选择VLM模型**: 用户可以从已注册的 `VLM` 模型中选择一个,用于“视觉转文本”路径。 - -用户可以只选择其中一种模型,也可以两种都选。系统将根据用户的配置,在创建索引时执行一个或两个路径。这些配置将随 Collection 信息一同存储,并传递给索引任务。 - -### 2.2 图像处理与解析 (`DocParser` 扩展) - -`DocParser` 的职责需要扩展,以实现“从文档提取图片”的能力。其输出应为图像内容的列表(如二进制数据或临时文件路径),供后续的 `create_index_task.VISION` 任务使用。 - -具体处理逻辑如下: -- **PDF 文档**: 将每一页渲染成一张图片。 -- **DOCX 文档**: 解析文档内容,提取所有嵌入的图片。 -- **MinerU 集成**: 如果系统配置了 MinerU,可用于为复杂文档和 OCR 场景生成更稳定的结构化解析结果。 -- **原生图片**: 对于 `JPEG, PNG` 等原始图片文件,直接返回其二进制内容。 -- **输出格式**: `parse_document_task` 的输出 `ParsedDocumentData` 中需要包含一个图像列表字段,例如 `images: List[bytes]`。 - -### 2.3 Vision Index 创建任务 - -`create_index_task` 将增加对 `VISION` 类型的处理逻辑。这个任务将是 Vision Index 的核心,它会根据 Collection 的配置决定执行哪几种索引路径。 - -```python -# config/celery_tasks.py - -@current_app.task(...) -def create_index_task(self, document_id: str, index_type: str, ...): - # ... - if index_type == DocumentIndexType.VISION: - # 调用 vision_indexer.create_index() - result = vision_indexer.create_index(document_id, parsed_data, ...) - # ... -``` - -一个新的 `aperag/index/vision_index.py` 文件将包含 `VisionIndexer` 类,负责具体的实现。该类将继承自 `aperag/index/base.py` 中的 `BaseIndexer`。 - -### 2.4 现有服务扩展 - -#### 2.4.1 `EmbeddingService` 扩展 -`aperag/llm/embed/embedding_service.py` 需要进行如下扩展: - -- **重载输入类型**: `embed_documents` 方法的输入参数 `texts: List[str]` 需要重载,使其能够接受 `List[Union[str, Image]]`,其中 `Image` 可以是图像的二进制数据或路径。 -- **逻辑分支**: 在方法内部,需要增加类型检查。如果输入是图像,则调用多模态 embedding 模型的特定逻辑;如果输入是文本,则保持现有逻辑。 -- **底层调用**: `_embed_batch` 方法中,调用 `litellm.embedding` 时,需要根据输入类型构建不同的 `input` 参数,以符合多模态模型的要求。 - -#### 2.4.2 `CompletionService` 扩展 -`aperag/llm/completion/completion_service.py` 需要进行如下扩展: - -- **支持多模态消息**: `_build_messages` 方法需要改造,使其能构建符合 VLM(如 GPT-4o)要求的图文混合消息体。例如,`content` 字段可以是一个包含文本和图像 URL 的列表。 -- **接口参数调整**: `agenerate` 等核心方法的 `prompt` 参数需要能够接收图像信息。 -- **底层调用**: 调用 `litellm.acompletion` 时,`messages` 参数需要能正确传递这种多模态结构。 - -## 3. Vision Index 详细数据流 - -Vision Index 的创建流程可以分为两条并行的子路径,最终的结果可以合并或独立存储。 - -```mermaid -flowchart TD - %% 定义样式 - classDef process fill:#e8f5e9,stroke:#4caf50,stroke-width:2px - classDef model fill:#fff3e0,stroke:#ff9800,stroke-width:2px - classDef data fill:#e3f2fd,stroke:#2196f3,stroke-width:2px - classDef decision fill:#fce4ec,stroke:#e91e63,stroke-width:2px - - subgraph "Vision Index 创建流程" - A["🖼️ 图像数据 (来自 docparser)"] --> B{Vision Indexing Strategy} - - subgraph "路径 A: 纯视觉向量" - B --> C["👁️ Multimodel Embedding Model
(e.g., CLIP, ViT)"] - C --> D["🖼️ Image Vector
(图像的向量表示)"] - end - - subgraph "路径 B: 视觉转文本" - B --> E["🧠 Vision Language Model (VLM)
(e.g., GPT-4o, LLaVA)"] - E --> F["- 图像描述
- 内容总结
- OCR文本提取"] - F --> G["📝 Combined Text
(合并后的描述性文本)"] - G --> H["🔡 Text Embedding Model
(e.g., bge-large-zh)"] - H --> I["📄 Text Vector
(文本的向量表示)"] - end - - D --> J["💾 向量数据库 (Vector Store)"] - I --> J - - G --> K["📝 全文搜索引擎 (Optional)
(Full-text Search Engine)"] - end - - %% 应用样式 - class A,D,G,I data - class C,E,H model - class F,J,K process - class B decision -``` - -### 3.1 路径 A: 纯视觉向量 (Pure Vision Embedding) - -1. **输入**: `DocParser` 提取的图像数据列表。 -2. **处理**: 根据 Collection 配置,初始化一个 `EmbeddingService` 实例,并传入选定的多模态 Embedding 模型。调用其 `embed_documents` 方法(传入图像列表)来生成每个图像的向量。 -3. **输出**: 一组能够代表各图像内容的向量。 -4. **存储**: 将向量存入向量数据库。向量记录的元数据需包含图像来源信息(如原文档ID、页码/图片序号)。 - -### 3.2 路径 B: 视觉转文本 (Vision-to-Text) - -1. **输入**: `DocParser` 提取的图像数据列表。 -2. **处理**: 根据 Collection 配置,初始化一个 `CompletionService` 实例,并传入选定的 VLM 模型。遍历图像列表,调用其 `agenerate` 方法(传入图像和相应的文本提示)来执行 VLM 任务。 - - **VLM任务**: VLM 对每个图像执行多项任务: - - **图像描述 (Description)**: 生成一段描述图像场景、物体、人物等的文字。 - - **内容总结 (Summary)**: 对图像核心内容进行一句话总结。 - - **OCR**: 提取图像中包含的所有可读文本。 -3. **文本合并**: 将上述三部分生成的文本(描述、总结、OCR)合并成一个单一的文档,使用明确的分隔符(如换行符)以区分不同部分的内容。 -4. **文本向量化**: 使用扩展后的 `EmbeddingService`,传入合并后的文本,生成文本向量。 -5. **存储**: - - 将生成的文本向量存入向量数据库。 - - (可选)将合并后的原始文本存入全文搜索引擎,以支持对图像内容的关键词搜索。 - -### 3.3 存储层设计 - -在向量数据库中,我们需要能够区分不同来源的向量。这可以通过在元数据(metadata)中添加字段来实现。 - -**向量记录元数据示例**: -```json -// 路径 A 的向量 -{ - "document_id": "doc_xyz", - "source_type": "vision_direct", - "model_used": "clip-vit-large-patch14" -} - -// 路径 B 的向量 -{ - "document_id": "doc_xyz", - "source_type": "vision_to_text", - "model_used": "text-embedding-ada-002", - "vlm_model_used": "gpt-4o", - "raw_text_ref": "path/to/generated_text.txt" // 指向生成的原始文本 -} -``` - -**向量等同性与去重**: -- **等同处理**: 路径 A 和路径 B 产生的向量在召回时被视为等同,使用相同的相似度计算方式进行检索。 -- **结果去重**: 在召回结果呈现时,如果多个召回的向量(无论是纯视觉向量还是文本向量)指向同一张源图片或同一文档的同一页,应将其视为单一来源进行去重,避免结果冗余。 - -## 4. 召回与重排(Rerank)策略 - -当用户提问时,系统利用 Vision Index 的策略如下: - -### 4.1 召回流程 -1. **文本问题**: 用户输入文本问题。系统将问题文本进行向量化,然后在向量数据库中进行相似性搜索。搜索范围将同时覆盖常规的文本向量、路径A的纯视觉向量以及路径B的视觉转文本向量。 -2. **图像问题**: 用户上传一张图片作为问题。系统可以并行执行以下两种召回方式: - - **视觉相似性召回 (Vision-to-Vision)**: 直接对问题图片进行向量化,在库中寻找相似的纯视觉向量(路径A的结果)。 - - **图生文召回 (Image-to-Text)**: 使用 VLM 将问题图片转换为文本描述,再用此文本描述去进行向量相似性搜索(可召回所有类型的向量)。 - -### 4.2 融合与重排(Rerank) -召回结果的融合与重排将采用分层策略: - -- **文本内容重排**: - - 从 **Vision Index 路径 B** 召回的结果(本质是文本)将与从 **Vector Index** 召回的常规文本结果合并。 - - 这批合并后的纯文本内容,将统一进入现有的 **Rerank** 模块进行重排序。 -- **视觉内容排序**: - - 从 **Vision Index 路径 A** 召回的结果(纯视觉匹配)**不进入** Rerank 模块。 - - 这部分结果将直接根据向量相似度得分进行排序,取前k个。 -- **最终结果呈现**: - - 系统将 Rerank 后的文本结果和按相似度排序的视觉结果分开聚合展示。例如,在UI上可以设计两个区域,一个用于展示文本召回结果,另一个用于展示图片召回结果。 - -## 5. 实现优先级(草案) - -### 第一阶段:核心框架与单一路径 -1. 在数据库模型中添加 `VISION` 类型。 -2. 实现 `VisionIndexer` 的基本框架和 `create_index_task` 的调度逻辑。 -3. 优先实现 **路径 B (Vision-to-Text)**,因为它可以复用现有的文本 Embedding 和全文检索引擎,集成成本较低。 -4. 在 `docparser` 中增加对常见图片格式(JPEG, PNG)的支持。 - -### 第二阶段:双路径实现与优化 -1. 实现 **路径 A (Pure Vision Embedding)**,集成一个主流的多模态模型。 -2. 设计并实现两种路径向量在存储和召回时的区分与融合策略。 -3. 完善 `docparser` 对复合文档中图像的提取能力。 - -### 第三阶段:高级功能 -1. 实现基于图像输入的查询功能。 -2. 开发高级的 rerank 策略,用于融合多模态召回结果。 -3. 性能监控与优化,特别是对 VLM 和多模态模型服务的调用。 - -## 6. 异常处理 - -Vision Index 的创建任务将完全融入现有的异步任务体系,因此也继承了其强大的异常处理机制。 -- **任务级重试**: `create_index_task` 任务在执行 `VisionIndexer` 逻辑时,如果发生临时性错误(如网络超时、外部服务5xx错误),Celery 会根据预设策略(如重试3次)自动重试。 -- **状态标记**: 如果任务在所有重试后仍然失败,`DocumentIndex` 表中对应的记录状态将被标记为 `FAILED`,并记录错误信息,以便后续排查或手动触发重建。 -- **版本验证**: 任务执行前会验证版本号,防止对已过期的状态进行操作,确保数据一致性。 - -## 7. 总结 - -本方案为 ApeRAG 设计了一个可扩展、高内聚的 Vision Index 框架。它不仅在功能上补全了系统对多媒体数据的处理能力,在架构上也与现有设计哲学深度融合。 - -### 核心技术特点 - -1. **架构一致性**: 完全遵循双链路、状态驱动的异步索引架构,作为一种新的索引类型无缝集成。 -2. **服务复用与扩展**: 通过扩展现有的 `EmbeddingService` 和 `CompletionService` 来支持多模态能力,避免了引入新的服务层,保持了架构的精简和高内聚。 -3. **双路径索引**: 创新地结合了“纯视觉向量”和“视觉转文本”两种路径,最大化地利用了图像信息,兼顾了视觉相似性与语义相似性两种召回模式。 -4. **配置灵活**: 用户可在创建 Collection 时自主选择是否启用视觉索引,并指定使用的具体模型,满足不同场景的需求。 -5. **生产就绪**: 方案内置了完整的错误处理、重试机制和状态管理,确保了在生产环境中的稳定性和可靠性。 - -通过这些设计,Vision Index 不仅是一个功能模块的增加,更是对 ApeRAG 整个 RAG 体系能力的有力增强。 diff --git a/docs/zh-CN/design/web-search-design.md b/docs/zh-CN/design/web-search-design.md deleted file mode 100644 index dd14d129c..000000000 --- a/docs/zh-CN/design/web-search-design.md +++ /dev/null @@ -1,740 +0,0 @@ -# ApeRAG Web搜索与内容读取服务设计文档 - -## 1. 设计概述 - -### 1.1 设计理念 - -ApeRAG Web搜索模块采用**Provider抽象模式**,参考现有LLM服务架构(EmbeddingService、RerankService等),提供统一的Web搜索和内容读取能力。 - -**核心特性**: -- **统一接口**:上层Service统一调用,底层可切换Provider -- **插件化架构**:新增搜索引擎或内容提取器只需实现Provider接口 -- **资源安全管理**:完整的异步资源生命周期管理 -- **双路供给**:同时提供HTTP API和MCP工具支持 -- **生产就绪**:内置错误处理、超时控制、并发限制 - -### 1.2 已实现架构 - -``` -┌─────────────────────────────────────────────────────────┐ -│ API Layer │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ HTTP Views │ │ MCP Tools │ │ -│ │ /api/v1/web/* │ │ web_search │ │ -│ └─────────────────┘ │ web_read │ │ -└──────────────┬──────────└─────────────────┘────────────┘ - │ │ - ┌──────▼──────┐ ┌──────▼──────┐ - │SearchService│ │ReaderService│ - │+ async with │ │+ async with │ - └──────┬──────┘ └──────┬──────┘ - │ │ - ┌─────────▼─────────┐ ┌─────▼─────────┐ - │BaseSearchProvider│ │BaseReaderProvider│ - │+ close() support │ │+ close() support│ - └─────────┬─────────┘ └─────┬─────────┘ - │ │ - ┌──────────▼──────────┐ ┌────▼──────────┐ - │ DuckDuckGoProvider │ │TrafilaturaProvider│ - │ JinaSearchProvider │ │JinaReaderProvider │ - └─────────────────────┘ └─────────────────┘ -``` - -## 2. 实际目录结构 - -### 2.1 已实现的模块结构 - -``` -aperag/websearch/ # Web搜索模块根目录 -├── __init__.py # 导出SearchService, ReaderService -├── search/ # 搜索功能模块 -│ ├── __init__.py # 导出SearchService, BaseSearchProvider -│ ├── base_search.py # 搜索Provider抽象基类 -│ ├── search_service.py # 搜索服务(支持上下文管理器) -│ └── providers/ # 搜索Provider实现 -│ ├── __init__.py # 导出所有Provider -│ ├── duckduckgo_search_provider.py # DuckDuckGo实现(默认) -│ └── jina_search_provider.py # JINA搜索实现 -├── reader/ # 内容读取功能模块 -│ ├── __init__.py # 导出ReaderService, BaseReaderProvider -│ ├── base_reader.py # 读取Provider抽象基类 -│ ├── reader_service.py # 读取服务(支持上下文管理器) -│ └── providers/ # 读取Provider实现 -│ ├── __init__.py # 导出所有Provider -│ ├── trafilatura_read_provider.py # Trafilatura实现(默认) -│ └── jina_read_provider.py # JINA读取实现 -├── utils/ # 工具模块 -│ ├── __init__.py # 导出工具类 -│ ├── url_validator.py # URL验证和规范化 -│ └── content_processor.py # 内容处理和清理 -└── README-zh.md # 模块使用文档 -``` - -### 2.2 集成的系统模块 - -``` -aperag/views/web.py # HTTP API视图(已实现) -aperag/mcp/server.py # MCP工具注册(待集成) -aperag/schema/view_models.py # 数据模型(已扩展) -``` - -## 3. API接口设计 - -### 3.1 HTTP API接口 - -**已实现的RESTful API**: - -```yaml -# OpenAPI规范片段 -/api/v1/web/search: - post: - summary: 执行Web搜索 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebSearchRequest' - responses: - '200': - description: 搜索成功 - content: - application/json: - schema: - $ref: '#/components/schemas/WebSearchResponse' - -/api/v1/web/read: - post: - summary: 读取Web页面内容 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WebReadRequest' - responses: - '200': - description: 读取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/WebReadResponse' -``` - -### 3.2 请求/响应数据模型 - -**WebSearchRequest**: -```python -class WebSearchRequest(BaseModel): - query: str # 搜索查询 - max_results: int = 5 # 最大结果数 - search_engine: str = "duckduckgo" # 搜索引擎 - timeout: int = 30 # 超时时间(秒) - locale: str = "zh-CN" # 语言地区 -``` - -**WebSearchResponse**: -```python -class WebSearchResponse(BaseModel): - query: str # 原始查询 - results: List[WebSearchResultItem] # 搜索结果列表 - search_engine: str # 使用的搜索引擎 - total_results: int # 结果总数 - search_time: float # 搜索耗时(秒) -``` - -**WebReadRequest**: -```python -class WebReadRequest(BaseModel): - urls: Union[str, List[str]] # 单个或多个URL - timeout: int = 30 # 超时时间(秒) - locale: str = "zh-CN" # 语言地区 - max_concurrent: int = 3 # 最大并发数(批量读取) -``` - -> **注意**:统一的接口设计确保了所有provider都使用相同的参数。JINA等高级provider的特有功能(如CSS选择器、SPA支持等)在provider内部使用合理的默认值自动处理。 - -**WebReadResponse**: -```python -class WebReadResponse(BaseModel): - results: List[WebReadResultItem] # 读取结果列表 - total_urls: int # 总URL数量 - successful: int # 成功数量 - failed: int # 失败数量 - processing_time: float # 处理耗时(秒) -``` - -## 4. 核心组件实现 - -### 4.1 SearchService实现特性 - -**参考架构**:`aperag/llm/embed/embedding_service.py` - -**实现特性**: -```python -class SearchService: - # 支持的Provider切换 - def __init__(self, provider_name: str = None, provider_config: Dict = None) - - # 异步搜索接口 - async def search(self, request: WebSearchRequest) -> WebSearchResponse - - # 简化搜索接口 - async def search_simple(self, query: str, **kwargs) -> List[WebSearchResultItem] - - # 资源管理 - async def close(self) - async def __aenter__(self) / __aexit__(self) # 上下文管理器支持 - - # 工厂方法 - @classmethod - def create_default(cls) -> "SearchService" - @classmethod - def create_with_provider(cls, provider_name: str, **config) -> "SearchService" -``` - -**资源安全使用**: -```python -# 推荐使用方式:上下文管理器 -async with SearchService() as search_service: - response = await search_service.search(request) - # 资源自动清理 - -# 或手动管理 -search_service = SearchService() -try: - response = await search_service.search(request) -finally: - await search_service.close() -``` - -### 4.2 ReaderService实现特性 - -**参考架构**:`aperag/llm/rerank/rerank_service.py` - -**实现特性**: -```python -class ReaderService: - # 支持单个和批量读取 - async def read(self, request: WebReadRequest) -> WebReadResponse - async def read_simple(self, url: str, **kwargs) -> WebReadResultItem - async def read_batch_simple(self, urls: List[str], **kwargs) -> List[WebReadResultItem] - - # 完整的资源管理 - async def close(self) - async def cleanup(self) # 别名 - async def __aenter__(self) / __aexit__(self) -``` - -**并发控制实现**: -```python -# 内部使用asyncio.Semaphore控制并发 -async def read_batch(self, urls: List[str], max_concurrent: int = 3): - semaphore = asyncio.Semaphore(max_concurrent) - - async def read_single(url: str): - async with semaphore: - return await self.read(url) - - results = await asyncio.gather(*[read_single(url) for url in urls]) -``` - -### 4.3 Provider接口规范 - -**BaseSearchProvider接口**: -```python -class BaseSearchProvider(ABC): - @abstractmethod - async def search(self, query: str, **kwargs) -> List[WebSearchResultItem] - - @abstractmethod - def get_supported_engines(self) -> List[str] - - def validate_search_engine(self, search_engine: str) -> bool - - async def close(self): # 资源清理 - pass -``` - -**BaseReaderProvider接口**: -```python -class BaseReaderProvider(ABC): - @abstractmethod - async def read(self, url: str, **kwargs) -> WebReadResultItem - - @abstractmethod - async def read_batch(self, urls: List[str], **kwargs) -> List[WebReadResultItem] - - def validate_url(self, url: str) -> bool - - async def close(self): # 资源清理 - pass -``` - -## 5. Provider实现详情 - -### 5.1 DuckDuckGoProvider(默认搜索) - -**实现特性**: -- ✅ 免费使用,无需API密钥 -- ✅ 基于`duckduckgo-search`库 -- ✅ 支持地区和语言定制 -- ✅ 异步包装(使用`loop.run_in_executor`) - -**配置示例**: -```python -# 无需配置,开箱即用 -service = SearchService() # 默认使用DuckDuckGo - -# 显式指定 -service = SearchService(provider_name="duckduckgo") -``` - -### 5.2 JinaSearchProvider - -**实现特性**: -- 🚀 专为LLM优化的搜索结果 -- 🔧 支持多搜索引擎后端(Google、Bing) -- 📊 提供引用信息和结构化输出 -- 🌐 基于JINA s.jina.ai API - -**配置示例**: -```python -service = SearchService( - provider_name="jina", - provider_config={"api_key": "jina_xxxxxxxxxxxxxxxx"} -) -``` - -### 5.3 TrafilaturaProvider(默认读取) - -**实现特性**: -- ⚡ 高性能本地处理,无需外部API -- 🎯 基于`trafilatura`库的准确正文提取 -- 📱 支持多种网页格式 -- 💰 完全免费 - -**配置示例**: -```python -# 无需配置,开箱即用 -service = ReaderService() # 默认使用Trafilatura -``` - -### 5.4 JinaReaderProvider - -**实现特性**: -- 🤖 LLM优化的内容提取 -- 📝 Markdown格式输出 -- 🎯 智能内容识别 -- 🌐 基于JINA r.jina.ai API - -**配置示例**: -```python -service = ReaderService( - provider_name="jina", - provider_config={"api_key": "jina_xxxxxxxxxxxxxxxx"} -) -``` - -## 6. 使用示例 - -### 6.1 基础搜索示例 - -```python -from aperag.domains.web_access.search.search_service import SearchService -from aperag.domains.web_access.schemas import WebSearchRequest - -async def basic_search(): - async with SearchService() as search_service: - request = WebSearchRequest( - query="ApeRAG RAG系统架构", - max_results=5, - search_engine="duckduckgo" - ) - - response = await search_service.search(request) - - for result in response.results: - print(f"标题: {result.title}") - print(f"URL: {result.url}") - print(f"摘要: {result.snippet}") - print(f"域名: {result.domain}") - print("---") -``` - -### 6.2 内容读取示例 - -```python -from aperag.domains.web_access.reader.reader_service import ReaderService -from aperag.domains.web_access.schemas import WebReadRequest - -async def basic_read(): - async with ReaderService() as reader_service: - # 单个URL读取 - request = WebReadRequest(urls="https://example.com/article") - response = await reader_service.read(request) - - result = response.results[0] - if result.status == "success": - print(f"标题: {result.title}") - print(f"内容长度: {result.word_count} 词") - print(f"内容预览: {result.content[:200]}...") -``` - -### 6.3 批量处理示例 - -```python -async def batch_processing(): - """搜索并批量读取内容的完整示例""" - - # 1. 执行搜索 - async with SearchService(provider_name="jina", - provider_config={"api_key": "your_key"}) as search_service: - search_request = WebSearchRequest( - query="人工智能最新发展", - max_results=5 - ) - search_response = await search_service.search(search_request) - urls = [result.url for result in search_response.results] - - # 2. 批量读取内容 - async with ReaderService() as reader_service: - read_request = WebReadRequest( - urls=urls, - max_concurrent=3, - timeout=30 - ) - read_response = await reader_service.read(read_request) - - # 3. 整合结果 - for i, search_result in enumerate(search_response.results): - read_result = read_response.results[i] - - print(f"\n=== {search_result.title} ===") - print(f"URL: {search_result.url}") - print(f"搜索摘要: {search_result.snippet}") - - if read_result.status == "success": - print(f"完整内容: {read_result.content[:300]}...") - print(f"字数: {read_result.word_count}") - else: - print(f"内容读取失败: {read_result.error}") -``` - -### 6.4 HTTP API使用示例 - -```bash -# 搜索API调用 -curl -X POST "http://localhost:8000/api/v1/web/search" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer your_token" \ - -d '{ - "query": "ApeRAG RAG系统", - "max_results": 5, - "search_engine": "duckduckgo" - }' - -# 读取API调用 -curl -X POST "http://localhost:8000/api/v1/web/read" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer your_token" \ - -d '{ - "urls": ["https://example.com/article1", "https://example.com/article2"], - "max_concurrent": 2, - "timeout": 30 - }' -``` - -## 7. 资源管理与安全 - -### 7.1 资源泄漏解决方案 - -**问题**:在异步环境中,未正确关闭的资源会导致`ResourceWarning: Unclosed `。 - -**解决方案**: -1. **所有Provider实现close()方法** -2. **Service层支持上下文管理器** -3. **HTTP视图层使用上下文管理器** - -**实现细节**: -```python -# 所有Provider基类都实现close() -async def close(self): - # 子类可重写进行资源清理 - pass - -# Service层上下文管理器 -async def __aenter__(self): - return self - -async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.close() - -# HTTP视图层安全使用 -async def web_search(request: WebSearchRequest): - async with SearchService() as search_service: - response = await search_service.search(request) - return response - # 资源自动清理,避免泄漏 -``` - -### 7.2 错误处理机制 - -**多层错误处理**: -```python -# Provider层:具体错误 -class SearchProviderError(Exception): - pass - -class ReaderProviderError(Exception): - pass - -# Service层:统一包装 -try: - results = await self.provider.search(...) -except SearchProviderError: - raise # 重新抛出Provider错误 -except Exception as e: - raise SearchProviderError(f"Search service error: {str(e)}") - -# HTTP视图层:用户友好的错误响应 -except SearchProviderError as e: - raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}") -``` - -## 8. 性能优化 - -### 8.1 并发控制 - -**批量处理优化**: -```python -# 使用Semaphore控制并发数 -semaphore = asyncio.Semaphore(max_concurrent) - -async def read_single(url: str): - async with semaphore: - return await self.provider.read(url) - -# 并发执行,自动限制并发数 -results = await asyncio.gather(*[read_single(url) for url in urls]) -``` - -### 8.2 超时控制 - -**多层超时保护**: -```python -# Provider层:网络请求超时 -async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session: - async with session.post(url, json=payload) as response: - # 自动超时保护 - -# Service层:整体操作超时 -response = await asyncio.wait_for( - self.provider.search(query), - timeout=request.timeout -) -``` - -## 9. 依赖管理 - -### 9.1 已集成依赖 - -```python -# 搜索相关 -duckduckgo-search>=6.0.0 # DuckDuckGo搜索 -aiohttp>=3.9.0 # 异步HTTP客户端 - -# 内容读取相关 -trafilatura>=1.12.0 # 内容提取 -markdownify>=0.11.0 # HTML转Markdown - -# 内容处理 -beautifulsoup4>=4.12.0 # HTML解析(可选) -lxml>=5.0.0 # 解析器(可选) -``` - -### 9.2 安装方法 - -```bash -# 通过项目Makefile安装 -make env-install - -# 或直接pip安装 -pip install duckduckgo-search trafilatura markdownify aiohttp -``` - -## 10. 配置管理 - -### 10.1 推荐配置方式 - -**代码配置(推荐)**: -```python -# 直接传递配置,最灵活 -search_config = { - "api_key": "your_jina_api_key", - "timeout": 30, - "max_retries": 3 -} - -service = SearchService(provider_name="jina", provider_config=search_config) -``` - -**环境变量配置(可选)**: -```bash -# .env文件 -JINA_API_KEY=jina_xxxxxxxxxxxxxxxx -WEB_SEARCH_TIMEOUT=30 -WEB_READ_MAX_CONCURRENT=3 -``` - -### 10.2 Provider选择策略 - -**智能降级**: -```python -class SmartWebService: - async def search_with_fallback(self, query: str): - # 主Provider:JINA(如果有API Key) - if self.has_jina_key(): - try: - async with SearchService("jina", {"api_key": self.jina_key}) as service: - return await service.search_simple(query) - except Exception as e: - logger.warning(f"JINA搜索失败,降级到DuckDuckGo: {e}") - - # 降级Provider:DuckDuckGo - async with SearchService("duckduckgo") as service: - return await service.search_simple(query) -``` - -## 11. 测试与监控 - -### 11.1 单元测试覆盖 - -``` -tests/unit_test/websearch/ -├── test_search_service.py # SearchService测试 -├── test_reader_service.py # ReaderService测试 -├── test_duckduckgo_provider.py # DuckDuckGo Provider测试 -├── test_jina_providers.py # JINA Providers测试 -└── test_web_views.py # HTTP接口测试 -``` - -### 11.2 性能监控 - -**关键指标**: -- 搜索响应时间 -- 内容读取成功率 -- 并发处理能力 -- 资源使用情况 - -**监控实现**: -```python -# 在Service层添加指标收集 -import time -import logging - -logger = logging.getLogger(__name__) - -async def search(self, request: WebSearchRequest): - start_time = time.time() - try: - result = await self.provider.search(...) - search_time = time.time() - start_time - - # 记录成功指标 - logger.info(f"搜索成功: query={request.query}, time={search_time:.2f}s, results={len(result)}") - return result - - except Exception as e: - # 记录失败指标 - logger.error(f"搜索失败: query={request.query}, error={str(e)}") - raise -``` - -## 12. 集成与部署 - -### 12.1 MCP工具集成(待完成) - -**计划集成的MCP工具**: -```python -# aperag/mcp/server.py 扩展 -@server.tool() -async def web_search(query: str, max_results: int = 5) -> dict: - """执行Web搜索""" - async with SearchService() as service: - request = WebSearchRequest(query=query, max_results=max_results) - response = await service.search(request) - return response.dict() - -@server.tool() -async def web_read(urls: List[str], max_concurrent: int = 3) -> dict: - """读取Web页面内容""" - async with ReaderService() as service: - request = WebReadRequest(urls=urls, max_concurrent=max_concurrent) - response = await service.read(request) - return response.dict() -``` - -### 12.2 生产环境配置 - -**Docker配置**: -```dockerfile -# Dockerfile中添加依赖 -RUN pip install duckduckgo-search trafilatura markdownify - -# 环境变量 -ENV WEB_SEARCH_PROVIDER=duckduckgo -ENV WEB_READ_PROVIDER=trafilatura -ENV WEB_REQUEST_TIMEOUT=30 -``` - -**Kubernetes配置**: -```yaml -# 通过ConfigMap管理配置 -apiVersion: v1 -kind: ConfigMap -metadata: - name: websearch-config -data: - JINA_API_KEY: "your_jina_api_key" - WEB_SEARCH_TIMEOUT: "30" - WEB_READ_MAX_CONCURRENT: "3" -``` - -## 13. 总结 - -### 13.1 实现成果 - -✅ **已完成功能**: -- 完整的Provider抽象架构 -- DuckDuckGo和JINA搜索Provider -- Trafilatura和JINA读取Provider -- HTTP API接口实现 -- 资源安全管理机制 -- 并发控制和错误处理 -- 完整的单元测试 - -✅ **技术优势**: -- **架构统一**:完全遵循ApeRAG现有LLM服务设计模式 -- **资源安全**:解决了异步资源泄漏问题,生产环境可靠 -- **性能优化**:内置并发控制、超时保护、智能降级 -- **易于扩展**:新增Provider只需实现标准接口 -- **开箱即用**:DuckDuckGo和Trafilatura无需配置即可使用 - -### 13.2 待完成集成 - -⏳ **计划中功能**: -- MCP工具注册和集成 -- 缓存机制实现 -- 监控指标完善 -- 更多Provider支持(Bing、Google等) - -### 13.3 最佳实践总结 - -1. **始终使用上下文管理器**:避免资源泄漏 -2. **合理设置并发数**:防止外部服务过载 -3. **实现智能降级**:提高服务可用性 -4. **监控关键指标**:确保服务质量 -5. **遵循统一接口**:便于Provider切换 - -这个Web搜索模块为ApeRAG Agent提供了强大的Web信息获取能力,架构设计完全符合系统整体设计理念,实现了生产级的稳定性和可扩展性。 \ No newline at end of file