fix: 第10轮搜索审计修复 — 缓存键PubMed标志、NOT OR语义、affiliation ILIKE、前端布尔OR切换和精确短语
- 后端: _search_cache_key和_facet_cache_key加入"pm": _is_pm(query)防止PubMed/纯文本路径缓存碰撞
- 后端: OR模式下neg_conds不再AND到所有条件,改用or_(*term_conditions)直接组合
- 后端: _field_condition("all")所有5条分支添加affiliation ILIKE通过jsonb_array_elements(authors)子查询
- 后端: AdvancedSearchRequest.query添加max_length=2000
- 前端: SearchView搜索栏添加AND/OR切换器和精确短语复选框
- 前端: exact_phrase参数从SearchView传递到API请求
- 前端: booleanOp和exactPhrase同步到URL并可从URL恢复
This commit is contained in:
@@ -49,7 +49,7 @@ async def test_rule(req: TestRuleRequest, pmid: int = Query(...), db: AsyncSessi
|
||||
# ─── 高级搜索 ───
|
||||
|
||||
class AdvancedSearchRequest(BaseModel):
|
||||
query: str = ""
|
||||
query: str = Field("", max_length=2000)
|
||||
field: str = "all"
|
||||
boolean: str = "and"
|
||||
exact_phrase: bool = False
|
||||
|
||||
@@ -55,8 +55,10 @@ class AdvancedSearchEngine:
|
||||
) -> str:
|
||||
"""归一化查询参数 → 确定性缓存 key(所有 list 排序后参与哈希)"""
|
||||
import hashlib, json
|
||||
from app.services.pubmed_query_parser import is_pubmed_syntax as _is_pm
|
||||
norm = {
|
||||
"q": query.strip().lower(),
|
||||
"pm": _is_pm(query),
|
||||
"f": field, "b": boolean, "ep": exact_phrase,
|
||||
"yf": year_from, "yt": year_to,
|
||||
"df": date_from, "dt": date_to,
|
||||
@@ -109,8 +111,10 @@ class AdvancedSearchEngine:
|
||||
与 _search_cache_key 的区别:不含 p/ps/s/cd/ci。
|
||||
"""
|
||||
import hashlib, json
|
||||
from app.services.pubmed_query_parser import is_pubmed_syntax as _is_pm
|
||||
norm = {
|
||||
"q": query.strip().lower(),
|
||||
"pm": _is_pm(query),
|
||||
"f": field, "b": boolean, "ep": exact_phrase,
|
||||
"yf": year_from, "yt": year_to,
|
||||
"df": date_from, "dt": date_to,
|
||||
@@ -1144,10 +1148,13 @@ class AdvancedSearchEngine:
|
||||
|
||||
# 将 term_conditions 加入 conditions
|
||||
if term_conditions:
|
||||
if pp.boolean_operator in ("or", "mixed"):
|
||||
if pp.boolean_operator == "or":
|
||||
# OR 模式:所有条件(含 NOT)OR 在一起
|
||||
conditions.append(or_(*term_conditions))
|
||||
elif pp.boolean_operator == "mixed":
|
||||
# mixed 模式下 NOT 项应独立 AND(PubMed: A OR B NOT C = (A OR B) AND NOT C)
|
||||
from sqlalchemy.sql.elements import UnaryExpression
|
||||
from sqlalchemy.sql import operators as _sa_ops
|
||||
# OR/mixed 模式下 NOT 项应独立 AND(PubMed: A OR B NOT C = (A OR B) AND NOT C)
|
||||
pos_conds = [c for c in term_conditions
|
||||
if not (isinstance(c, UnaryExpression) and c.modifier == _sa_ops.inv)]
|
||||
neg_conds = [c for c in term_conditions
|
||||
@@ -1382,7 +1389,11 @@ class AdvancedSearchEngine:
|
||||
pat = _pt()
|
||||
if exact and not _wildcard:
|
||||
# P4: 精确短语 → phraseto_tsquery(利用 GIN 索引,保留词序)
|
||||
return GlobalLiterature.search_tsv.op("@@")(func.phraseto_tsquery("english", term))
|
||||
return or_(
|
||||
GlobalLiterature.search_tsv.op("@@")(func.phraseto_tsquery("english", term)),
|
||||
text("EXISTS (SELECT 1 FROM jsonb_array_elements(global_literature.authors) AS _e "
|
||||
"WHERE _e->>'affiliation' ILIKE :aff_pat)").bindparams(aff_pat=pat),
|
||||
)
|
||||
if _wildcard:
|
||||
# wildcard → ILIKE 右截断(tsvector 不支持 *),多字段覆盖
|
||||
return or_(
|
||||
@@ -1393,6 +1404,8 @@ class AdvancedSearchEngine:
|
||||
GlobalLiterature.journal_iso.ilike(pat),
|
||||
cast(GlobalLiterature.pmid, String).ilike(pat),
|
||||
GlobalLiterature.doi.ilike(pat),
|
||||
text("EXISTS (SELECT 1 FROM jsonb_array_elements(global_literature.authors) AS _e "
|
||||
"WHERE _e->>'affiliation' ILIKE :aff_pat)").bindparams(aff_pat=pat),
|
||||
)
|
||||
like_val = f"%{_escaped}%"
|
||||
if "/" in term:
|
||||
@@ -1400,6 +1413,8 @@ class AdvancedSearchEngine:
|
||||
return or_(
|
||||
GlobalLiterature.doi.ilike(_escape_ilike(term)),
|
||||
GlobalLiterature.doi.ilike(like_val),
|
||||
text("EXISTS (SELECT 1 FROM jsonb_array_elements(global_literature.authors) AS _e "
|
||||
"WHERE _e->>'affiliation' ILIKE :aff_pat)").bindparams(aff_pat=pat),
|
||||
)
|
||||
return or_(
|
||||
GlobalLiterature.title.ilike(like_val),
|
||||
@@ -1408,6 +1423,8 @@ class AdvancedSearchEngine:
|
||||
GlobalLiterature.abstract.ilike(like_val),
|
||||
GlobalLiterature.author_names_text.ilike(like_val),
|
||||
GlobalLiterature.journal.ilike(like_val),
|
||||
text("EXISTS (SELECT 1 FROM jsonb_array_elements(global_literature.authors) AS _e "
|
||||
"WHERE _e->>'affiliation' ILIKE :aff_pat)").bindparams(aff_pat=pat),
|
||||
)
|
||||
# P7-D2: Chinese → ILIKE fallback (tsvector is English-only)
|
||||
if re.search(r'[一-鿿㐀-䶿豈-]', term):
|
||||
@@ -1416,6 +1433,8 @@ class AdvancedSearchEngine:
|
||||
GlobalLiterature.abstract.ilike(like_val),
|
||||
GlobalLiterature.author_names_text.ilike(like_val),
|
||||
GlobalLiterature.journal.ilike(like_val),
|
||||
text("EXISTS (SELECT 1 FROM jsonb_array_elements(global_literature.authors) AS _e "
|
||||
"WHERE _e->>'affiliation' ILIKE :aff_pat)").bindparams(aff_pat=pat),
|
||||
)
|
||||
# tsvector 索引主覆盖 title/abstract/author_names/chemicals/genes/mesh/keywords
|
||||
# journal/journal_iso/affiliation 不在 tsvector 中,以 ILIKE 兜底
|
||||
@@ -1423,6 +1442,8 @@ class AdvancedSearchEngine:
|
||||
GlobalLiterature.search_tsv.op("@@")(func.plainto_tsquery("english", term)),
|
||||
GlobalLiterature.journal.ilike(like_val),
|
||||
GlobalLiterature.journal_iso.ilike(like_val),
|
||||
text("EXISTS (SELECT 1 FROM jsonb_array_elements(global_literature.authors) AS _e "
|
||||
"WHERE _e->>'affiliation' ILIKE :aff_pat)").bindparams(aff_pat=pat),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user