feat: 高级检索式双向转换与登录用户收藏(#196) - #197
youngfish42 wants to merge 7 commits into
Conversation
Issue #196 part 2: logged-in users (OAuth provider:id or admin session) can persist named advanced-search DSL strings with a last-seen result count. Storage lives in a dedicated users.sqlite3 (WAL, lock-guarded writes, quota check inside the write lock) so the read-only, atomically rebuilt papers.sqlite3 is never touched. - UserStore service, lazily mounted on app.extensions["user_store"] - GET/POST /api/v1/saved_queries, PATCH/DELETE /api/v1/saved_queries/<id> behind require_user; unified ApiError envelope (401/403/404/400) - SavedQueryCreateIn/UpdateIn/Out Pydantic schemas; explicit null last_count clears the stored count, null name/dsl rejected as no-op - Settings: PAPERVAULT_USER_DB_PATH, PAPERVAULT_SAVED_QUERIES_MAX_PER_USER - tests/test_saved_queries.py: auth guard, CRUD, per-user isolation and quota, validation boundaries, last_count set/clear semantics
Issue #196 part 1 + favorites frontend: - queryDsl.ts: new parseDslToRows() — parses a text DSL back into builder rows (flat AND/OR/NOT chains of term/list/range leaves round-trip faithfully; nested groups / NEAR / leading NOT degrade to a single raw-text topic row with supported=false, semantics preserved). renderRow now also parens-wraps values with a leading/inner minus NOT. - AdvancedSearchView: copy button on the generated expression, a paste-and-parse import area, ?q= hand-off consumption (watched, cleared after apply), and a favorites drawer (save with initial result count, load into builder, copy, manual count refresh, rename, delete). Only the first AND-joined year row folds into the year-range inputs so NOT/ OR year clauses keep their semantics. - useAuth composable: shared module-level login state; MainNavBar now consumes it instead of fetching /auth/me itself. - savedQueries API wrapper + clipboard helper (async Clipboard API with textarea fallback); zh/en i18n for all new labels. - queryDsl.test.mjs: 12 new round-trip / degrade cases (33 total).
|
PR Title: feat: 高级检索式双向转换与登录用户收藏(#196) Commit: 整体评估本次变更为高级检索引入了「文本 DSL ⇄ 可视化构建器」互转(复制/导入)与「收藏检索式」功能,并配套新增后端 UserStore、收藏 API、schema 与配置项,前端新增共享登录态 composable、剪贴板工具与收藏抽屉,同时把首页与导航栏的登录态统一到 useAuth。整体方向清晰,后端按用户隔离存储、配额检查、前端解析回退等处理都比较细致,测试也覆盖了 CRUD、隔离、配额与 DSL 往返的主要路径。 总结总体实现质量较好,但收藏功能的「结果数」计算存在实质性缺陷:前端把 DSL 原文直接当作后端 q 参数,而后端只做子串匹配,导致每条收藏的命中数恒为 0,建议合并前修复;此外首页跳转高级搜索会顺带触发一次多余的首页搜索。 Findings (2)
Prompt To Fix All With AI |
…te watchers - HIGH: saved-query result counts no longer forward raw DSL as the backend `q` param (substring AND over literal tokens like `ts=a` always returned 0). New `dslToCoarseParams` in queryDsl.ts mirrors useHomeSearch's split: hoists q/author/conf/since/until via splitForBackend; when nothing is hoistable it collects the actual text-term leaves from the AST (skipping NOT subtrees and non-text fields) as a coarse q instead of scanning the whole corpus. - MEDIUM: navigating home -> /advanced?q=... fired HomeView's route.query.q watcher on the way out, launching a redundant search with the fullscreen loading mask. Both watchers (HomeView and AdvancedSearchView) now guard on route.path. - docs: AGENTS.md no longer claims the backend parses the DSL in `q`; documents splitForBackend / dslToCoarseParams as the client-side split. - tests: 7 new dslToCoarseParams cases (40 total in the DSL suite).
|
PR Title: feat: 高级检索式双向转换与登录用户收藏(#196) Commit: 整体评估本轮为针对上一轮评审的修复增量(提交 d569d7f):在 queryDsl.ts 新增 dslToCoarseParams,让收藏结果数改用粗粒度后端参数而非原始 DSL;并在 AdvancedSearchView / HomeView 的路由 watcher 中加入路径守卫,避免页面切换时误消费 outgoing query 与多余搜索。改动方向与上一轮结论一致,两条原发现基本得到处理,结构清晰。 总结修复总体有效:原始 DSL 不再被当作 q 直接下发(原恒为 0 的严重问题已消除),离开页面时的重复搜索与全屏 loading 也被拦住,路由路径校验与实际路由一致。剩余风险集中在结果数近似本身:对顶层 OR 表达式,dslToCoarseParams 把各文本词用 AND 拼成 q,得到的是命中数的下界而非注释所称上界,仍可能显示 0 或明显偏小,建议合并前一并修正或调整口径。 Findings (1)
Prompt To Fix All With AI |
|
感谢评审,两条意见均已确认属实并在 🟠 HIGH — 收藏结果数恒为 0 🟡 MEDIUM — 首页跳转高级搜索触发多余搜索与全屏 loading 顺带修正了 验证:DSL 套件 40/40 通过, |
Second-round review on PR #197: squeezing a top-level OR into one coarse q ANDs every text term, yielding a lower bound that collapses to 0 for the synonym-merge queries this app produces most (the doc comment's 'upper bound' claim was wrong). - queryDsl.ts: new dslToCountPlan — a top-level OR becomes one coarse param set per leg (field-qualified legs hoist their own author/conf/ year params; legs that narrow nothing are dropped). fetchResultCount counts legs concurrently and combines with Math.max: never 0 while any leg hits, never inflated by overlap the way a sum would be. - favorites UI count is now labelled as an approximation (约 {n} 篇 / ~{n} papers); refreshed messages unchanged. - tests: 5 new dslToCountPlan cases (45 total in the DSL suite); AGENTS.md convention updated accordingly.
|
PR Title: feat: 高级检索式双向转换与登录用户收藏(#196) Commit: 整体评估本轮增量(提交 7ddf409)承接上一轮结论,把收藏结果数的近似从「整表达式压成一组参数」改为 queryDsl.ts 新增的 dslToCountPlan:顶层 OR 拆成每条 leg 一组参数,AdvancedSearchView 逐腿计数后用 Math.max 合并,UI 文案改为「约/~」,并同步了 AGENTS.md 与单测。方向正确,上一轮的核心问题得到针对性处理,测试也覆盖了文本 OR、字段限定 OR 与空 leg 丢弃等主路径。 总结上一轮指出的「顶层 OR 被 AND 成单一 q、命中数恒为 0」已被修复,OR 查询不会再塌缩为 0。剩余风险集中在计数口径:仅剩非文本 leg 的顶层 OR 会退化成空参数并把全库总量当作结果数,以及各 leg 用 Math.max 合并在关键词集合不重叠时明显低估,建议一并收敛回退逻辑与合并口径。 Findings (2)
Prompt To Fix All With AI |
|
第二轮评审意见已确认并在 🟡 MEDIUM — 顶层 OR 计数被 AND 拼接(下界而非上界,可归零)
|
Third-round review on PR #197: - dslToCountPlan fell back to coarse params of the whole OR expression when fewer than two usable legs survived, yielding an empty param set for e.g. 'AU="Yang Liu" OR NOT survey' — the caller would then fire an unfiltered query and record the whole-corpus total as the saved count. A single surviving leg now becomes the single param set directly, and fetchResultCount maps an empty param set to null (count unknown) instead of an unfiltered request. - Math.max merging underestimated disjoint OR legs (different fields barely overlap). Legs now combine by shape: homogeneous bare text-term legs (synonyms, heavy overlap) use max; heterogeneous legs sum. Comments and AGENTS.md no longer claim max is universally close. - tests: 7 new/updated dslToCountPlan cases (47 total in the DSL suite).
|
PR Title: feat: 高级检索式双向转换与登录用户收藏(#196) Commit: 整体评估本轮增量(commit 6231e12)承接上一轮关于收藏检索式近似结果数的结论,把顶层 OR 的计数计划改为按形状合并:全文本 leg 用 Math.max,异字段 leg 求和,并让无可收窄参数的表达式返回「未知」而非全库总量;单 leg 情形直接复用该 leg 的参数。上一轮指出的两处问题得到针对性处理,改动与文档、单测保持一致,整体方向正确。 总结上一轮两个问题已修复:仅剩单个可用 leg 的顶层 OR 不再退化为空参数( Findings (1)
Prompt To Fix All With AI |
|
第三轮评审的两条意见均已确认并在 🟡 仅剩非文本 leg 的顶层 OR 退化为空参数(计数变全库总量)
🟡 Math.max 对不重叠的异构 OR 腿低估
新增/更新 7 个 补充说明:OR 计数在"后端无文本 OR"的约束下必然是近似值,当前的形态分治在同义词合并(本仓库最常见)与跨字段混合两类典型场景下分别贴近真实并集;该数字的用途是让用户感知收藏的检索式是否有新增文献,相对变化比绝对值更有意义。 |
Fourth-round review on PR #197: the plan-wide textOnly switch summed the heavily-overlapping synonym legs of a mixed plan like '(PY=2024 AND TS=llm) OR k1 OR k2', inflating the saved count several-fold. The merge decision now happens per leg: new pure combineLegCounts() groups legs carrying a text q (synonyms, heavy overlap -> max) apart from legs on other fields (author/venue/year, disjoint -> sum), and skips failed legs (null only when every leg failed, while a genuine zero count still counts). tests: 4 new combineLegCounts cases (51 total in the DSL suite).
|
PR Title: feat: 高级检索式双向转换与登录用户收藏(#196) Commit: 整体评估本轮增量(commit 317e687)承接上一轮关于收藏检索式近似结果数的结论,把顶层 OR 的合并判据从"整条计划的 textOnly 布尔开关"改为交由新增纯函数 combineLegCounts 按 leg 自身形状分组:携带文本 q 的 leg 用 Math.max 合并、其余字段 leg 求和,失败 leg 跳过、全部失败才返回 null。改动同步更新了 queryDsl.ts 与 AdvancedSearchView 的注释、清理了 CountPlan 上的 textOnly 字段、并补充了针对混合计划与失败 leg 的回归单测,范围收敛、方向正确。 总结上一轮指出的"混合字段顶层 OR 会把高度重叠的文本同义 leg 一并相加、结果数被放大"已针对性修复并有单测锁定: |
|
第四轮评审意见已确认并在 🟡 混合字段顶层 OR 把同义文本腿也相加(计数被放大) 修复(按建议把合并决策下沉到腿级别):
验证:新增 4 个 |
关联 Issue
Closes #196
功能 1:高级检索式「UI 行 ⇄ 纯文本」双向转换 + 复制
queryDsl.ts新增parseDslToRows():文本 DSL 解析回构建器行。单层 AND/OR/NOT 链、引号短语、SO=列表、PY=区间均可无损往返;嵌套分组 / NEAR / 前导 NOT 等无法行化的结构降级为单条原文行并提示(重编译后语义完全等价)。/#/advanced?q=...),高级搜索页自动解析填入,消费后清除 URL 参数。功能 2:登录用户收藏检索式(服务端持久化)
/api/v1/saved_queries(GET/POST/PATCH/DELETE):按 OAuthprovider:id(或 admin 会话)归属用户,统一错误信封(401/403/404/400),每用户配额 100 条(PAPERVAULT_SAVED_QUERIES_MAX_PER_USER)。users.sqlite3(PAPERVAULT_USER_DB_PATH可配置),不触碰只读且会原子重建的papers.sqlite3;配额检查与插入在同一写锁内完成。composables/useAuth.ts;中英双语 i18n 已补齐;AGENTS.md已同步。测试
tests/test_saved_queries.py9 项(401 / CRUD / 用户隔离 / 配额 / 校验边界 / last_count 置空);全量pytest -q仅余 2 个与本 PR 无关的既有 Windows 平台失败(已在干净 main 上复现确认)。node:testDSL 回归套件 33/33(新增 12 个往返/降级用例);type-check/lint:check零告警;生产构建通过。未覆盖
OAuth 登录的浏览器端到端冒烟依赖真实 GitHub/知乎凭据,未在本机走通;该路径由后端 session 注入测试与前端构建覆盖。