# Phase R.2：多索引架构

日期：2026-04-03
状态：已规划
依赖：R.1（Token 感知分块）
工作量：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 天** |
