# 设计文档 · 全市场标的名称模糊搜索

> 状态：设计中 → 待实现 · 目标版本 v0.40.0 · 网页版见 `/design-symbol-search`

## 背景与问题

对话调研（`/ask`）与工作台 ⌘K 搜索现在靠一张 ~50 条的内置静态表把「名称 → uid」映射死：

- 后端 `web-backend/investresearch_data/resolve.py` 的 `NAME_TO_UID`（cn_names 反查 + 精选 A 股 + 别名）；
- 前端 `web/app.html` 的 `NAME_UID_SEED`。

表外的标的（如「亚信科技」）一律查不到 → 诚实降级要求用代码。用户诉求：**所有美股 / 港股 / A 股名称都能查，没有的也能模糊搜。**

## 数据源现实（决定诚实边界）

| 市场 | 中文名 | 英文/拼音/代码 | 可达源 |
|---|---|---|---|
| A 股 | ✅ 全量 | ✅ | baostock `query_all_stock(day)` 返回全部代码 + 中文名（`code_name`） |
| 港股 / 美股 | ⚠️ 仅精选 | ✅ 全量 | Yahoo Finance 搜索端点（认英文/拼音/代码，**不认中文**）；中文名无海外可达的全量免费源 |

**结论**：A 股中文名可全量模糊搜；港/美股英文·拼音·代码可全量模糊搜 + 现有精选中文名，任意港/美股**中文名不保证**（数据源限制），用户可用代码兜底、精选表逐步扩充。

Yahoo 搜索端点实测（`query2.finance.yahoo.com/v1/finance/search?q=asiainfo`）：返回 `1675.HK ASIAINFO TECH`、`600519.SS`、`NVDA` 等，带交易所 + `quoteType`，可映射 uid。

## 方案：混合 · 实时为主 + 日缓存

### 后端（需 ECS rebuild）

1. **`investresearch_data/search.py`** — `async def search_symbols(query, *, limit=8, http_get=…, ashare_names=…) -> [{uid, name, market}]`：
   - 先查本地精选表（`resolve.py`：`normalize_ticker` + `NAME_TO_UID`，秒回精确）；
   - query 含中文 → A 股中文名索引（baostock，日缓存）子串模糊 + 精选 cn_names 反查；
   - query 为拉丁/数字 → Yahoo 搜索端点，`quoteType==EQUITY` 过滤，`symbol → uid`：`.HK`→`HK:`+补零 5 位、`.SS`→`SH:`、`.SZ`→`SZ:`、纯字母→`US:`；
   - **best-effort**：Yahoo/baostock 超时失败 → 只回本地、不抛（诚实降级）。`http_get` / `ashare_names` 依赖注入，离线可测。
2. **A 股名日缓存**：进程内 TTL(24h) 缓存 `{code_name → uid}`，baostock `query_all_stock` 拉一次（复用 `baostock_provider` 的登录/登出与前缀映射），过滤到股票。
3. **`GET /search?q=&limit=`**（`investresearch_api/app.py`，仿只读端点）→ `{query, results:[…]}`；加进 `observability.py` 的 `KNOWN_PATHS`。
4. **`/ask` 解析兜底**（`investresearch_agent/ask.py`）：实体经 `resolve_company` 未命中 → `search_symbols` 兜底。恰一条高置信 → 用之（答复明示选中公司名 + uid）；多条歧义/空 → 留 unresolved、降级**列候选**。守铁律：搜索结果是数据源事实（同报价），非 LLM 猜；歧义不硬选。

### 前端（随 CF Pages）

5. **`web/app.html` `onSearchInput`**：backendOn 时防抖调 `/search?q=`，与本地 `DB.searchIndex` 合并去重进下拉；点选 → `openCompanyByUid(uid, name)`。本地精选即时先出、Yahoo 结果异步补。
6. 对话调研无需改前端：走 `/ask`，后端加搜索兜底后「没有的也能搜」自动生效。

### 测试

- `test_search.py`（离线注入桩）：uid 映射（`1675.HK→HK:01675` / `600519.SS→SH:600519` / `NVDA→US:NVDA`）；中文 query 命中假 A 股名表；拉丁 query 解析桩 Yahoo JSON + EQUITY 过滤；网络失败回落不抛。
- `test_ask.py` 增：本地未命中但搜索命中 → 路由；歧义 → 降级列候选。

## 诚实边界（对用户明示）

- A 股中文名：全量可搜。
- 港/美股：英文·拼音·代码全量可搜 + 精选中文名。
- 港/美股任意中文名：不保证（无海外可达全量免费源），代码兜底。

## 验证（端到端）

1. `pytest -q`（test_search + test_ask + 全量绿）。
2. `GET /search?q=asiainfo` → 含 `HK:01675`；`?q=宁德` → `SZ:300750`。
3. Playwright：⌘K 输「亚信」「宁德」出候选、点选进对应公司页；对话调研问「亚信科技怎么样」→ 命中 `HK:01675`。
4. ECS rebuild 后：`curl "https://api.oaf.world/search?q=asiainfo"`、`POST /ask -d '{"question":"平安银行怎么样"}'`（未在精选表 → baostock 搜到 `SZ:000001`）。

## 交付

单个 feature PR → squash merge → ECS rebuild + 前端 CF Pages → 可选发版 v0.40.0（能力升级）。
