feat(search): MySQL ngram FULLTEXT分支(LIKE 13.5s→索引毫秒级;索引缺失自动回退LIKE)
MySQL LIKE 全扫698k行13.5s撞axios 15s超时→前端ERR_ABORTED; MATCH...AGAINST(ngram)毫秒级;本地C盘不足索引待服务器建 (索引SQL: CREATE FULLTEXT INDEX ft_msg_text ON message(text) WITH PARSER ngram) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,12 @@ async def _search_messages(
|
||||
"""
|
||||
from app.db.fts import fts_available
|
||||
|
||||
if session.bind is not None and session.bind.dialect.name == "mysql":
|
||||
# MySQL: ngram FULLTEXT 索引(LIKE 全扫 698k 行要 13.5s,2026-08-22 实测
|
||||
# 撞 axios 15s 超时前端 ERR_ABORTED);索引不存在时自动回退 LIKE
|
||||
return await _search_messages_mysql(
|
||||
session, q, page, page_size, channel, start, end
|
||||
)
|
||||
use_fts = len(q) >= 3 and await fts_available(session)
|
||||
if use_fts:
|
||||
return await _search_messages_fts(session, q, page, page_size, channel, start, end)
|
||||
@@ -132,6 +138,60 @@ async def _search_messages_fts(
|
||||
return Page(items=items, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
async def _search_messages_mysql(
|
||||
session: AsyncSession,
|
||||
q: str,
|
||||
page: int,
|
||||
page_size: int,
|
||||
channel: str | None,
|
||||
start: int | None,
|
||||
end: int | None,
|
||||
) -> Page[MessageOut]:
|
||||
"""MySQL ngram FULLTEXT MATCH 路径;索引缺失自动回退 LIKE。"""
|
||||
conds = ["MATCH(m.text) AGAINST(:ftq IN NATURAL LANGUAGE MODE)"]
|
||||
params: dict[str, Any] = {"ftq": q}
|
||||
if channel is not None:
|
||||
conds.append("m.channel = :ch")
|
||||
params["ch"] = channel
|
||||
if start is not None:
|
||||
conds.append("m.timestamp >= :s")
|
||||
params["s"] = start
|
||||
if end is not None:
|
||||
conds.append("m.timestamp <= :e")
|
||||
params["e"] = end
|
||||
where = " AND ".join(conds)
|
||||
|
||||
try:
|
||||
total = (
|
||||
await session.execute(
|
||||
sa.text(f"SELECT count(*) FROM message m WHERE {where}"), params
|
||||
)
|
||||
).scalar_one()
|
||||
id_rows = (
|
||||
await session.execute(
|
||||
sa.text(
|
||||
f"SELECT m.msg_id FROM message m WHERE {where} "
|
||||
f"ORDER BY m.timestamp DESC, m.msg_id ASC LIMIT :lim OFFSET :off"
|
||||
),
|
||||
{**params, "lim": page_size, "off": (page - 1) * page_size},
|
||||
)
|
||||
).scalars().all()
|
||||
except Exception:
|
||||
# 索引不存在(部署早期) → 回退 LIKE
|
||||
return await _search_messages_like(session, q, page, page_size, channel, start, end)
|
||||
|
||||
items: list[MessageOut] = []
|
||||
if id_rows:
|
||||
rows = (
|
||||
await session.execute(
|
||||
sa.select(Message).where(Message.msg_id.in_(list(id_rows)))
|
||||
)
|
||||
).scalars().all()
|
||||
by_id = {r.msg_id: r for r in rows}
|
||||
items = [MessageOut.model_validate(by_id[mid]) for mid in id_rows if mid in by_id]
|
||||
return Page(items=items, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
async def _search_messages_like(
|
||||
session: AsyncSession,
|
||||
q: str,
|
||||
|
||||
Reference in New Issue
Block a user