feat(search): 智能搜索建议 - 固化候选词表 (search_keywords + search_title_suggestions)
后端: - alembic 0009: 两张固化表 + GIN prefix_keys 索引 + articles trigger - /api/v1/search/suggestions: 混合 A(高频词 ts_stat) + B(真实标题) + 冷启动 fallback - worker 每日 03:00 + 启动时刷新 search_keywords - 顺便填 commit 11 TODO: articles.title_zh_tsv + GIN 索引(未来 FTS 基础) 前端: - NInput -> NAutoComplete + debounce 250ms - 选标题 -> 跳详情;选关键词 -> 填入 + 触发搜索 - AbortController 防 race condition 性能: prefix_keys @> ARRAY[prefix] 走 GIN 亚毫秒,100w 行也稳
This commit is contained in:
@@ -7,13 +7,13 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from app.config import settings
|
||||
from app.database import AsyncSessionLocal
|
||||
@@ -28,6 +28,22 @@ logging.basicConfig(
|
||||
)
|
||||
|
||||
|
||||
async def _refresh_search_keywords() -> None:
|
||||
"""每日刷新 search_keywords(ts_stat 词频表)。
|
||||
|
||||
- 调用 PG 函数 refresh_search_keywords()(迁移 0009 创建)
|
||||
- 全量 truncate + insert,词频会变,不适合增量
|
||||
- 失败也不应阻塞 worker,只记 log
|
||||
"""
|
||||
try:
|
||||
async with AsyncSessionLocal() as s:
|
||||
await s.execute(text("SELECT refresh_search_keywords()"))
|
||||
await s.commit()
|
||||
logger.info("search_keywords refreshed")
|
||||
except Exception as e:
|
||||
logger.exception("search_keywords refresh failed: %s", e)
|
||||
|
||||
|
||||
async def _rebuild_jobs(scheduler: AsyncIOScheduler) -> None:
|
||||
"""从 sources 表动态构建 job(可热更新)。
|
||||
|
||||
@@ -95,6 +111,23 @@ async def main() -> None:
|
||||
id="startup_run",
|
||||
)
|
||||
|
||||
# === 搜索建议相关 ===
|
||||
# 每日凌晨 03:00 刷新 search_keywords(ts_stat 词频)
|
||||
scheduler.add_job(
|
||||
_refresh_search_keywords,
|
||||
trigger=CronTrigger(hour=3, minute=0),
|
||||
id="refresh_search_keywords",
|
||||
replace_existing=True,
|
||||
)
|
||||
# 启动时延迟 10 秒跑一次(冷启动友好,worker 起来时 search_keywords 就有数据;
|
||||
# 延迟是等 DB 完全就绪 + 不和 startup_run 抢资源)
|
||||
scheduler.add_job(
|
||||
_refresh_search_keywords,
|
||||
trigger=DateTrigger(run_date=datetime.now() + timedelta(seconds=10)),
|
||||
id="startup_refresh_search_keywords",
|
||||
)
|
||||
logger.info("scheduled: refresh_search_keywords daily 03:00 + on startup (+10s)")
|
||||
|
||||
scheduler.start()
|
||||
logger.info("scheduler started with %d jobs", len(scheduler.get_jobs()))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user