
## 1）BI 怎么设计（对标 Shopify/独立站“搜索/推荐”常见报表）
Shopify 的 Search & Discovery/行为报表里，搜索常看的就是 **query、no results、no clicks、click rate、purchase rate**（会话口径漏斗）。推荐系统的看板更强调 **曝光→点击→加购→购买→收入**，并按**推荐位/算法/实验**拆解。

### 1.1 搜索 BI（核心看板）
- **搜索总览（KPI + 漏斗）**
  - **搜索 PV/UV**：search request 次数 / 搜索用户数
  - **Click rate（会话口径，Shopify 常用）**：有点击的搜索会话 / 搜索会话
  - **ATC rate**：有加购的搜索会话 / 搜索会话
  - **Purchase rate（会话口径，Shopify 常用）**：有购买的搜索会话 / 搜索会话
  - **结果曝光 CTR（结果口径）**：点击次数 / 结果曝光次数（更适合诊断排序/样式）
  - **延迟&稳定性**：P50/P95 搜索耗时、错误率、空响应率

- **Query 分析（运营/选品/词库最常用）**
  - **高热搜索词**：按搜索 UV、曝光 UV、点击 UV、购买 UV 排序
  - **飙升词/新词**：与过去 \(7/14/28\) 天对比的增速
  - **无结果 query**（no results）
  - **有结果但无点击 query**（no clicks）——强信号：相关性差/图片价位不吸引/首屏无货等
  - **建议词（suggestion）**：suggest 点击发起的搜索占比、suggest→点击/购买转化

- **结果质量诊断（给算法/工程用）**
  - **点击位置分布**：position=1/2/3… 的点击占比（反推排序质量）
  - **类目/品牌/价格带分布**：哪些 query 对哪些类目贡献转化
  - **筛选/排序使用率**：filter 使用率、排序切换率、filter 后的 CTR/CVR 变化

- **用户与渠道拆解（增长/产品用）**
  - **新/老客**、地域、语言、设备、来源（自然/广告/社媒）
  - **页面维度**：首页搜索框 / 搜索页 / 分类页搜索 / 详情页内搜索等

- **Session/链路回放（你提到的 track_id/trace_id）**
  - 单用户或单 trace：**搜索→换词→曝光→点击→详情→加购→支付**
  - 支持按 **trace_id** 串联一次“搜索请求”与后续行为（你们文档也强调这一点）

