|
@@ -143,6 +143,35 @@ class StructuredIndex:
|
|
|
ON relationships(char1, char2)
|
|
ON relationships(char1, char2)
|
|
|
""")
|
|
""")
|
|
|
|
|
|
|
|
|
|
+ # 4. 角色索引表(优化模糊搜索性能)
|
|
|
|
|
+ self.conn.execute("""
|
|
|
|
|
+ CREATE TABLE IF NOT EXISTS characters (
|
|
|
|
|
+ name TEXT PRIMARY KEY,
|
|
|
|
|
+ description TEXT,
|
|
|
|
|
+ personality TEXT,
|
|
|
|
|
+ importance TEXT, -- 'major' / 'minor'
|
|
|
|
|
+ power_level TEXT,
|
|
|
|
|
+ first_appearance INTEGER,
|
|
|
|
|
+ last_appearance INTEGER,
|
|
|
|
|
+ status TEXT DEFAULT 'active', -- 'active' / 'archived'
|
|
|
|
|
+ archived_at TEXT, -- ISO timestamp
|
|
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
+ )
|
|
|
|
|
+ """)
|
|
|
|
|
+
|
|
|
|
|
+ # 角色名索引(加速模糊搜索)
|
|
|
|
|
+ self.conn.execute("""
|
|
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_character_name
|
|
|
|
|
+ ON characters(name)
|
|
|
|
|
+ """)
|
|
|
|
|
+
|
|
|
|
|
+ # 状态索引
|
|
|
|
|
+ self.conn.execute("""
|
|
|
|
|
+ CREATE INDEX IF NOT EXISTS idx_character_status
|
|
|
|
|
+ ON characters(status)
|
|
|
|
|
+ """)
|
|
|
|
|
+
|
|
|
self.conn.commit()
|
|
self.conn.commit()
|
|
|
|
|
|
|
|
# ================== 核心功能 1:章节元数据索引 ==================
|
|
# ================== 核心功能 1:章节元数据索引 ==================
|
|
@@ -381,6 +410,74 @@ class StructuredIndex:
|
|
|
else:
|
|
else:
|
|
|
return 20 # 正常
|
|
return 20 # 正常
|
|
|
|
|
|
|
|
|
|
+ def sync_characters_from_state(self):
|
|
|
|
|
+ """从 state.json 同步角色数据到索引(优化模糊搜索性能)
|
|
|
|
|
+
|
|
|
|
|
+ 触发时机:
|
|
|
|
|
+ - update_state.py 更新角色后调用
|
|
|
|
|
+ - --rebuild-index 批量重建时调用
|
|
|
|
|
+ """
|
|
|
|
|
+ if not self.state_file.exists():
|
|
|
|
|
+ print("❌ state.json 不存在,跳过角色同步")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 读取 state.json
|
|
|
|
|
+ with open(self.state_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ state = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ characters = state.get('entities', {}).get('characters', [])
|
|
|
|
|
+
|
|
|
|
|
+ for char in characters:
|
|
|
|
|
+ self._index_character(char, status='active')
|
|
|
|
|
+
|
|
|
|
|
+ self.conn.commit()
|
|
|
|
|
+ print(f"✅ 角色索引已同步:{len(characters)} 个角色")
|
|
|
|
|
+
|
|
|
|
|
+ def _index_character(self, char: Dict, status: str = 'active'):
|
|
|
|
|
+ """为单个角色建立索引"""
|
|
|
|
|
+ self.conn.execute("""
|
|
|
|
|
+ INSERT OR REPLACE INTO characters
|
|
|
|
|
+ (name, description, personality, importance, power_level,
|
|
|
|
|
+ first_appearance, last_appearance, status, updated_at)
|
|
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
|
|
|
+ """, (
|
|
|
|
|
+ char.get('name', ''),
|
|
|
|
|
+ char.get('description', ''),
|
|
|
|
|
+ char.get('personality', ''),
|
|
|
|
|
+ char.get('importance', 'minor'),
|
|
|
|
|
+ char.get('power_level', ''),
|
|
|
|
|
+ char.get('first_appearance_chapter', 0),
|
|
|
|
|
+ char.get('last_appearance_chapter', 0),
|
|
|
|
|
+ status
|
|
|
|
|
+ ))
|
|
|
|
|
+
|
|
|
|
|
+ def mark_character_archived(self, name: str, archived_at: str = None):
|
|
|
|
|
+ """标记角色为已归档状态(Priority 2 修复)
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ name: 角色名
|
|
|
|
|
+ archived_at: 归档时间戳(ISO格式),默认当前时间
|
|
|
|
|
+ """
|
|
|
|
|
+ if archived_at is None:
|
|
|
|
|
+ from datetime import datetime
|
|
|
|
|
+ archived_at = datetime.now().isoformat()
|
|
|
|
|
+
|
|
|
|
|
+ self.conn.execute("""
|
|
|
|
|
+ UPDATE characters
|
|
|
|
|
+ SET status = 'archived', archived_at = ?, updated_at = CURRENT_TIMESTAMP
|
|
|
|
|
+ WHERE name = ?
|
|
|
|
|
+ """, (archived_at, name))
|
|
|
|
|
+ self.conn.commit()
|
|
|
|
|
+
|
|
|
|
|
+ def mark_character_active(self, name: str):
|
|
|
|
|
+ """恢复角色为活跃状态(与 mark_character_archived 对应)"""
|
|
|
|
|
+ self.conn.execute("""
|
|
|
|
|
+ UPDATE characters
|
|
|
|
|
+ SET status = 'active', archived_at = NULL, updated_at = CURRENT_TIMESTAMP
|
|
|
|
|
+ WHERE name = ?
|
|
|
|
|
+ """, (name,))
|
|
|
|
|
+ self.conn.commit()
|
|
|
|
|
+
|
|
|
def query_urgent_foreshadowing(self, threshold: int = 60) -> List[Dict]:
|
|
def query_urgent_foreshadowing(self, threshold: int = 60) -> List[Dict]:
|
|
|
"""查询紧急伏笔(urgency >= threshold)
|
|
"""查询紧急伏笔(urgency >= threshold)
|
|
|
|
|
|
|
@@ -402,49 +499,64 @@ class StructuredIndex:
|
|
|
# ================== 核心功能 3:模糊查询(Fuzzy Search via SQL LIKE)==================
|
|
# ================== 核心功能 3:模糊查询(Fuzzy Search via SQL LIKE)==================
|
|
|
|
|
|
|
|
def fuzzy_search_character(self, keywords: List[str]) -> List[Dict]:
|
|
def fuzzy_search_character(self, keywords: List[str]) -> List[Dict]:
|
|
|
- """模糊查询角色(支持多关键词)
|
|
|
|
|
|
|
+ """模糊查询角色(支持多关键词)- O(log n) SQL查询
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
keywords: 关键词列表,如 ["李", "女弟子"]
|
|
keywords: 关键词列表,如 ["李", "女弟子"]
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
- [{'name': '李雪', 'description': '...', 'last_appearance_chapter': 45}, ...]
|
|
|
|
|
|
|
+ [{'name': '李雪', 'description': '...', 'last_appearance_chapter': 45, 'status': 'active'}, ...]
|
|
|
|
|
|
|
|
示例:
|
|
示例:
|
|
|
fuzzy_search_character(["李", "女弟子"])
|
|
fuzzy_search_character(["李", "女弟子"])
|
|
|
→ 返回所有名字或描述包含"李"和"女弟子"的角色
|
|
→ 返回所有名字或描述包含"李"和"女弟子"的角色
|
|
|
|
|
+
|
|
|
|
|
+ 性能:
|
|
|
|
|
+ - 旧版:O(n) 遍历 state.json 所有角色(210个角色 = ~500ms)
|
|
|
|
|
+ - 新版:O(log n) SQL 索引查询(~10ms)
|
|
|
|
|
+ """
|
|
|
|
|
+ # 构建 WHERE 子句(每个关键词都必须匹配)
|
|
|
|
|
+ conditions = []
|
|
|
|
|
+ params = []
|
|
|
|
|
+
|
|
|
|
|
+ for kw in keywords:
|
|
|
|
|
+ # 每个关键词在 name/description/personality 任一字段中出现即可
|
|
|
|
|
+ conditions.append("(name LIKE ? OR description LIKE ? OR personality LIKE ?)")
|
|
|
|
|
+ params.extend([f'%{kw}%', f'%{kw}%', f'%{kw}%'])
|
|
|
|
|
+
|
|
|
|
|
+ # AND 连接所有关键词条件(所有关键词都必须匹配)
|
|
|
|
|
+ where_clause = " AND ".join(conditions)
|
|
|
|
|
+
|
|
|
|
|
+ # 执行 SQL 查询
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ SELECT name, description, personality, importance, power_level,
|
|
|
|
|
+ first_appearance, last_appearance, status
|
|
|
|
|
+ FROM characters
|
|
|
|
|
+ WHERE {where_clause}
|
|
|
|
|
+ ORDER BY
|
|
|
|
|
+ status ASC, -- 活跃角色优先
|
|
|
|
|
+ last_appearance DESC -- 最近出场优先
|
|
|
|
|
+ LIMIT 10
|
|
|
"""
|
|
"""
|
|
|
- if not self.state_file.exists():
|
|
|
|
|
- return []
|
|
|
|
|
|
|
|
|
|
- # 读取 state.json 中的角色数据
|
|
|
|
|
- with open(self.state_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
- state = json.load(f)
|
|
|
|
|
|
|
+ cursor = self.conn.execute(query, params)
|
|
|
|
|
+ rows = cursor.fetchall()
|
|
|
|
|
|
|
|
- characters = state.get('entities', {}).get('characters', [])
|
|
|
|
|
|
|
+ # 转换为字典列表
|
|
|
matched = []
|
|
matched = []
|
|
|
-
|
|
|
|
|
- for char in characters:
|
|
|
|
|
- # 检查所有关键词是否都匹配
|
|
|
|
|
- name = char.get('name', '')
|
|
|
|
|
- description = char.get('description', '')
|
|
|
|
|
- personality = char.get('personality', '')
|
|
|
|
|
-
|
|
|
|
|
- # 组合文本
|
|
|
|
|
- combined_text = f"{name} {description} {personality}"
|
|
|
|
|
-
|
|
|
|
|
- # 检查所有关键词是否都在 combined_text 中
|
|
|
|
|
- if all(keyword in combined_text for keyword in keywords):
|
|
|
|
|
- matched.append({
|
|
|
|
|
- 'name': name,
|
|
|
|
|
- 'description': description,
|
|
|
|
|
- 'last_appearance_chapter': char.get('last_appearance_chapter', 0)
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- # 按最后出场章节排序
|
|
|
|
|
- matched.sort(key=lambda x: x['last_appearance_chapter'], reverse=True)
|
|
|
|
|
-
|
|
|
|
|
- return matched[:10] # 最多返回 10 个
|
|
|
|
|
|
|
+ for row in rows:
|
|
|
|
|
+ matched.append({
|
|
|
|
|
+ 'name': row[0],
|
|
|
|
|
+ 'description': row[1],
|
|
|
|
|
+ 'personality': row[2],
|
|
|
|
|
+ 'importance': row[3],
|
|
|
|
|
+ 'power_level': row[4],
|
|
|
|
|
+ 'first_appearance_chapter': row[5],
|
|
|
|
|
+ 'last_appearance_chapter': row[6],
|
|
|
|
|
+ 'status': row[7] # 'active' / 'archived'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return matched
|
|
|
|
|
|
|
|
# ================== 批量操作 ==================
|
|
# ================== 批量操作 ==================
|
|
|
|
|
|