This commit is contained in:
Kagura 2026-04-25 00:43:08 +00:00 committed by GitHub
commit f2d64e0b61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 39 additions and 4 deletions

View file

@ -1308,9 +1308,10 @@ class SessionDB:
else: else:
matches = [dict(row) for row in cursor.fetchall()] matches = [dict(row) for row in cursor.fetchall()]
# LIKE fallback for CJK queries: FTS5 default tokenizer splits CJK # LIKE supplement for CJK queries: FTS5 unicode61 tokenizer drops
# characters individually, causing multi-character queries to fail. # many CJK characters, causing partial or empty results. Always run
if not matches and self._contains_cjk(query): # LIKE for CJK queries and merge with FTS5 results (dedup by id).
if self._contains_cjk(query):
raw_query = query.strip('"').strip() raw_query = query.strip('"').strip()
like_where = ["m.content LIKE ?"] like_where = ["m.content LIKE ?"]
like_params: list = [f"%{raw_query}%"] like_params: list = [f"%{raw_query}%"]
@ -1341,7 +1342,13 @@ class SessionDB:
like_params = [raw_query] + like_params like_params = [raw_query] + like_params
with self._lock: with self._lock:
like_cursor = self._conn.execute(like_sql, like_params) like_cursor = self._conn.execute(like_sql, like_params)
matches = [dict(row) for row in like_cursor.fetchall()] like_matches = [dict(row) for row in like_cursor.fetchall()]
# Merge: deduplicate by message id, LIKE results supplement FTS5
seen_ids = {m["id"] for m in matches}
for m in like_matches:
if m["id"] not in seen_ids:
matches.append(m)
seen_ids.add(m["id"])
# Add surrounding context (1 message before + after each match). # Add surrounding context (1 message before + after each match).
# Done outside the lock so we don't hold it across N sequential queries. # Done outside the lock so we don't hold it across N sequential queries.

View file

@ -503,6 +503,7 @@ AUTHOR_MAP = {
"codex@openai.invalid": "teknium1", "codex@openai.invalid": "teknium1",
"screenmachine@gmail.com": "teknium1", "screenmachine@gmail.com": "teknium1",
"chenzeshi@live.com": "chen1749144759", "chenzeshi@live.com": "chen1749144759",
"kagura.agent.ai@gmail.com": "kagura-agent",
} }

View file

@ -716,6 +716,33 @@ class TestCJKSearchFallback:
results = db.search_messages("Agent通信") results = db.search_messages("Agent通信")
assert len(results) == 1 assert len(results) == 1
def test_cjk_partial_fts5_results_supplemented_by_like(self, db):
"""When FTS5 returns *some* CJK results, LIKE must supplement them.
Regression test for #14829: FTS5 unicode61 tokenizer drops certain
CJK characters, so multi-character queries may return partial results.
The LIKE path must always run for CJK queries and merge results.
"""
db.create_session(session_id="s1", source="cli")
db.create_session(session_id="s2", source="telegram")
# Insert messages containing the same CJK substring.
# FTS5 may index one but not the other depending on tokenizer quirks.
db.append_message("s1", role="user", content="昨晚讨论了记忆系统")
db.append_message("s2", role="user", content="昨晚的会议纪要已发送")
results = db.search_messages("昨晚")
# Both messages contain "昨晚" — LIKE must find both even if FTS5
# only returns one (or zero). Dedup ensures no duplicates.
assert len(results) == 2
session_ids = {r["session_id"] for r in results}
assert session_ids == {"s1", "s2"}
def test_cjk_like_dedup_no_duplicates(self, db):
"""When FTS5 and LIKE both find the same message, no duplicates."""
db.create_session(session_id="s1", source="cli")
db.append_message("s1", role="user", content="测试去重逻辑")
results = db.search_messages("测试")
assert len(results) == 1
# ========================================================================= # =========================================================================
# Session search and listing # Session search and listing