summaryrefslogtreecommitdiff
path: root/docs/search-fix.md
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 08:27:41 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 08:27:41 +0800
commit088735b948d46896b8af30efcb0a2dc5d362b97f (patch)
tree0dcab0d5ebc65309267a42d7cda827e9fdd866e7 /docs/search-fix.md
parentbf6681eb29ac8b0c80ca17b2b5869f7de3da1198 (diff)
downloadstrategy-lab-088735b948d46896b8af30efcb0a2dc5d362b97f.tar.gz
docs(release): 全周期交接文档入库(含 ui-shadcn 迁移交付说明)
[变更性质] 纯文档提交,无运行时逻辑。 [文档内容] 补齐此前各轮未入库的交接/验收文档:backend-auth/backend-domain/ domain-authorization-user(认证与授权域)、etf-recovery-release- handoff(ETF 修复 + ops 演练定稿与生产部署命令)、recovery-* 系列、 frontend/parent-ui-findings(UI 迁移上下文)、worker/integration 等, 以及本轮 docs/ui-shadcn-handoff.md(shadcn-svelte 迁移交接,含 Chart.svelte 契约、runes $state 踩坑记录与 375/768/1440 验证证据)。 [更新方案] 按主题分文;每份文档只记录可复现的命令、验证结果与语义边界, 不导出密钥或生产敏感路径。 [影响范围] 文档渠道:后续 leader/client 审阅入口;与代码提交一一对应便于回溯。
Diffstat (limited to 'docs/search-fix.md')
-rw-r--r--docs/search-fix.md111
1 files changed, 111 insertions, 0 deletions
diff --git a/docs/search-fix.md b/docs/search-fix.md
new file mode 100644
index 0000000..7e27304
--- /dev/null
+++ b/docs/search-fix.md
@@ -0,0 +1,111 @@
+# Production instrument-search fix — handoff notes
+
+Date: 2026-09-17. Scope: production search stall on `/api/instruments?q=...`
+(0 bytes in ~20s). All work confined to this repo; nothing deployed, user DB
+untouched, no services restarted.
+
+## Root causes fixed
+
+1. **worker/data.py** — the old `_catalog_frame` fetched the ENTIRE A-share
+ stock catalog (`stock_info_a_code_name`) and the ENTIRE ETF live snapshot
+ (`fund_etf_spot_em`) via AKShare on every search query. Those calls are
+ unbounded and stalled inside the worker container until the Rust-side
+ runner timeout fired, so the browser's fetch starved and kept spinning.
+ Replaced with **bounded direct provider suggestion search**:
+ - Eastmoney `https://searchapi.eastmoney.com/api/suggest/get`
+ (`input=<q>`, `type=14`, bounded `count`), JSON, per-call timeout **4s**.
+ - Tencent `https://smartbox.gtimg.cn/s3/?q=<q>&t=all`, text hint
+ `v_hint="sh~600000~name~py~GP-A^..."`, per-call timeout **4s**.
+ - No synthetic data, no full catalogs anywhere in the search path anymore.
+2. **worker (main.py/data.py) envelope** — search now prints one JSON object
+ envelope `{"source","status","items","error","providers"}` with explicit
+ `status: ready|failed`. `run_search` exits non-zero on failure.
+3. **server/src/worker.rs** — container runtime bound reduced **120s → 12s**
+ (outer bound incl. container startup; provider calls are ≤2×4s inside).
+ The stdout is now parsed as the full JSON envelope; the old code extracted
+ `first[...]..last[...]` which silently turned a failed-status envelope into
+ an empty success. A `failed` status now surfaces as a real error
+ (`search_unavailable`, provider error code in the message), and **only
+ ready responses enter the server-side TTL cache**.
+4. **server/src/main.rs** — `instruments()` success source label is now
+ `provider_suggest`; failures keep returning HTTP 200 with
+ `status: unavailable: ...` so the existing UI contract is unchanged.
+5. **Frontend** (`DatasetWizard.svelte` + new `lib/instrumentSearch.ts`):
+ - 350 ms debounce on input, prior in-flight queries are **aborted**
+ (AbortController) and stale responses can never repaint.
+ - Hard **AbortController timer (12s)** bound; hung requests surface
+ 搜索超时,请重试 instead of an eternal spinner.
+ - Non-`ok` envelope status / provider failure shows
+ 搜索数据源暂时不可用,请稍后重试 with a **重试** button; the spinner is
+ always cleared (`onFinish` in `finally`).
+ - Result rows carry per-item **provenance** (东财 / 腾讯).
+
+## Asset-class rules (validated identity, never guessed from code shape)
+
+Live-probed provider classifications (2026-09-17, both endpoints):
+
+| Provider | Field | accepted |
+|-----------|------------------|-----------------------------|
+| Eastmoney | `Classify` | `AStock`→stock, `Index`→index, `Fund`→etf (cross-confirmed, see below) |
+| Eastmoney | `Classify` (excluded) | `Bond`,`HK`,`OTCFUND`, others |
+| Eastmoney | `MktNum` | `1`→SH, `0`→SZ (others dropped) |
+| Tencent | hint tag | `GP-A`→stock, `ETF`→etf, `ZS`→index; `LOF`/others excluded |
+| Tencent | hint market | `sh`/`sz` only (`bj` dropped) |
+
+- An asset's class comes only from the provider's own classification field —
+ a numeric code like `000300` is never inferred to be an index by shape
+ (test: `test_search_numeric_code_not_auto_index`).
+- **Verified live**: Eastmoney `Classify=Fund` is ambiguous — ETF `510300`
+ and LOF `160706` share the same `Classify` and `SecurityType`. Therefore an
+ Eastmoney *Fund* item is only emitted when Tencent presents the same code
+ with tag `ETF`. Tencent alone classifies ETF/LOF unambiguously. When
+ Tencent unavailable, Eastmoney-only fund suggestions are suppressed
+ (`test_search_etf_ambiguous_fund_excluded_without_tencent_confirmation`,
+ `test_search_lof_not_present_when_eastmoney_only`). Guaranteed: no LOF
+ ever surfaces as an ETF.
+- Per-item provenance is preserved: identical codes may appear once per
+ actual source (`source: eastmoney` / `source: tencent`).
+
+## Live verification (fresh container `strategy-lab-worker:local`, rebuilt bd9e9f06d56b)
+
+```
+docker run --rm --network=bridge strategy-lab-worker:local \
+ python -m worker.main search --query 600000 --limit 5
+→ {"status":"ready","items":[{"symbol":"600000","...":"浦发银行","asset_type":"stock",...}],...}
+ wall time ≈1.9s (container start included; previously 0 bytes / 20+s)
+
+q=510300 → etf SH#510300 (eastmoney+tencent)
+q=沪深300 → index SH#000300 / SZ#399300 (+ ETFs), providers ok
+q=160706 → {"items":[]} (LOF correctly suppressed, providers ok, status ready)
+```
+
+## Tests actually run and passing
+
+- `server`: `cargo test` → **54 passed**; `cargo build --release` OK.
+- `worker`: `.venv/bin/python -m pytest tests` (repo root) → **49 passed**,
+ of which new/updated in `tests/worker/test_data.py` (real shapes for
+ stock/ETF/index, Chinese query `浦发银行`/`沪深300`, unsupported classes
+ bond/HK/OTC/LOF, provider failure + `requests.Timeout` → explicit failed
+ status, partial-provider warning) and `tests/worker/test_fetch_cli.py::test_search_cli_envelope_contract`.
+- `frontend`: `npm run test` (vitest) → **45 passed** across 13 files,
+ including new `src/lib/instrumentSearch.test.ts` (success, unavailable
+ status → Chinese message, provider rejection, hard-timer abort → 超时
+ wording with spinner cleared, **stale-prior-query never repaints and never
+ hangs**, dispose invalidates); `npm run check` → 0 errors/warnings;
+ `npm run build` → OK.
+
+## Deployment state
+
+- Worker image rebuilt exactly as `strategy-lab-worker:local` (context repo
+ root, `-f worker/Dockerfile`): id `bd9e9f06d56b`.
+- Frontend `dist/` rebuilt from the fixed sources.
+- Server binary built release but **not deployed / no restarts performed**.
+- Rollback note: previous worker image tag remains on the host; server
+ changes are code-only (no config/DB changes).
+
+## Known limitation (documented honestly)
+
+When Tencent is unavailable and only Eastmoney responds, fund-class
+suggestions are suppressed (stock/index still return). This trades a
+slightly narrower ETF list for a hard guarantee that no LOF is presented
+as an ETF without provider confirmation.