diff --git a/backend/app/api/v1/literature.py b/backend/app/api/v1/literature.py index b3b7f3d..3bfec9e 100644 --- a/backend/app/api/v1/literature.py +++ b/backend/app/api/v1/literature.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import PlainTextResponse from pydantic import BaseModel -from sqlalchemy import case, func, literal, or_, select, text +from sqlalchemy import case, exists, func, literal, or_, select, text from sqlalchemy.ext.asyncio import AsyncSession from app.core.permissions import get_current_user @@ -289,10 +289,11 @@ async def search_literature( ).limit(100) )).scalars().all() if _tag_matches: - _tag_lit_subq = select(GlobalLiteratureTag.literature_id).where( - GlobalLiteratureTag.tag_id.in_(list(_tag_matches)) + _tag_exist = exists().where( + GlobalLiteratureTag.literature_id == GlobalLiterature.id, + GlobalLiteratureTag.tag_id.in_(list(_tag_matches)), ) - search_cond = or_(search_cond, GlobalLiterature.id.in_(_tag_lit_subq)) + search_cond = or_(search_cond, _tag_exist) # COUNT:使用 LIMIT 10001 截断,避免大结果集的精确计数 # 结果集 ≤10000 时返回精确值,否则返回 10001+ MAX_EXACT = 10000 diff --git a/backend/app/services/query_expansion.py b/backend/app/services/query_expansion.py index f68571b..021f401 100644 --- a/backend/app/services/query_expansion.py +++ b/backend/app/services/query_expansion.py @@ -20,7 +20,7 @@ import logging import re from uuid import UUID -from sqlalchemy import or_ as _or_, select +from sqlalchemy import exists, or_ as _or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.literature import GlobalTag, GlobalTagTreeNumber, GlobalLiteratureTag, GlobalLiterature @@ -53,13 +53,11 @@ async def expand_atm(db: AsyncSession, query: str): if not expanded_ids: return None - # 不用 .distinct():IN (subquery) 语义本身就按值去重,显式 DISTINCT 只会让 PG - # 先物化+排序海量 literature_id('cancer' 这类词展开出 150+ 个 tag,命中的 - # literature_id 可达数百万),再对 id IN (百万集合) 扫描。去掉后可直接走 - # ix_glt_tag 索引半连接,year_counts 全量扫描和主查询都受益。 - return GlobalLiterature.id.in_( - select(GlobalLiteratureTag.literature_id) - .where(GlobalLiteratureTag.tag_id.in_(expanded_ids)) + # 不用 .distinct():EXISTS 关联子查询按 literature_id 探测 PK 索引(每外行一次 + # 索引查找),不走 ix_glt_tag 全量物化。 + return exists().where( + GlobalLiteratureTag.literature_id == GlobalLiterature.id, + GlobalLiteratureTag.tag_id.in_(expanded_ids), ) diff --git a/backend/app/services/search_engine.py b/backend/app/services/search_engine.py index b4aac32..85ca304 100644 --- a/backend/app/services/search_engine.py +++ b/backend/app/services/search_engine.py @@ -2190,19 +2190,20 @@ class AdvancedSearchEngine: return None uids = list(mesh_tag_ids) - # 不用 func.distinct:IN (subquery) 语义本身就按值去重,显式 DISTINCT 只让 - # PG 多一次物化排序;去掉后可直接走 ix_glt_tag 索引半连接(year_counts 等 - # 全量扫描场景更敏感)。 + # 不用 .distinct():EXISTS 关联子查询按 literature_id 探测 PK 索引(每外行一次 + # 索引查找),不走 ix_glt_tag 全量物化。IN (subquery) 在 OR 过滤器里会被 planner + # 反复重扫('cancer' 展开命中的 literature_id 达数十万,Materialize 循环数十次 + # 直接 15s 超时),EXISTS 关联每次外行只做一次 PK 探测。 if major_only: - subq = select(GlobalLiteratureTag.literature_id).where( + return exists().where( + GlobalLiteratureTag.literature_id == GlobalLiterature.id, GlobalLiteratureTag.tag_id.in_(uids), GlobalLiteratureTag.is_major == True, ) - else: - subq = select(GlobalLiteratureTag.literature_id).where( - GlobalLiteratureTag.tag_id.in_(uids), - ) - return GlobalLiterature.id.in_(subq) + return exists().where( + GlobalLiteratureTag.literature_id == GlobalLiterature.id, + GlobalLiteratureTag.tag_id.in_(uids), + ) @staticmethod def _best_match_order(tsq):