summaryrefslogtreecommitdiff
path: root/docs/search-fix.md
blob: 7e2730476c6b655dc906e5ccbbde9f9bd8ed5992 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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.