小肥柴的Hadoop之旅 快速实验篇(B08)搜索词分析实战
目录
- 写在前面
- [5 分钟速读](#5 分钟速读)
- [0. 环境准备](#0. 环境准备)
- [1. 数据长什么样](#1. 数据长什么样)
- [2. 探测:先搞清楚能做什么](#2. 探测:先搞清楚能做什么)
- [2.1 探测一:搜索词有几个](#2.1 探测一:搜索词有几个)
- [2.2 探测二:用户有没有搜索偏好](#2.2 探测二:用户有没有搜索偏好)
- [2.3 探测三:搜索行为的时间分布](#2.3 探测三:搜索行为的时间分布)
- [2.4 探测四:行为与字段自洽性](#2.4 探测四:行为与字段自洽性)
- [3. 口径设计:两个必须先钉死的问题](#3. 口径设计:两个必须先钉死的问题)
- [3.1 转化率粒度:session 还是 event](#3.1 转化率粒度:session 还是 event)
- [3.2 边界对齐:07-27 要不要排除](#3.2 边界对齐:07-27 要不要排除)
- [4. 建表:从探测到落地](#4. 建表:从探测到落地)
- [4.1 event 级窗口切分的核心技巧](#4.1 event 级窗口切分的核心技巧)
- [4.2 ads_search_keyword 建表](#4.2 ads_search_keyword 建表)
- [4.3 ads_search_summary 建表](#4.3 ads_search_summary 建表)
- [5. 验证:15 项检查](#5. 验证:15 项检查)
- [6. 结果解读:6 个词说了什么](#6. 结果解读:6 个词说了什么)
- [6.1 词频:5 个扎堆,1 个掉队](#6.1 词频:5 个扎堆,1 个掉队)
- [6.2 转化率:6 个词几乎一样](#6.2 转化率:6 个词几乎一样)
- [6.3 搜索占比 22.3%](#6.3 搜索占比 22.3%)
- [7. 对运营和数据团队意味着什么](#7. 对运营和数据团队意味着什么)
- [8. 写在最后](#8. 写在最后)
- 附录:完整脚本
写在前面
B08 的原始设想是搜索词 TopN 排行榜 + 用户搜索偏好,这个任务看起来只需要按关键词分组、统计转化率,是相对简单的一个课题;但前置探测推翻了这个设想:这份数据里的搜索词,只有 6 个。
本文完整记录 B08 从探测到交付的全过程。每一步都有可直接复制的脚本、预期输出和结论。读者可以据此自己走一遍同样的流程,也能把这套方法迁移到别的数据集上。
-
环境: Hadoop 3.3.6 + Hive 3.1.3(on MR),1 master + 3 worker。
-
数据:Hive 的
b_group库,DWD 层表名叫dwd_user_behavior。
5 分钟速读
- 原始设想:搜索词 Top10 排行榜 + 用户搜索偏好。
- 探测结论:搜索词只有 6 个,全部 100 个用户都搜过每一个词。排行榜和偏好,都做不了。
- 改用方案:6 个词的全景对比(热度、覆盖、搜索后转化)。
- 核心方法论:转化率必须用 event 级,不能用 session 级。两者在同一份数据上差 3 倍。
- 主要发现:搜索后点击率高达 70%,远超真实电商(一般 20~40%)。数据极可能是人工构造的。
- 可复用产出 :
ads_search_keyword表 + 16 项汇总指标 + 15 项验证。
0. 环境准备
所有操作都在 /home/hadoop/b08 目录下进行。先建目录结构:
bash
mkdir -p /home/hadoop/b08/{sql,output,docs}
cd /home/hadoop/b08
启动集群(如果没启):
bash
start-dfs.sh && start-yarn.sh
检查 Hive 能正常连上:
bash
hive -e "USE b_group; SHOW TABLES;" 2>&1 | tail -20
应该能看到 dwd_user_behavior 以及 B01--B07 留下的其他表。
每次写 HQL 脚本,开头必须带这一段 SET 参数。这是前面几个课题踩了 OOM 之后定下来的内存控制配置,B08 及以后都继承:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
参数说明,方便迁移到别的环境:
| 参数 | 设置理由 |
|---|---|
hive.exec.mode.local.auto=false |
小输入查询会自动走本地模式(256MB 堆),容易 OOM,强制关掉 |
mapreduce.task.io.sort.mb=32 |
默认 100MB 对 2GB 节点太大 |
mapreduce.map.memory.mb=512 |
每个 map 容器上限 |
mapreduce.map.java.opts=-Xmx384m |
map JVM 堆,比容器少留 128MB 余量 |
mapreduce.reduce.memory.mb=768 |
reduce 容器上限 |
mapreduce.reduce.java.opts=-Xmx512m |
reduce JVM 堆 |
hive.exec.reducers.max=1 |
数据量小,多 reducer 反而增加调度开销 |
另外客户端堆也要设,写进 ~/.bashrc:
bash
export HADOOP_CLIENT_OPTS="-Xmx512m -Xms256m"
不设的话,Hive CLI 本身可能 OOM。
1. 数据长什么样
这份数据是电商用户行为日志,180570 行,覆盖 11 天(2019-07-17 ~ 2019-07-27)。每一行是一次用户行为。
DWD 表 dwd_user_behavior 的关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | bigint | 用户 ID |
| session_id | string | 会话 ID |
| page_id | bigint | 页面 ID |
| action_time | string | 行为时间 |
| behavior_type | string | click / order / pay / search |
| search_keyword | string | 搜索词(只在 search 行为非空) |
| click_category_id | bigint | 点击品类 ID |
| order_category_ids | array | 下单品类 ID 列表 |
| seq_in_session | bigint | 行为在 session 内的序号 |
| dt | string | 分区日期 |
字段里有 search_keyword 这一列,用来存搜索词。理论上,这是 B08 的全部素材。
2. 探测:先搞清楚能做什么
在写任何 ADS 表之前,先跑 4 组探测,把"能做什么"和"怎么做"钉死。
2.1 探测一:搜索词有几个
第一步,先确认字段存在。
bash
cd /home/hadoop/b08 && hive -e "USE b_group; DESCRIBE dwd_user_behavior;" 2>&1 | tee output/probe1_desc.log
看输出里有没有 search_keyword string 这一行。
第二步,统计搜索词的基本分布。
把下面的内容保存为 sql/probe1.hql:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
SELECT
COUNT(*) AS total_rows,
COUNT(search_keyword) AS non_null_kw,
COUNT(DISTINCT search_keyword) AS distinct_kw,
SUM(CASE WHEN search_keyword='' THEN 1 ELSE 0 END) AS empty_kw,
SUM(CASE WHEN search_keyword IS NULL THEN 1 ELSE 0 END) AS null_kw
FROM dwd_user_behavior;
保存并执行:
bash
cd /home/hadoop/b08
hive -f sql/probe1.hql 2>&1 | tee output/probe1.log
预期输出:
180570 40232 6 0 140338
结论:
- 全量 180570 行。
- 有搜索词的行 40232 行。
- 搜索词只有 6 个。 这直接推翻了"TopN 排行榜"的设想。
- 140338 行是 NULL(非搜索行为)。
第三步,看这 6 个词都是什么。
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT search_keyword, COUNT(*) AS cnt FROM dwd_user_behavior WHERE search_keyword IS NOT NULL GROUP BY search_keyword ORDER BY cnt DESC;" 2>&1 | tee output/probe1_top.log
预期输出:
吃鸡 7604
i7 7464
笔记本 7407
内存 7336
苹果 7310
手机 3111
6 个词全在这里了。前 5 个词都在 7300~7600 之间,只有"手机"明显低。
到这里,"搜索词 TopN 排行榜"的设想直接作废,6 个词就是全部。
2.2 探测二:用户有没有搜索偏好
排行榜做不成,退一步,看能否做"谁喜欢搜什么"。把下面保存为 sql/probe2.hql:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
SELECT 'C_kw_dim' AS probe_tag, search_keyword,
COUNT(*) AS search_cnt,
COUNT(DISTINCT user_id) AS user_cnt,
COUNT(DISTINCT session_id) AS session_cnt
FROM dwd_user_behavior
WHERE search_keyword IS NOT NULL
GROUP BY search_keyword;
执行:
bash
cd /home/hadoop/b08 && hive -f sql/probe2.hql 2>&1 | tee output/probe2.log
预期输出:
C_kw_dim i7 7464 100 4250
C_kw_dim 内存 7336 100 4238
C_kw_dim 吃鸡 7604 100 4354
C_kw_dim 手机 3111 100 2371
C_kw_dim 笔记本 7407 100 4286
C_kw_dim 苹果 7310 100 4248
看第三列 user_cnt:6 行全是 100。
100 个用户,每个人都搜过全部 6 个词。
这说明"搜索偏好"这个维度不存在,每个用户的搜索行为完全一样。到这里,B08 的原始设想 "TopN 排行榜 + 用户偏好" 全部作废。
2.3 探测三:搜索行为的时间分布
数据 11 天,前面几个课题都发现 07-27 疑似截断。搜索行为是否也一样?
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT dt, COUNT(*) AS search_rows, COUNT(DISTINCT user_id) AS user_cnt, COUNT(DISTINCT session_id) AS session_cnt FROM dwd_user_behavior WHERE search_keyword IS NOT NULL GROUP BY dt ORDER BY dt;" 2>&1 | tee output/probe3_D.log
预期输出:
2019-07-17 3683 100 739
2019-07-18 3684 100 757
2019-07-19 3817 100 722
2019-07-20 3653 100 743
2019-07-21 3801 100 718
2019-07-22 3743 100 774
2019-07-23 3641 100 692
2019-07-24 3701 100 766
2019-07-25 3786 100 761
2019-07-26 3606 100 760
2019-07-27 3117 100 601
三个观察:
- 每天固定 100 用户:再次确认数据集只有 100 个用户。
- 前 10 天 3600~3817:日间波动 ±3%,几乎人工构造的均匀。
- 07-27 只有 3117:比前 10 天均值低约 15%,疑似截断。
2.4 探测四:行为与字段自洽性
搜索词只在 search 行为上出现吗?会不会有 click 也带搜索词?这一步验证数据一致性。
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT behavior_type, COUNT(*) AS row_cnt, COUNT(search_keyword) AS kw_non_null, COUNT(DISTINCT search_keyword) AS kw_distinct FROM dwd_user_behavior GROUP BY behavior_type;" 2>&1 | tee output/probe4.log
预期输出:
click 120519 0 0
order 11768 0 0
pay 8051 0 0
search 40232 40232 6
完全自洽 :search_keyword 只在 search 行为非空,其他行为全 NULL。
到这里,探测告一段落。B08 能做什么、不能做什么,已经清楚了。接下来是最关键的一步:设计口径。
3. 口径设计:两个必须先钉死的问题
3.1 转化率粒度:session 还是 event
先把问题摆清楚:一个 session 里,用户如果搜索过某个词,之后又下单了,怎么算"这个词带来了转化"?
初步的想法是:
session 里搜索过 → 之后有订单 → 算这个词转化
写出来大概这样:
sql
-- 先别跑,这个算法有坑
SELECT search_keyword,
SUM(CASE WHEN 后续有 order THEN 1 ELSE 0 END) / COUNT(*) AS 转化率
FROM session_search_view
GROUP BY search_keyword;
按这个算法跑出来的数字是:搜索后 53% 下单。
这个数字明显高于合理范围。追查后发现问题出在:
一个用户的 session 可能是这样的:
搜索"苹果" → 点击 → 搜索"手机" → 点击 → 下单
按 session 级的算法:
- "苹果"这个 session 里有搜索,之后有下单 → 算转化
- "手机"这个 session 里有搜索,之后有下单 → 算转化
同一个订单,被同时算给了两个搜索词。
这不是特例。统计一下一个 session 里有几次搜索:
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT search_per_session, COUNT(*) AS session_cnt FROM (SELECT session_id, COUNT(*) AS search_per_session FROM dwd_user_behavior WHERE behavior_type='search' GROUP BY session_id) t GROUP BY search_per_session ORDER BY search_per_session;" 2>&1 | tee output/probe3_H.log
预期输出(截取前几行):
1 1553
2 1321
3 1064
4 806
5 630
6 520
...
44 1
81% 的含搜索 session 里有多于 1 次搜索,最多的一个 session 搜了 44 次。
所以 53% 的下单率是虚高的,因为同一个订单被反复计入每个搜过的词。必须改成 event 级。
修法:
- 按 session 算 = 一个顾客逛了一次店,最后买没买?
- 按每次搜索算 = 这个顾客每次问店员一个问题,问完之后买没买?
一个顾客可能问了 5 次问题,第一种算法只算 1 次,第二种算 5 次。第二种才是真实因果链,"问完苹果手机之后买了什么"跟"问完手机壳之后买了什么"是两回事。
改完重跑,转化率从 53% 掉到 19%。差了近 3 倍,这是 B08 最核心的方法论改进。
3.2 边界对齐:07-27 要不要排除
前面几个课题都排除了 07-27。B08 也要排除,但排除方式必须和 B07 完全一致,否则跨表验证会失败。
先探测两件事。
第一,07-27 是不是只有搜索少,还是所有行为都少?
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT dt, behavior_type, COUNT(*) AS cnt FROM dwd_user_behavior WHERE dt IN ('2019-07-26','2019-07-27') GROUP BY dt, behavior_type ORDER BY behavior_type, dt;" 2>&1 | tee output/probe3_F.log
预期输出:
2019-07-26 click 11206
2019-07-27 click 9209
2019-07-26 order 1078
2019-07-27 order 881
2019-07-26 pay 725
2019-07-27 pay 593
2019-07-26 search 3606
2019-07-27 search 3117
所有行为都跌约 18%,不是只有 search 跌。说明 07-27 是整体截断,不是搜索专项异常。
第二,B07 的 session 表是怎么处理 07-27 的?
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT SUBSTR(start_ts,1,10) AS start_dt, COUNT(*) AS cnt FROM ads_session_sequence GROUP BY SUBSTR(start_ts,1,10) ORDER BY start_dt;" 2>&1 | tee output/probe3_G.log
预期输出:
2019-07-17 871
2019-07-18 882
...
2019-07-26 904
B07 的 session 起始日只到 07-26,07-27 已被排除。
第三,B08 用物理过滤 dt < '2019-07-27' 后,含搜索的 session 数是否等于 B07 的对应数?
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT COUNT(DISTINCT session_id) AS sessions_with_search_before_0727 FROM dwd_user_behavior WHERE dt < '2019-07-27' AND behavior_type='search';" 2>&1 | tee output/probe3_K.log
预期输出:
7426
同时,B07 中含搜索的 session 数:
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT COUNT(DISTINCT s.session_id) AS b07_sessions_with_search FROM ads_session_sequence s WHERE EXISTS (SELECT 1 FROM dwd_user_behavior d WHERE d.session_id = s.session_id AND d.behavior_type = 'search');" 2>&1 | tee output/probe3_I.log
预期输出:
7426
两个数字完全相等。 说明 B07 走的是"物理切掉 07-27 的所有行"的策略,B08 用 WHERE dt < '2019-07-27' 即可 100% 对齐。
结论:B08 的口径定了。
| 项 | 值 |
|---|---|
| 分析范围 | WHERE dt < '2019-07-27' |
| 转化率粒度 | event 级,窗口 = (本次搜索, 下一次搜索] 或 (本次搜索, session 结束] |
| 排序字段 | seq_in_session(不用时间戳,避免同秒乱序) |
| 辅助口径 | session 级,用于与 B03 漏斗对照,同时输出 |
4. 建表:从探测到落地
4.1 event 级窗口切分的核心技巧
event 级的关键,是要知道每个行为"属于第几次搜索"。用一个窗口函数实现:
sql
SUM(CASE WHEN behavior_type='search' THEN 1 ELSE 0 END)
OVER (PARTITION BY session_id
ORDER BY seq_in_session
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS search_no
解释:从 session 开头算到当前行为,累计出现了几次搜索。
举例,行为序列是:
搜索, 点击, 搜索, 点击, 支付
search_no 依次是:
1, 1, 2, 2, 2
这样点击和支付就自动归属到"它前面最近的那次搜索"了。搜索行为本身也带 search_no,但可以在后续筛选时排除。
4.2 ads_search_keyword 建表
把下面的内容保存为 sql/01_build_search_keyword.hql:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
DROP TABLE IF EXISTS ads_search_keyword;
CREATE TABLE ads_search_keyword (
search_keyword string COMMENT '搜索词',
search_cnt bigint COMMENT '搜索事件数',
user_cnt bigint COMMENT '覆盖用户数',
session_cnt bigint COMMENT '覆盖 session 数',
events_with_click_after bigint COMMENT 'event 级:后续窗口含 click 的搜索事件数',
events_with_order_after bigint COMMENT 'event 级:后续窗口含 order 的搜索事件数',
events_with_pay_after bigint COMMENT 'event 级:后续窗口含 pay 的搜索事件数',
event_click_rate double COMMENT 'event 级 click 转化率',
event_order_rate double COMMENT 'event 级 order 转化率',
event_pay_rate double COMMENT 'event 级 pay 转化率',
sessions_with_click_after bigint COMMENT 'session 级:含该词且最早搜索后含 click 的 session 数',
sessions_with_order_after bigint COMMENT 'session 级:同上,order',
sessions_with_pay_after bigint COMMENT 'session 级:同上,pay',
session_click_rate double COMMENT 'session 级 click 转化率',
session_order_rate double COMMENT 'session 级 order 转化率',
session_pay_rate double COMMENT 'session 级 pay 转化率'
)
STORED AS ORC;
WITH
filtered AS (
SELECT session_id, user_id, behavior_type, search_keyword, seq_in_session
FROM dwd_user_behavior
WHERE dt < '2019-07-27'
),
search_events AS (
SELECT session_id, search_keyword, seq_in_session,
ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY seq_in_session) AS search_no
FROM filtered
WHERE behavior_type = 'search'
),
behavior_tagged AS (
SELECT session_id, behavior_type, seq_in_session,
SUM(CASE WHEN behavior_type='search' THEN 1 ELSE 0 END)
OVER (PARTITION BY session_id ORDER BY seq_in_session
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS search_no
FROM filtered
),
event_win AS (
SELECT s.session_id, s.search_keyword, s.search_no,
MAX(CASE WHEN a.behavior_type='click' THEN 1 ELSE 0 END) AS has_click,
MAX(CASE WHEN a.behavior_type='order' THEN 1 ELSE 0 END) AS has_order,
MAX(CASE WHEN a.behavior_type='pay' THEN 1 ELSE 0 END) AS has_pay
FROM search_events s
LEFT JOIN behavior_tagged a
ON s.session_id = a.session_id
AND a.search_no = s.search_no
AND a.behavior_type <> 'search'
GROUP BY s.session_id, s.search_keyword, s.search_no
),
event_agg AS (
SELECT search_keyword,
COUNT(*) AS search_cnt,
SUM(has_click) AS events_with_click_after,
SUM(has_order) AS events_with_order_after,
SUM(has_pay) AS events_with_pay_after
FROM event_win
GROUP BY search_keyword
),
user_agg AS (
SELECT search_keyword, COUNT(DISTINCT user_id) AS user_cnt
FROM filtered
WHERE behavior_type='search'
GROUP BY search_keyword
),
session_min AS (
SELECT session_id, search_keyword, MIN(seq_in_session) AS min_search_seq
FROM filtered
WHERE behavior_type='search'
GROUP BY session_id, search_keyword
),
session_win AS (
SELECT sm.session_id, sm.search_keyword,
MAX(CASE WHEN f.behavior_type='click' AND f.seq_in_session > sm.min_search_seq THEN 1 ELSE 0 END) AS has_click,
MAX(CASE WHEN f.behavior_type='order' AND f.seq_in_session > sm.min_search_seq THEN 1 ELSE 0 END) AS has_order,
MAX(CASE WHEN f.behavior_type='pay' AND f.seq_in_session > sm.min_search_seq THEN 1 ELSE 0 END) AS has_pay
FROM session_min sm
JOIN filtered f ON sm.session_id = f.session_id
GROUP BY sm.session_id, sm.search_keyword
),
session_agg AS (
SELECT search_keyword,
COUNT(*) AS session_cnt,
SUM(has_click) AS sessions_with_click_after,
SUM(has_order) AS sessions_with_order_after,
SUM(has_pay) AS sessions_with_pay_after
FROM session_win
GROUP BY search_keyword
)
INSERT OVERWRITE TABLE ads_search_keyword
SELECT
ea.search_keyword,
ea.search_cnt,
ua.user_cnt,
sa.session_cnt,
ea.events_with_click_after,
ea.events_with_order_after,
ea.events_with_pay_after,
CAST(ea.events_with_click_after AS DOUBLE) / ea.search_cnt AS event_click_rate,
CAST(ea.events_with_order_after AS DOUBLE) / ea.search_cnt AS event_order_rate,
CAST(ea.events_with_pay_after AS DOUBLE) / ea.search_cnt AS event_pay_rate,
sa.sessions_with_click_after,
sa.sessions_with_order_after,
sa.sessions_with_pay_after,
CAST(sa.sessions_with_click_after AS DOUBLE) / sa.session_cnt AS session_click_rate,
CAST(sa.sessions_with_order_after AS DOUBLE) / sa.session_cnt AS session_order_rate,
CAST(sa.sessions_with_pay_after AS DOUBLE) / sa.session_cnt AS session_pay_rate
FROM event_agg ea
JOIN user_agg ua ON ea.search_keyword = ua.search_keyword
JOIN session_agg sa ON ea.search_keyword = sa.search_keyword;
注意 Hive 的一个语法坑 :WITH 必须在 INSERT 之前 ,不能写成 INSERT ... WITH ...。这跟标准 SQL 的习惯相反,写成后者会报 ParseException。
执行:
bash
cd /home/hadoop/b08 && hive -f sql/01_build_search_keyword.hql 2>&1 | tee output/01_build_kw.log
这个脚本会跑 10 个 MR job,耗时 6~7 分钟。跑完看日志末尾有没有 OK 和 Time taken。
查看结果:
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT * FROM ads_search_keyword ORDER BY search_cnt DESC;" 2>&1 | tee output/04_view_kw.log
预期输出(6 行,每行 16 个字段):
吃鸡 7055 100 4042 5009 1368 990 0.710 0.194 0.140 3736 2158 1791 0.924 0.534 0.443
i7 6881 100 3921 4870 1280 922 0.708 0.186 0.134 3632 2087 1716 0.926 0.532 0.438
笔记本 6840 100 3952 4715 1296 934 0.689 0.189 0.137 3627 2136 1759 0.918 0.540 0.445
内存 6771 100 3933 4799 1343 970 0.709 0.198 0.143 3617 2137 1716 0.920 0.543 0.436
苹果 6722 100 3910 4735 1254 924 0.704 0.187 0.137 3596 2079 1737 0.920 0.532 0.444
手机 2846 100 2172 1971 540 401 0.693 0.190 0.141 1994 1171 977 0.918 0.539 0.450
4.3 ads_search_summary 建表
汇总指标表,kv 形式。保存为 sql/02_build_search_summary.hql:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
DROP TABLE IF EXISTS ads_search_summary;
CREATE TABLE ads_search_summary (
metric_name string,
metric_value string
)
STORED AS ORC;
INSERT OVERWRITE TABLE ads_search_summary
SELECT 'total_search_cnt', CAST(COUNT(*) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27' AND behavior_type='search'
UNION ALL
SELECT 'distinct_keyword', CAST(COUNT(DISTINCT search_keyword) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27' AND behavior_type='search'
UNION ALL
SELECT 'distinct_user', CAST(COUNT(DISTINCT user_id) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27' AND behavior_type='search'
UNION ALL
SELECT 'distinct_session_with_search', CAST(COUNT(DISTINCT session_id) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27' AND behavior_type='search'
UNION ALL
SELECT 'search_share_in_behavior',
CAST(ROUND(SUM(CASE WHEN behavior_type='search' THEN 1 ELSE 0 END)*1.0/COUNT(*), 6) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27'
UNION ALL
SELECT 'avg_search_per_session',
CAST(ROUND(
SUM(CASE WHEN behavior_type='search' THEN 1 ELSE 0 END)*1.0 /
COUNT(DISTINCT CASE WHEN behavior_type='search' THEN session_id END),
4) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27'
UNION ALL
SELECT 'top1_keyword', search_keyword
FROM (SELECT search_keyword, COUNT(*) AS c FROM dwd_user_behavior
WHERE dt<'2019-07-27' AND behavior_type='search'
GROUP BY search_keyword ORDER BY c DESC LIMIT 1) t
UNION ALL
SELECT 'top1_keyword_cnt', CAST(MAX(c) AS STRING)
FROM (SELECT search_keyword, COUNT(*) AS c FROM dwd_user_behavior
WHERE dt<'2019-07-27' AND behavior_type='search'
GROUP BY search_keyword) t
UNION ALL
SELECT 'bottom1_keyword', search_keyword
FROM (SELECT search_keyword, COUNT(*) AS c FROM dwd_user_behavior
WHERE dt<'2019-07-27' AND behavior_type='search'
GROUP BY search_keyword ORDER BY c ASC LIMIT 1) t
UNION ALL
SELECT 'bottom1_keyword_cnt', CAST(MIN(c) AS STRING)
FROM (SELECT search_keyword, COUNT(*) AS c FROM dwd_user_behavior
WHERE dt<'2019-07-27' AND behavior_type='search'
GROUP BY search_keyword) t
UNION ALL
SELECT 'date_range_start', MIN(dt) FROM dwd_user_behavior WHERE dt<'2019-07-27'
UNION ALL
SELECT 'date_range_end', MAX(dt) FROM dwd_user_behavior WHERE dt<'2019-07-27'
UNION ALL
SELECT 'date_range_days', CAST(COUNT(DISTINCT dt) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27'
UNION ALL
SELECT 'excluded_dt', '2019-07-27'
UNION ALL
SELECT 'excluded_dt_rows', CAST(COUNT(*) AS STRING)
FROM dwd_user_behavior WHERE dt='2019-07-27'
UNION ALL
SELECT 'total_rows_analyzed', CAST(COUNT(*) AS STRING)
FROM dwd_user_behavior WHERE dt<'2019-07-27';
执行:
bash
cd /home/hadoop/b08 && hive -f sql/02_build_search_summary.hql 2>&1 | tee output/02_build_summary.log
查看结果:
bash
cd /home/hadoop/b08 && hive -e "SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=32; SET mapreduce.map.memory.mb=512; SET mapreduce.map.java.opts=-Xmx384m; SET mapreduce.reduce.memory.mb=768; SET mapreduce.reduce.java.opts=-Xmx512m; SET hive.exec.reducers.max=1; USE b_group; SELECT * FROM ads_search_summary;" 2>&1 | tee output/05_view_summary.log
预期输出(16 行):
distinct_session_with_search 7426
search_share_in_behavior 0.222552
avg_search_per_session 4.998
date_range_start 2019-07-17
total_rows_analyzed 166770
date_range_end 2019-07-26
bottom1_keyword_cnt 2846
total_search_cnt 37115
bottom1_keyword 手机
excluded_dt_rows 13800
top1_keyword_cnt 7055
top1_keyword 吃鸡
distinct_keyword 6
date_range_days 10
distinct_user 100
excluded_dt 2019-07-27
5. 验证:15 项检查
数据交付前跑 15 项验证。保存为 sql/03_verify.hql:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
SELECT 'V01_total_search_events', CAST(SUM(search_cnt) AS STRING) FROM ads_search_keyword;
SELECT 'V02_distinct_keyword', CAST(COUNT(*) AS STRING) FROM ads_search_keyword;
SELECT 'V03_user_cnt_distinct', CAST(COUNT(DISTINCT user_cnt) AS STRING) FROM ads_search_keyword;
SELECT 'V04_user_cnt_all_100',
CASE WHEN SUM(CASE WHEN user_cnt=100 THEN 1 ELSE 0 END)=6 THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V05_event_click_range',
CASE WHEN MIN(event_click_rate)>=0 AND MAX(event_click_rate)<=1 THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V06_event_order_range',
CASE WHEN MIN(event_order_rate)>=0 AND MAX(event_order_rate)<=1 THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V07_event_pay_range',
CASE WHEN MIN(event_pay_rate)>=0 AND MAX(event_pay_rate)<=1 THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V08_event_funnel_monotonic',
CASE WHEN SUM(CASE WHEN event_click_rate>=event_order_rate
AND event_order_rate>=event_pay_rate THEN 1 ELSE 0 END)=6
THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V09_session_le_search',
CASE WHEN SUM(CASE WHEN session_cnt<=search_cnt THEN 1 ELSE 0 END)=6
THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V10_session_click_le_total',
CASE WHEN SUM(CASE WHEN sessions_with_click_after<=session_cnt THEN 1 ELSE 0 END)=6
THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V11_session_click_range',
CASE WHEN MIN(session_click_rate)>=0 AND MAX(session_click_rate)<=1 THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V12_session_ge_event_click',
CASE WHEN SUM(CASE WHEN session_click_rate>=event_click_rate THEN 1 ELSE 0 END)=6
THEN 'PASS' ELSE 'FAIL' END
FROM ads_search_keyword;
SELECT 'V13_top1_is_chi_ji',
CASE WHEN (SELECT search_keyword FROM ads_search_keyword ORDER BY search_cnt DESC LIMIT 1)='吃鸡'
THEN 'PASS' ELSE 'FAIL' END;
SELECT 'V14_bottom1_is_shou_ji',
CASE WHEN (SELECT search_keyword FROM ads_search_keyword ORDER BY search_cnt ASC LIMIT 1)='手机'
THEN 'PASS' ELSE 'FAIL' END;
SELECT 'V15_summary_metric_cnt', CAST(COUNT(*) AS STRING) FROM ads_search_summary;
执行:
bash
cd /home/hadoop/b08 && hive -f sql/03_verify.hql 2>&1 | tee output/03_verify.log
预期输出:
V01_total_search_events 37115
V02_distinct_keyword 6
V03_user_cnt_distinct 1
V04_user_cnt_all_100 PASS
V05_event_click_range PASS
V06_event_order_range PASS
V07_event_pay_range PASS
V08_event_funnel_monotonic PASS
V09_session_le_search PASS
V10_session_click_le_total PASS
V11_session_click_range PASS
V12_session_ge_event_click PASS
V13_top1_is_chi_ji PASS
V14_bottom1_is_shou_ji PASS
V15_summary_metric_cnt 16
-
V03 说明 :
user_cnt全等于 100,COUNT(DISTINCT 100)结果是 1,这是对的,不是 bug。 -
V12 是 B08 设计的核心验证。event 级窗口是 session 级的严格子集,所以 session 级转化率必须 ≥ event 级。如果这条失败,说明窗口切分逻辑有 bug。
6. 结果解读
6.1 词频:5 个扎堆,1 个掉队
| 词 | 搜索次数 |
|---|---|
| 吃鸡 | 7055 |
| i7 | 6881 |
| 笔记本 | 6840 |
| 内存 | 6771 |
| 苹果 | 6722 |
| 手机 | 2846 |
前 5 个词都在 6700~7100 之间,波动不超过 4%,几乎是均匀分布。只有"手机"只有 2846 次,是其他 5 个词的 40%。
"手机"是个泛词,其他 5 个(吃鸡、i7、笔记本、内存、苹果)都是 3C 里的具体品类。如果这是真实数据,有两种可能:
- 用户更习惯搜具体型号(i7、吃鸡)而不是泛词(手机)
- 数据在构造时对"手机"做了特殊处理
单看这份数据没法确定。记下来,B12 异常检测时再核对。
6.2 转化率:6 个词几乎一样
| 词 | 搜索后点击 | 搜索后下单 | 搜索后支付 |
|---|---|---|---|
| 吃鸡 | 71.0% | 19.4% | 14.0% |
| i7 | 70.8% | 18.6% | 13.4% |
| 笔记本 | 68.9% | 18.9% | 13.7% |
| 内存 | 70.9% | 19.8% | 14.3% |
| 苹果 | 70.4% | 18.7% | 13.7% |
| 手机 | 69.3% | 19.0% | 14.1% |
6 个词的转化率差异全部小于 2 个百分点。这在真实电商里几乎不可能,搜"吃鸡"和搜"苹果"的后续行为怎么可能完全一样?
这是 B08 最重要的一个数据质量信号:这份数据极可能是人工构造的。
6.3 搜索占比 22.3%
全部行为 166770 条里,搜索占了 37115 条,22.3%。用户每做 5 个动作,就有 1 个是搜索。
这个比例在真实电商里偏高(一般 5~15%),但在"3C 类目 + 人工构造"的语境下倒也能理解。
7. 对运营和数据团队意味着什么
- 三条运营参考建议:
第一,别用"session 级"转化率做决策。 如果看到报告说"搜索后 53% 下单",不要拿去做搜索广告的投放预算。真实数字是 19%,差 3 倍。任何基于 session 级的数字,都要先问一句"这是按每次搜索算,还是按整个 session 算?"
第二,搜索词本身没什么优化空间。 6 个词,每个用户都搜过全部 6 个,转化率还都差不多。这意味着"优化搜索推荐"、"引导用户搜高转化词"这类动作,在这份数据上做不出效果。
第三,"手机"这个词单独看。 它的搜索量明显低于其他 5 个词。后续做类目分析时,它可能是特殊样本,要么单列分析,要么剔除。
- 给数据团队两条:
第一,这份数据的转化率不能对外用。 搜索后 71% 点击、19% 下单,这两个数字都远高于真实电商。引用这份数据的报告,都要标注"数据为演示数据"。
第二,B12 异常检测可以用 B08 做基线。 既然 6 个词高度均匀、转化率也高度均匀,那"均匀"就是这份数据的正常状态。B12 做异常检测时,任何偏离均匀的样本,才是需要重点排查的。
8. 写在最后
- 还没搞明白,需要进一步探索分析的的:
- "手机"为什么异常低? 数据构造时人为压低,还是真实业务现象?B09 城市维度分析时留意一下。
- 转化率全都一样,是数据构造的问题,还是 3C 类目的真实特征?B10 品类共现分析时再看。
- 71% 的搜索后点击率,太高了。B12 异常检测要建立"正常基线"时,用 B08 的均匀分布做起点。
- 小结:
搜索词分析,看起来是最没悬念的活儿。但只要探测做得够细,就能发现"能做的"和"想做的"不是一回事,也能发现"数字好看"和"数字可信"不是一回事。
B08 交付的不是一份漂亮的排行榜,而是一份"知道边界在哪"的分析。
- 踩过的坑速查表:
| 坑 | 表现 | 解法 |
|---|---|---|
rows 是 Hive 保留字 |
AS rows 报 ParseException |
改 row_cnt |
both 是 Hive 保留字 |
AS both 报 ParseException |
改 both_cnt |
ads_session_sequence 无 dt 字段 |
报 Invalid column | 用 SUBSTR(start_ts,1,10) |
Hive WITH 必须在 INSERT 前 |
INSERT ... WITH 报 ParseException |
改为 WITH ... INSERT |
tee 前必须 cd |
日志落盘失败 | 每次先 cd /home/hadoop/b08 |
附录:完整脚本
本文涉及的全部脚本,都在 /home/hadoop/b08/sql/ 下:
| 文件 | 用途 |
|---|---|
probe1.hql |
探测搜索词维度(2.1) |
probe2.hql |
探测用户覆盖(2.2) |
01_build_search_keyword.hql |
建 ads_search_keyword(4.2) |
02_build_search_summary.hql |
建 ads_search_summary(4.3) |
03_verify.hql |
15 项验证(5) |
其余探测命令是 hive -e 一行式,已在正文中给出,直接复制运行即可。
表结构速查:
ads_search_keyword(6 行,每行一个搜索词)
| 字段 | 说明 |
|---|---|
| search_keyword | 搜索词 |
| search_cnt | 搜索事件数 |
| user_cnt | 覆盖用户数 |
| session_cnt | 覆盖 session 数 |
| events_with_click_after | event 级:后续窗口含 click 的搜索事件数 |
| events_with_order_after | event 级:后续窗口含 order 的搜索事件数 |
| events_with_pay_after | event 级:后续窗口含 pay 的搜索事件数 |
| event_click_rate | event 级 click 转化率 |
| event_order_rate | event 级 order 转化率 |
| event_pay_rate | event 级 pay 转化率 |
| sessions_with_click_after | session 级:最早搜索后含 click 的 session 数 |
| sessions_with_order_after | session 级:同上,order |
| sessions_with_pay_after | session 级:同上,pay |
| session_click_rate | session 级 click 转化率 |
| session_order_rate | session 级 order 转化率 |
| session_pay_rate | session 级 pay 转化率 |
ads_search_summary(16 项 kv)
| metric_name | 说明 |
|---|---|
| total_search_cnt | 搜索事件总数 |
| distinct_keyword | 去重搜索词数 |
| distinct_user | 去重搜索用户数 |
| distinct_session_with_search | 含搜索的 session 数 |
| search_share_in_behavior | 搜索占全部行为比例 |
| avg_search_per_session | 平均每 session 搜索次数 |
| top1_keyword / top1_keyword_cnt | Top1 词及次数 |
| bottom1_keyword / bottom1_keyword_cnt | Bottom1 词及次数 |
| date_range_start / date_range_end | 分析范围起止日 |
| date_range_days | 天数 |
| excluded_dt / excluded_dt_rows | 排除日及行数 |
| total_rows_analyzed | 分析总行数 |