Files
JARVIS/development-doc/plan/rag-update/phase-r-2-multi-index.md

245 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase R.2:多索引架构
日期2026-04-03
状态:已规划
依赖R.1Token 感知分块)
工作量4 天
---
## 1. 本阶段目的
按知识类型/重要性分层,支持懒加载和 LRU 淘汰。
---
## 2. 核心任务
### Task R.2.1:设计 Collection 分离策略
**目标:** 按知识类型分离 ChromaDB Collection
**新增文件:** `backend/app/services/multi_index.py`
```python
class MultiIndexManager:
"""多索引管理器,按知识类型分离"""
INDEX_STRATEGIES = {
"default": {
"name": "user_{user_id}_default",
"description": "通用文档"
},
"important": {
"name": "user_{user_id}_important",
"description": "重要文档(1.2x加权)"
},
"code": {
"name": "user_{user_id}_code",
"description": "代码片段"
},
"meeting": {
"name": "user_{user_id}_meeting",
"description": "会议记录"
},
}
def get_collection(self, user_id: str, index_type: str = "default"):
name = self.INDEX_STRATEGIES[index_type]["name"].format(user_id=user_id)
return self.chroma_client.get_or_create_collection(name=name)
```
---
### Task R.2.2:实现懒加载 + LRU TTL
**目标:** 2小时 TTL访问时加载不访问不加载
```python
import time
from threading import Lock
class LazyIndexLoader:
"""懒加载索引,支持 TTL 淘汰"""
def __init__(self, ttl_seconds: int = 7200):
self._cache = {}
self._last_used = {}
self._lock = Lock()
self._ttl = ttl_seconds
def get_or_load(self, key: str, loader_fn) -> Any:
with self._lock:
if key in self._cache:
self._last_used[key] = time.time()
return self._cache[key]
value = loader_fn()
self._cache[key] = value
self._last_used[key] = time.time()
return value
def sweep(self):
"""清理过期索引"""
now = time.time()
expired = [
k for k, t in self._last_used.items()
if now - t > self._ttl
]
for k in expired:
del self._cache[k]
del self._last_used[k]
```
---
### Task R.2.3:实现重要性感知检索
**目标:** important 索引加权 1.2x
```python
async def retrieve_with_importance(
self,
query: str,
user_id: str,
top_k: int = 5,
) -> list[SearchResult]:
"""重要性感知检索,优先返回高重要性文档"""
# 1. 从 default 索引检索
default_results = await self.retrieve(query, user_id, top_k=top_k * 2)
# 2. 从 important 索引检索
important_results = await self.retrieve(
query, user_id,
collection_name=f"user_{user_id}_important",
top_k=top_k
)
# 3. 合并,重要文档加权
scored = []
for r in default_results:
scored.append((r.score * 0.8, r))
for r in important_results:
scored.append((r.score * 1.2, r)) # 重要文档 1.2x
scored.sort(key=lambda x: x[0], reverse=True)
return [r for _, r in scored[:top_k]]
```
---
## 3. 修改现有文件
### `backend/app/models/document.py`
增加 `importance` 字段:
```python
class Document(Base):
# ... existing fields ...
importance = Column(Float, default=0.5) # 0.0 ~ 1.0, >0.8 进入 important 索引
```
---
### `backend/app/services/knowledge_service.py`
集成多索引支持:
```python
from app.services.multi_index import MultiIndexManager, LazyIndexLoader
class KnowledgeService:
def __init__(self, ...):
# ... existing init
self.multi_index = MultiIndexManager(self.chroma_client)
self.lazy_loader = LazyIndexLoader(ttl_seconds=7200)
async def index_document(self, document_id: str, user_id: str, ...):
# 根据 importance 选择索引
doc = await self._get_document(document_id)
if doc.importance >= 0.8:
collection = self.multi_index.get_collection(user_id, "important")
else:
collection = self.multi_index.get_collection(user_id, "default")
# ... rest of indexing
```
---
## 4. 新增测试
**新增文件:** `backend/tests/services/test_multi_index.py`
```python
import pytest
from app.services.multi_index import MultiIndexManager, LazyIndexLoader
class TestMultiIndexManager:
def test_get_collection_creates_if_not_exists(self):
manager = MultiIndexManager(mock_chroma_client)
col = manager.get_collection("user123", "default")
assert col is not None
def test_collection_name_format(self):
manager = MultiIndexManager(mock_chroma_client)
name = manager.INDEX_STRATEGIES["important"]["name"].format(user_id="user123")
assert name == "user_user123_important"
class TestLazyIndexLoader:
def test_get_or_load_caches(self):
loader = LazyIndexLoader()
load_fn = lambda: {"data": "test"}
result1 = loader.get_or_load("key1", load_fn)
result2 = loader.get_or_load("key1", load_fn)
# 第二次调用应该返回缓存的结果,而不是重新加载
assert result1 is result2
def test_sweep_removes_expired(self):
loader = LazyIndexLoader(ttl_seconds=1)
loader.get_or_load("key1", lambda: "value1")
import time
time.sleep(1.1) # 等待过期
loader.sweep()
assert "key1" not in loader._cache
```
---
## 5. 验收标准
- [ ] 多 Collection 创建成功
- [ ] 懒加载索引生效(访问时加载,不访问不加载)
- [ ] TTL 淘汰机制工作2小时无访问自动卸载
- [ ] 重要性感知检索加权生效
- [ ] 单元测试覆盖率 > 80%
---
## 6. 变更文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `backend/app/services/multi_index.py` | 新增 | 多索引管理器 |
| `backend/app/services/knowledge_service.py` | 修改 | 集成多索引支持 |
| `backend/app/models/document.py` | 修改 | 增加 importance 字段 |
| `backend/tests/services/test_multi_index.py` | 新增 | 多索引单元测试 |
---
## 7. 工作量估算
| 任务 | 估算 |
|------|------|
| R.2.1 Collection 分离策略 | 1 天 |
| R.2.2 懒加载 + LRU | 1 天 |
| R.2.3 重要性感知检索 | 0.5 天 |
| 测试 + 调试 | 1.5 天 |
| **R.2 总计** | **4 天** |