diff --git a/yunke-server/app/api/routers/search.py b/yunke-server/app/api/routers/search.py index fcf6204..8e07ea3 100644 --- a/yunke-server/app/api/routers/search.py +++ b/yunke-server/app/api/routers/search.py @@ -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,