快速实验篇(B08)搜索词分析实战

小肥柴的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

三个观察:

  1. 每天固定 100 用户:再次确认数据集只有 100 个用户。
  2. 前 10 天 3600~3817:日间波动 ±3%,几乎人工构造的均匀。
  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. 写在最后

  • 还没搞明白,需要进一步探索分析的的:
  1. "手机"为什么异常低? 数据构造时人为压低,还是真实业务现象?B09 城市维度分析时留意一下。
  2. 转化率全都一样,是数据构造的问题,还是 3C 类目的真实特征?B10 品类共现分析时再看。
  3. 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 分析总行数
相关推荐
IT研究室1 小时前
最新大数据毕业设计选题推荐-基于大数据的考研相关视频数据可视化分析-大数据-Spark-Hadoop-Bigdata
大数据·考研·课程设计
johnsong2 小时前
幽灵协议:当AI推荐死去的SaaS,编码Agent控制层正在形成
大数据·人工智能
蝶依斓(湖南)全案整装2 小时前
旧房改造整装内容写作框架:用户痛点与叙述结构
大数据
蝶依斓(湖南)全案整装3 小时前
整装行业长尾需求内容矩阵搭建方法:从五个维度拆解用户需求
大数据·人工智能·矩阵
回眸&啤酒鸭3 小时前
GLM 5.3 Flash 批量处理实战指南
大数据·人工智能
躺柒3 小时前
读数据架构知识体系指南13现代数据仓库
大数据·数据分析·数据湖·mdm·rdm
科技研学社3 小时前
外贸订单回流与关税新规下,服装工厂技改如何突围
大数据·人工智能
liulilittle4 小时前
短期内不存在通用 AI 工作流魔法
大数据·开发语言·c++·人工智能·llm·agent·tools
计算机源码社4 小时前
基于Spark的租房数据挖掘与可视化平台构建研究 基于K-Means与PCA的租房价值洞察与可视化分析
大数据·hadoop·数据挖掘·spark·毕业设计·kmeans·课程设计