> 参考：Shopify 官方对搜索分析的核心指标包括 click rate、purchase rate、queries、no results、no clicks（见 [Shopify Search & Discovery analytics](https://help.shopify.com/en/manual/online-store/storefront-search/search-and-discovery-analytics)）。

### 1.2 推荐 BI（核心看板）
- **推荐位总览（按 placement/module）**
  - **Impressions/Clicks/CTR**
  - **ATC、Purchase、Revenue**
  - **Revenue per impression / per click**
  - **AOV（推荐归因订单）**
  - **覆盖与多样性**：覆盖商品数、长尾曝光占比、重复率（避免“只推爆款”）

- **按算法/实验拆解**
  - 算法版本（model_version/strategy_id）
  - 实验（experiment_id/variant_id）
  - **Lift（增量）**：相对对照组的 CTR/CVR/Revenue uplift（最好有 holdout）

- **健康度与风控**
  - 缺货/下架命中率、被过滤原因分布（无库存/不可售/地域不发货）
  - 冷启动占比（新用户/新商品）与表现

---

## 2）数据层面的设计（埋点→数仓→指标→推荐特征）
你们已有统一事件骨架的方向（`tenant_id / session_id / trace_id / event(oneof)`）。建议在数据层分成 **ODS（原始）→DWD（清洗明细）→DWS（汇总）→ADS（看板语义层）**，同时给推荐做 **离线+在线特征库**。

### 2.1 关键 ID 设计（决定 BI/推荐能否串起来）
- **tenant_id**：多租户隔离必备（所有表分区/主键都带）
- **user_key**：归一后的用户主键（优先 login/user_id；否则 anonymous_id/cookie_id）
- **session_id**：会话（SDK 产生或用 30min inactivity 规则补齐）
- **trace_id（最重要）**：一次搜索/一次推荐刷新生成一个，后续点击/详情/加购尽量继承
- **request_id**：后端请求日志关联（用于延迟、错误、召回/排序 debug）
- **order_id/cart_id/item_id(sku/spu)**：交易与商品归因

### 2.2 ODS：原始事件（“可回放、可重放”）
- `ods_ua_event_raw`
  - tenant_id, event_time, received_time
  - identities（原始字段：distinct_id/login_id/anonymous_id/cookie_id…）
  - page/device/geo/referrer
  - trace_id, session_id, experiment 信息
  - event_name + properties（原始 JSON，保留全量）

> 你现在的 `$pageview/$WebClick` 自动采集能覆盖点击和页面，但 **推荐/搜索“曝光”必须补埋点**（否则 CTR、归因、训练样本都会缺）。

### 2.3 DWD：清洗后的事实表（BI/训练统一口径）
建议把“强分析对象”拆成事实表（便于 join、去重、做漏斗）：

- **搜索域**
  - `dwd_search_request`：一次“搜索结果刷新/请求”的主表（强烈建议：**每次请求一个 request_id，同时每次请求一个 trace_id**）
    - **用途**：search PV/UV、零结果、延迟、query 聚合、会话口径漏斗分母（search_session）
    - **主键建议**：`(tenant_id, request_id)`
    - **核心索引建议**：
      - `(tenant_id, created_at)`
      - `(tenant_id, user_key, created_at)`
      - `(tenant_id, query_normalized, created_at)`
      - `(tenant_id, trace_id)`（用于 join 曝光/点击）
      - `(tenant_id, search_session_id, created_at)`
    - **字段建议（MySQL 参考类型，可按你们存储调整）**
      - `tenant_id` VARCHAR(64) NOT NULL：店铺/租户
      - `request_id` VARCHAR(64) NOT NULL：该次搜索请求唯一 ID（前端生成或后端返回）
      - `trace_id` VARCHAR(64) NOT NULL：该次搜索链路 ID（一次结果集 = 一个 trace）
      - `session_id` VARCHAR(64) NULL：访问会话（30min inactivity）
      - `search_session_id` VARCHAR(64) NULL：搜索会话（一次“找东西”的连续过程）
      - `user_id` VARCHAR(64) NULL：登录用户 ID
      - `anonymous_id` VARCHAR(64) NULL：匿名用户（cookie 级稳定）
      - `user_key` VARCHAR(64) NOT NULL：归一用户主键（ETL 生成）
      - `query` TEXT NOT NULL：原始 query
      - `query_normalized` VARCHAR(512) NOT NULL：归一化 query（聚合用）
      - `is_suggestion` TINYINT NOT NULL DEFAULT 0：是否由 suggestion 触发
      - `suggestion_text` VARCHAR(512) NULL：命中的 suggestion（如有）
      - `page_number` INT NOT NULL DEFAULT 1：翻页页码（1-based）
      - `sort` VARCHAR(64) NULL：排序方式（如 relevance/price_asc/…）
      - `filters_json` JSON NULL：结构化筛选条件（原子字段更好；JSON 为起步方案）
      - `filters_hash` CHAR(32) NULL：filters 归一化后的 hash（便于聚合/去重）
      - `results_count` INT NOT NULL DEFAULT 0：总命中数
      - `returned_count` INT NULL：本页返回数量（page size）
      - `latency_ms` INT NULL：搜索耗时
      - `is_zero_result` TINYINT NOT NULL DEFAULT 0：是否零结果（可由 results_count=0 派生）
      - `is_error` TINYINT NOT NULL DEFAULT 0：是否错误
      - `error_code` VARCHAR(64) NULL：错误码（如超时/限流等）
      - `page_type` VARCHAR(64) NULL：发生搜索的页面（home/search/pdp/collection…）
      - `referrer` TEXT NULL：来源页
      - `device_type` VARCHAR(32) NULL：pc/mobile（可从 UA 解析）
      - `country` VARCHAR(8) NULL：国家（建议由 IP->Geo 派生；避免落全量 IP）
      - `created_at` DATETIME(3) NOT NULL：事件时间（毫秒级可选）

  - `dwd_search_impression_item`：搜索结果“曝光明细”（**必须补**，否则无法算 position CTR、训练负样本）
    - **用途**：曝光 PV/UV、位置 CTR、召回/排序诊断、训练样本（曝光未点为负样本）
    - **主键建议**：`(tenant_id, trace_id, item_id, position)`（或 `(tenant_id, request_id, item_id, position)`，二选一但全局统一）
    - **核心索引建议**：
      - `(tenant_id, trace_id)`
      - `(tenant_id, request_id)`
      - `(tenant_id, item_id, exposed_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `request_id` VARCHAR(64) NOT NULL
      - `trace_id` VARCHAR(64) NOT NULL
      - `search_session_id` VARCHAR(64) NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `query_normalized` VARCHAR(512) NOT NULL
      - `item_id` VARCHAR(64) NOT NULL：spu 或 sku，需统一
      - `position` INT NOT NULL：在搜索结果中的排名（1-based）
      - `score` DOUBLE NULL：排序分（如 ES score/learning-to-rank score）
      - `price` DECIMAL(18,2) NULL：曝光时价格快照（可选但强烈建议）
      - `currency` VARCHAR(8) NULL
      - `in_stock` TINYINT NULL：曝光时库存可售快照（可选但强烈建议）
      - `exposed_at` DATETIME(3) NOT NULL：曝光时间

  - `dwd_search_click_item`：搜索结果点击明细（点击必须能回链到曝光/请求）
    - **用途**：click UV、CTR（曝光口径/会话口径）、位置点击分布、归因触点
    - **主键建议**：`(tenant_id, click_id)`（若无 click_id，可用 `(tenant_id, trace_id, item_id, clicked_at)` 近似）
    - **核心索引建议**：
      - `(tenant_id, trace_id)`
      - `(tenant_id, request_id)`
      - `(tenant_id, item_id, clicked_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `click_id` VARCHAR(64) NULL：点击事件唯一 ID（推荐补）
      - `request_id` VARCHAR(64) NOT NULL
      - `trace_id` VARCHAR(64) NOT NULL
      - `search_session_id` VARCHAR(64) NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `query_normalized` VARCHAR(512) NOT NULL
      - `item_id` VARCHAR(64) NOT NULL
      - `position` INT NULL：点击时该商品所在排名（无法取到则置空）
      - `clicked_at` DATETIME(3) NOT NULL
      - `target_url` TEXT NULL：点击跳转 URL（可选）

- **推荐域**
  - `dwd_rec_request`：一次推荐请求/刷新主表（一次刷新 = 一个 trace_id）
    - **用途**：推荐 PV/UV、分推荐位指标、实验/版本拆解、延迟与稳定性
    - **主键建议**：`(tenant_id, request_id)`
    - **核心索引建议**：
      - `(tenant_id, created_at)`
      - `(tenant_id, placement, created_at)`
      - `(tenant_id, trace_id)`
      - `(tenant_id, algo_id, model_version, created_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `request_id` VARCHAR(64) NOT NULL
      - `trace_id` VARCHAR(64) NOT NULL
      - `session_id` VARCHAR(64) NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `placement` VARCHAR(64) NOT NULL：推荐位（home/pdp/cart/checkout/search…）
      - `module_id` VARCHAR(128) NULL：页面内模块标识（一个页面多个推荐模块）
      - `trigger_item_id` VARCHAR(64) NULL：触发商品（PDP/Cart 场景常用）
      - `candidates_count` INT NULL：候选数量
      - `returned_count` INT NULL：返回条数
      - `algo_id` VARCHAR(64) NULL：策略/算法标识（rule/i2i/embedding/…）
      - `model_version` VARCHAR(64) NULL：模型版本
      - `experiment_id` VARCHAR(64) NULL：实验 ID（如有）
      - `variant_id` VARCHAR(64) NULL：分桶/分组（如有）
      - `latency_ms` INT NULL
      - `is_error` TINYINT NOT NULL DEFAULT 0
      - `error_code` VARCHAR(64) NULL
      - `created_at` DATETIME(3) NOT NULL

  - `dwd_rec_impression_item`：推荐曝光明细
    - **主键建议**：`(tenant_id, trace_id, item_id, position)`
    - **核心索引建议**：`(tenant_id, trace_id)`、`(tenant_id, item_id, exposed_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `request_id` VARCHAR(64) NOT NULL
      - `trace_id` VARCHAR(64) NOT NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `placement` VARCHAR(64) NOT NULL
      - `module_id` VARCHAR(128) NULL
      - `trigger_item_id` VARCHAR(64) NULL
      - `item_id` VARCHAR(64) NOT NULL
      - `position` INT NOT NULL
      - `score` DOUBLE NULL
      - `price` DECIMAL(18,2) NULL
      - `currency` VARCHAR(8) NULL
      - `in_stock` TINYINT NULL
      - `exposed_at` DATETIME(3) NOT NULL

  - `dwd_rec_click_item`：推荐点击明细
    - **主键建议**：`(tenant_id, click_id)`（或 `(tenant_id, trace_id, item_id, clicked_at)`）
    - **核心索引建议**：`(tenant_id, trace_id)`、`(tenant_id, item_id, clicked_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `click_id` VARCHAR(64) NULL
      - `request_id` VARCHAR(64) NOT NULL
      - `trace_id` VARCHAR(64) NOT NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `placement` VARCHAR(64) NOT NULL
      - `module_id` VARCHAR(128) NULL
      - `trigger_item_id` VARCHAR(64) NULL
      - `item_id` VARCHAR(64) NOT NULL
      - `position` INT NULL
      - `clicked_at` DATETIME(3) NOT NULL

- **转化域（全站通用）**
  - `dwd_view_item`：商品详情页浏览（可承接 search/rec 的 trace，用于链路与兴趣序列）
    - **主键建议**：`(tenant_id, view_id)`（或 `(tenant_id, user_key, item_id, viewed_at)`）
    - **核心索引建议**：`(tenant_id, user_key, viewed_at)`、`(tenant_id, item_id, viewed_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `view_id` VARCHAR(64) NULL
      - `session_id` VARCHAR(64) NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `item_id` VARCHAR(64) NOT NULL
      - `source_type` VARCHAR(32) NULL：来源（search/rec/direct/ads/…）
      - `source_trace_id` VARCHAR(64) NULL：来源 trace（若来自 search/rec）
      - `dwell_time_ms` INT NULL
      - `viewed_at` DATETIME(3) NOT NULL

  - `dwd_add_to_cart`：加购事实表（全站统一）
    - **主键建议**：`(tenant_id, atc_id)`（或 `(tenant_id, user_key, item_id, added_at)`）
    - **核心索引建议**：`(tenant_id, user_key, added_at)`、`(tenant_id, item_id, added_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `atc_id` VARCHAR(64) NULL
      - `session_id` VARCHAR(64) NULL
      - `cart_id` VARCHAR(64) NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `item_id` VARCHAR(64) NOT NULL
      - `quantity` INT NOT NULL DEFAULT 1
      - `price` DECIMAL(18,2) NULL
      - `currency` VARCHAR(8) NULL
      - `source_type` VARCHAR(32) NULL：来源（search/rec/direct/…）
      - `source_trace_id` VARCHAR(64) NULL：来源 trace（若来自 search/rec）
      - `added_at` DATETIME(3) NOT NULL
      - `cart_snapshot_json` JSON NULL：购物车快照（可选，体积大时可下沉到独立表）

  - `dwd_purchase`：订单事实表（全站统一）
    - **主键建议**：`(tenant_id, order_id)`
    - **核心索引建议**：`(tenant_id, user_key, paid_at)`、`(tenant_id, paid_at)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `order_id` VARCHAR(64) NOT NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `currency` VARCHAR(8) NULL
      - `total_amount` DECIMAL(18,2) NULL
      - `paid_at` DATETIME(3) NOT NULL

  - `dwd_purchase_item`：订单明细事实表（全站统一）
    - **主键建议**：`(tenant_id, order_id, item_id)`（若一单同商品可多行，则增加 `line_id`）
    - **核心索引建议**：`(tenant_id, item_id, paid_at)`、`(tenant_id, order_id)`
    - **字段建议**
      - `tenant_id` VARCHAR(64) NOT NULL
      - `order_id` VARCHAR(64) NOT NULL
      - `line_id` VARCHAR(64) NULL
      - `user_key` VARCHAR(64) NOT NULL
      - `item_id` VARCHAR(64) NOT NULL
      - `quantity` INT NOT NULL DEFAULT 1
      - `price` DECIMAL(18,2) NULL
      - `paid_at` DATETIME(3) NOT NULL

  - **落库/分区建议（MySQL）**
    - 量小：按 `tenant_id` + `created_at` 索引即可
    - 量大：建议按月分表/分区（`*_202601`），或落到 ClickHouse/ES/湖仓；DWD 保持明细可追溯，DWS/ADS 做聚合提速

- **归因桥（可选但强烈建议）**
  - `dwd_attribution_touch`
    - order_id, item_id, last_touch_trace_id, touch_type(search/rec), touch_time, window（如 7d click/1d view）
  - 这样 BI 里的“搜索带来收入/推荐带来收入”不会口径混乱。

### 2.4 DWS/ADS：面向看板的汇总层（高性能）
- `dws_search_kpi_daily`（tenant_id, date, page_type/device/geo/new_vs_returning…）
  - search_pv, search_uv, sessions_with_click, sessions_with_atc, sessions_with_purchase
  - click_rate_session, purchase_rate_session, zero_result_rate, no_click_rate, p95_latency
- `dws_query_daily`（tenant_id, date, query_normalized）
  - searches, exposure_uv, click_uv, purchase_uv, zero_cnt, no_click_cnt
- `dws_rec_kpi_daily`（tenant_id, date, placement, algo_id/model_version）
  - impressions, clicks, ctr, atc, purchases, revenue, rpi（revenue per impression）
- `dws_item_daily`（tenant_id, date, item_id）
  - search_impr/click/atc/purchase、rec_impr/click/…（用于“商品天级统计特征”与运营）

---

## 3）推荐系统依赖的数据（“能训练、能实时、能解释”）
把特征分成 **用户/商品/上下文**，再分 **静态/快照/统计/序列（实时）**。

### 3.1 用户基础信息（dim）
- `dim_user`：user_key、注册时间、是否会员、国家/语言、获客渠道（如可得）、新老客标签
- 合规：只存业务需要的最小信息，敏感字段做脱敏/哈希

### 3.2 用户实时特征（在线序列 + 聚合）
在线（Redis/Key-Value）建议保留：
- **最近 N 次行为序列**（按时间衰减）
  - view_item_seq（最近浏览商品）
  - search_query_seq（最近搜索词/类目）
  - click_seq（点击的商品，区分来自 search/rec）
  - cart_seq、purchase_seq
- **实时聚合**
  - 最近 1h/24h 搜索次数、点击次数、加购次数
  - 最近一次偏好类目/品牌/价格带（从序列实时计算）

离线（天/小时级）输出：
- RFM、长期偏好向量（类目/品牌 embedding）、价格敏感度、复购周期等

### 3.3 商品基础特征（静态）
- `dim_item`（sku/spu）
  - 类目、品牌、属性（颜色/尺码/材质…）、标题/描述、价格、币种、图片
  - 文本/图片 embedding（你们已有 embeddings 模块可复用）

### 3.4 商品快照（强业务约束，必须可回溯）
- `item_snapshot`（tenant_id, item_id, snapshot_time）
  - 库存/可售、折扣、上新、发货国家限制、活动标签
- 推荐/搜索曝光明细里最好**写入当时的关键快照字段**（至少 in_stock、price），避免事后回算失真。

### 3.5 商品天级统计特征（训练/排序最常用）
- 由 `dws_item_daily` 派生：
  - 1d/7d/28d：search_ctr、search_cvr、rec_ctr、rec_cvr、atc_rate、purchase_rate
  - 热度、趋势（环比/同比）、退货/取消（如可得）
  - 分 placement 的表现（同一商品在“购物车推荐”与“首页推荐”差异巨大）

### 3.6 标签（Label）与训练样本（建议一开始就定口径）
- **曝光→点击**（CTR）标签：以 `impression_item` 为样本
- **点击→加购/购买**（CVR/GMV）标签：以 click 或 impression 为样本，设定归因窗口
- 负样本：同一 trace_id 下未被点击的曝光 item（更稳定）

---

## 4）你提到的两项“未完成工作”，数据层如何补齐
- **商品曝光（搜索/推荐）**：必须新增 `ExposureEvent`（建议一屏/一次渲染批量上报 item 列表 + position + module/placement + trace_id）
- **链路跟踪**：以 `trace_id` 为主串联；若购买跨会话严重，可在 purchase 上补 `attribution_trace_id` 或单独落 `attribution_touch` 表做归因

---

如果你愿意，我可以基于你们现有 `proto`/事件骨架，把“搜索请求/曝光/点击/加购/购买/推荐请求”这几类事件的**最小必填字段清单**列成一张表（直接给前端埋点和后端 ETL 用），并给出每个指标（CTR/CVR/zero/no-click）的**精确定义与 SQL 计算口径**。