feat: 添加搜索功能的优先级选择、AI 开关和最低展示分数控制,优化用户体验
This commit is contained in:
104
app.py
104
app.py
@@ -30,6 +30,7 @@ from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask, Response, redirect, render_template, request, session, url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import or_, and_
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
@@ -339,6 +340,11 @@ SEARCH_FUZZY_PROFILES = {
|
||||
},
|
||||
}
|
||||
|
||||
# 搜索结果最低展示分数,低于该分数的匹配将被隐藏(可按需调整)
|
||||
SEARCH_MIN_DISPLAY_SCORE = 3.0
|
||||
# 每次搜索最多评分的候选元件数量,避免对整个库逐条打分导致页面刷新变慢
|
||||
SEARCH_MAX_CANDIDATES = 800
|
||||
|
||||
|
||||
def _apply_box_type_overrides() -> None:
|
||||
"""加载盒型覆盖配置。
|
||||
@@ -2332,6 +2338,10 @@ def _build_rule_based_search_plan(query: str) -> dict:
|
||||
if values:
|
||||
summary_bits.append(f"{field_labels[field]}: {' / '.join(values)}")
|
||||
|
||||
# 保证 field_map 四个字段都为 list,防止模板渲染出错
|
||||
for k in ("part_no", "name", "specification", "note"):
|
||||
if k not in field_map or not isinstance(field_map[k], list):
|
||||
field_map[k] = []
|
||||
return {
|
||||
"query": query,
|
||||
"mode": "rule",
|
||||
@@ -5179,6 +5189,12 @@ def lcsc_import_to_edit_slot(box_id: int, slot: int):
|
||||
def search_page():
|
||||
keyword = request.args.get("q", "").strip()
|
||||
fuzziness = _parse_search_fuzziness(request.args.get("fuzziness", "balanced"))
|
||||
# 从请求参数读取最小展示分数(allow override),否则使用全局常量
|
||||
min_score_raw = (request.args.get("min_score", "") or "").strip()
|
||||
try:
|
||||
min_display_score = float(min_score_raw) if min_score_raw else SEARCH_MIN_DISPLAY_SCORE
|
||||
except (TypeError, ValueError):
|
||||
min_display_score = SEARCH_MIN_DISPLAY_SCORE
|
||||
notice = request.args.get("notice", "").strip()
|
||||
error = request.args.get("error", "").strip()
|
||||
results = []
|
||||
@@ -5186,21 +5202,101 @@ def search_page():
|
||||
search_parse_notice = ""
|
||||
search_trace = None
|
||||
|
||||
# 解析是否启用 AI:支持通过 GET 参数 `use_ai=0/1` 控制,
|
||||
# 如果未提供则根据是否配置 AI API 自动决定(已配置则默认开启)。
|
||||
use_ai_raw = (request.args.get("use_ai", "") or "").strip().lower()
|
||||
if use_ai_raw in ("0", "false", "no", "off"):
|
||||
use_ai = False
|
||||
elif use_ai_raw in ("1", "true", "yes", "on"):
|
||||
use_ai = True
|
||||
else:
|
||||
tmp_settings = _get_ai_settings()
|
||||
use_ai = bool((tmp_settings.get("api_key") or "") and (tmp_settings.get("api_url") or "") and (tmp_settings.get("model") or ""))
|
||||
|
||||
if keyword:
|
||||
# 读取优先级控制:name(名称优先)或 part_no(料号优先)
|
||||
priority = (request.args.get("priority", "") or "").strip().lower() or "name"
|
||||
settings = _get_ai_settings()
|
||||
search_plan, search_parse_notice, search_trace = _build_search_plan(keyword, settings)
|
||||
if use_ai:
|
||||
search_plan, search_parse_notice, search_trace = _build_search_plan(keyword, settings)
|
||||
else:
|
||||
# 用户显式关闭 AI,直接使用规则兜底计划
|
||||
fallback = _build_rule_based_search_plan(keyword)
|
||||
search_plan = fallback
|
||||
search_parse_notice = ""
|
||||
search_trace = {
|
||||
"query": keyword,
|
||||
"used_ai": False,
|
||||
"used_fallback": True,
|
||||
"final_mode": "rule",
|
||||
"fallback_plan": fallback,
|
||||
"ai_raw": "",
|
||||
"ai_error": "",
|
||||
}
|
||||
|
||||
if search_trace is None:
|
||||
search_trace = {}
|
||||
search_trace["fuzziness"] = fuzziness
|
||||
search_trace["fuzziness_label"] = SEARCH_FUZZY_PROFILES.get(fuzziness, SEARCH_FUZZY_PROFILES["balanced"])["label"]
|
||||
enabled_components = Component.query.filter_by(is_enabled=True).order_by(Component.part_no.asc(), Component.name.asc()).all()
|
||||
search_trace["fuzziness_label"] = SEARCH_FUZZY_PROFILES.get(fuzziness, SEARCH_FUZZY_PROFILES["balanced"])['label']
|
||||
# 只拉取一定数量候选项以加快页面响应;如需全库扫描可调大 SEARCH_MAX_CANDIDATES
|
||||
# 非 AI 模式下使用 SQL 预筛选:对每个词生成 OR 子句(匹配 part_no/name/specification/note),
|
||||
# 并用 AND 将各词串联,要求每个词至少在某个字段中出现以增强精确度。
|
||||
if not use_ai:
|
||||
# 优先按字段顺序拉取候选:料号 > 名称 > 规格 > 备注
|
||||
terms = [t for t in _split_natural_language_terms(keyword) if t]
|
||||
candidates = []
|
||||
matched_ids = set()
|
||||
remaining = SEARCH_MAX_CANDIDATES
|
||||
|
||||
# 根据 priority 参数设置字段搜索顺序
|
||||
if priority == "part_no":
|
||||
fields_order = ("part_no", "name", "specification", "note")
|
||||
else:
|
||||
fields_order = ("name", "part_no", "specification", "note")
|
||||
for field in fields_order:
|
||||
if remaining <= 0:
|
||||
break
|
||||
if not terms:
|
||||
break
|
||||
like_clauses = [getattr(Component, field).ilike(f"%{term}%") for term in terms]
|
||||
if not like_clauses:
|
||||
continue
|
||||
q = Component.query.filter(or_(*like_clauses), Component.is_enabled == True)
|
||||
if matched_ids:
|
||||
q = q.filter(~Component.id.in_(matched_ids))
|
||||
rows = q.order_by(Component.part_no.asc(), Component.name.asc()).limit(remaining).all()
|
||||
for r in rows:
|
||||
candidates.append(r)
|
||||
matched_ids.add(r.id)
|
||||
remaining = SEARCH_MAX_CANDIDATES - len(candidates)
|
||||
|
||||
# 如果仍不足,补充其他已启用的组件(排除已选)
|
||||
if remaining > 0:
|
||||
q = Component.query.filter(Component.is_enabled == True)
|
||||
if matched_ids:
|
||||
q = q.filter(~Component.id.in_(matched_ids))
|
||||
rows = q.order_by(Component.part_no.asc(), Component.name.asc()).limit(remaining).all()
|
||||
for r in rows:
|
||||
candidates.append(r)
|
||||
|
||||
enabled_components = candidates
|
||||
else:
|
||||
enabled_components = (
|
||||
Component.query.filter_by(is_enabled=True)
|
||||
.order_by(Component.part_no.asc(), Component.name.asc())
|
||||
.limit(SEARCH_MAX_CANDIDATES)
|
||||
.all()
|
||||
)
|
||||
box_by_id = {box.id: box for box in Box.query.all()}
|
||||
|
||||
matched_rows = []
|
||||
for c in enabled_components:
|
||||
match_info = _search_component_match_info(c, search_plan, fuzziness=fuzziness)
|
||||
# 过滤极低置信度的匹配,避免界面显示大量无关条目
|
||||
if not match_info["is_match"]:
|
||||
continue
|
||||
if match_info.get("score", 0) < min_display_score:
|
||||
continue
|
||||
box = box_by_id.get(c.box_id)
|
||||
matched_rows.append(
|
||||
{
|
||||
@@ -5237,6 +5333,8 @@ def search_page():
|
||||
search_parse_notice=search_parse_notice,
|
||||
notice=notice,
|
||||
error=error,
|
||||
min_display_score=min_display_score,
|
||||
priority=priority if 'priority' in locals() else 'name',
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user