|
|
@@ -0,0 +1,256 @@
|
|
|
+package com.tzld.piaoquan.recommend.server.service.funnel;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import org.apache.commons.collections4.CollectionUtils;
|
|
|
+import org.apache.commons.collections4.MapUtils;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 把 FunnelContext 序列化成一条 SLS 日志(8 个 step JSON 字段 + filter reasons)。
|
|
|
+ */
|
|
|
+public class FunnelAggregator {
|
|
|
+
|
|
|
+ public static Map<String, String> toLogItem(FunnelContext ctx) {
|
|
|
+ Map<String, String> row = new LinkedHashMap<>();
|
|
|
+ if (ctx == null) return row;
|
|
|
+
|
|
|
+ // base
|
|
|
+ row.put("traceId", StringUtils.defaultString(ctx.getTraceId()));
|
|
|
+ row.put("recommendTraceId", StringUtils.defaultString(ctx.getRecommendTraceId()));
|
|
|
+ row.put("sessionId", StringUtils.defaultString(ctx.getSessionId()));
|
|
|
+ row.put("subSessionId", StringUtils.defaultString(ctx.getSubSessionId()));
|
|
|
+ row.put("rootSessionId", StringUtils.defaultString(ctx.getRootSessionId()));
|
|
|
+ row.put("mid", StringUtils.defaultString(ctx.getMid()));
|
|
|
+ row.put("appType", String.valueOf(ctx.getAppType()));
|
|
|
+ row.put("newExpGroup", StringUtils.defaultString(ctx.getNewExpGroup()));
|
|
|
+ row.put("abExpCode", JSON.toJSONString(ctx.getAbExpCodes()));
|
|
|
+
|
|
|
+ // step 1-3 + filter reasons
|
|
|
+ row.put("step_1_recall", JSON.toJSONString(buildStep1(ctx)));
|
|
|
+ row.put("step_2_filtered", JSON.toJSONString(buildStep2(ctx)));
|
|
|
+ row.put("step_2_filter_reasons", JSON.toJSONString(buildStep2Reasons(ctx)));
|
|
|
+ row.put("step_3_truncated", JSON.toJSONString(buildStep3(ctx)));
|
|
|
+
|
|
|
+ // step 4-8
|
|
|
+ row.put("step_4_merged", JSON.toJSONString(buildStep4(ctx)));
|
|
|
+ row.put("step_5_ranked", JSON.toJSONString(buildStep5(ctx)));
|
|
|
+ row.put("step_6_rank_truncated", JSON.toJSONString(buildStep6(ctx)));
|
|
|
+ row.put("step_7_cold_start", JSON.toJSONString(buildStep7(ctx)));
|
|
|
+ row.put("step_8_output", JSON.toJSONString(buildStep8(ctx)));
|
|
|
+
|
|
|
+ return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== step 1-3: {pushFrom: [...]} =====
|
|
|
+ private static Map<String, JSONArray> buildStep1(FunnelContext ctx) {
|
|
|
+ Map<String, JSONArray> out = new LinkedHashMap<>();
|
|
|
+ ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (RecallVideoEntry e : entries) {
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", e.getVideoId());
|
|
|
+ o.put("index", displayIndex(e.getIndex()));
|
|
|
+ o.put("score", round6(e.getScore()));
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ out.put(pf, arr);
|
|
|
+ });
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, JSONArray> buildStep2(FunnelContext ctx) {
|
|
|
+ Map<String, JSONArray> out = new LinkedHashMap<>();
|
|
|
+ ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (RecallVideoEntry e : entries) {
|
|
|
+ if (!e.isFilteredIn()) continue;
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", e.getVideoId());
|
|
|
+ o.put("index", displayIndex(e.getIndex()));
|
|
|
+ o.put("score", round6(e.getScore()));
|
|
|
+ o.put("index_new", displayIndex(e.getIndexNewAfterFilter()));
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ out.put(pf, arr);
|
|
|
+ });
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, JSONArray> buildStep2Reasons(FunnelContext ctx) {
|
|
|
+ Map<String, JSONArray> out = new LinkedHashMap<>();
|
|
|
+ ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (RecallVideoEntry e : entries) {
|
|
|
+ if (e.isFilteredIn()) continue;
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", e.getVideoId());
|
|
|
+ o.put("index", displayIndex(e.getIndex()));
|
|
|
+ o.put("score", round6(e.getScore()));
|
|
|
+ o.put("reasons", e.getFilterReasons());
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ if (!arr.isEmpty()) out.put(pf, arr);
|
|
|
+ });
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, JSONArray> buildStep3(FunnelContext ctx) {
|
|
|
+ Map<String, JSONArray> out = new LinkedHashMap<>();
|
|
|
+ ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (RecallVideoEntry e : entries) {
|
|
|
+ if (e.getSelect() == null) continue;
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", e.getVideoId());
|
|
|
+ o.put("index", displayIndex(e.getIndex()));
|
|
|
+ o.put("score", round6(e.getScore()));
|
|
|
+ o.put("index_new", displayIndex(e.getIndexNewAfterFilter()));
|
|
|
+ o.put("select", e.getSelect().name().toLowerCase());
|
|
|
+ o.put("truncate", e.getTruncate());
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ out.put(pf, arr);
|
|
|
+ });
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== step 4-8: 按 vid 组织,每条带完整 recalls 历史 =====
|
|
|
+ private static JSONArray buildStep4(FunnelContext ctx) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (Long vid : ctx.getStep4MergedVideoIds()) {
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", vid);
|
|
|
+ o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
|
|
|
+ o.put("recalls", buildRecallsForVid(ctx, vid));
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JSONArray buildStep5(FunnelContext ctx) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (Long vid : ctx.getStep4MergedVideoIds()) {
|
|
|
+ RankVideoEntry r = ctx.getStep5RankedData().get(vid);
|
|
|
+ if (r == null) continue;
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", vid);
|
|
|
+ o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
|
|
|
+ o.put("recalls", buildRecallsForVid(ctx, vid));
|
|
|
+ o.put("rank", buildRankBlock(r));
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JSONArray buildStep6(FunnelContext ctx) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (int i = 0; i < ctx.getStep6RankTruncatedVideoIds().size(); i++) {
|
|
|
+ long vid = ctx.getStep6RankTruncatedVideoIds().get(i);
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", vid);
|
|
|
+ o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
|
|
|
+ o.put("recalls", buildRecallsForVid(ctx, vid));
|
|
|
+ RankVideoEntry r = ctx.getStep5RankedData().get(vid);
|
|
|
+ if (r != null) o.put("rank", buildRankBlock(r));
|
|
|
+ o.put("rank_index", i + 1);
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JSONArray buildStep7(FunnelContext ctx) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ // step 7 = step 6 + if_cold_start
|
|
|
+ for (int i = 0; i < ctx.getStep6RankTruncatedVideoIds().size(); i++) {
|
|
|
+ long vid = ctx.getStep6RankTruncatedVideoIds().get(i);
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", vid);
|
|
|
+ o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
|
|
|
+ o.put("recalls", buildRecallsForVid(ctx, vid));
|
|
|
+ RankVideoEntry r = ctx.getStep5RankedData().get(vid);
|
|
|
+ if (r != null) o.put("rank", buildRankBlock(r));
|
|
|
+ o.put("rank_index", i + 1);
|
|
|
+ ColdStartAction action = ctx.getStep7ColdStartActions().getOrDefault(vid, ColdStartAction.NONE);
|
|
|
+ o.put("if_cold_start", action != ColdStartAction.NONE);
|
|
|
+ o.put("cold_start_action", action.name());
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ // 冷启 INSERTED 的视频可能不在 step6RankTruncatedVideoIds 里,补
|
|
|
+ ctx.getStep7ColdStartActions().forEach((vid, action) -> {
|
|
|
+ if (action == ColdStartAction.INSERTED && !ctx.getStep6RankTruncatedVideoIds().contains(vid)) {
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", vid);
|
|
|
+ o.put("if_cold_start", true);
|
|
|
+ o.put("cold_start_action", action.name());
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JSONArray buildStep8(FunnelContext ctx) {
|
|
|
+ JSONArray arr = new JSONArray();
|
|
|
+ for (int i = 0; i < ctx.getStep8OutputVideoIds().size(); i++) {
|
|
|
+ long vid = ctx.getStep8OutputVideoIds().get(i);
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("vid", vid);
|
|
|
+ o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
|
|
|
+ o.put("recalls", buildRecallsForVid(ctx, vid));
|
|
|
+ RankVideoEntry r = ctx.getStep5RankedData().get(vid);
|
|
|
+ if (r != null) o.put("rank", buildRankBlock(r));
|
|
|
+ ColdStartAction action = ctx.getStep7ColdStartActions().getOrDefault(vid, ColdStartAction.NONE);
|
|
|
+ o.put("if_cold_start", action != ColdStartAction.NONE);
|
|
|
+ o.put("rank_index", i + 1);
|
|
|
+ arr.add(o);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== helpers =====
|
|
|
+ private static JSONArray buildRecallsForVid(FunnelContext ctx, long vid) {
|
|
|
+ JSONArray recalls = new JSONArray();
|
|
|
+ ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
|
|
|
+ for (RecallVideoEntry e : entries) {
|
|
|
+ if (e.getVideoId() != vid) continue;
|
|
|
+ if (e.getSelect() != SelectKind.SELF) continue; // 只列该路实际贡献到合并的视频
|
|
|
+ JSONObject r = new JSONObject();
|
|
|
+ r.put("strategy", pf);
|
|
|
+ r.put("index", displayIndex(e.getIndex()));
|
|
|
+ r.put("score", round6(e.getScore()));
|
|
|
+ r.put("index_new", displayIndex(e.getIndexNewAfterFilter()));
|
|
|
+ r.put("truncate", e.getTruncate());
|
|
|
+ recalls.add(r);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return recalls;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JSONObject buildRankBlock(RankVideoEntry r) {
|
|
|
+ JSONObject o = new JSONObject();
|
|
|
+ o.put("score", round6(r.getRankScore()));
|
|
|
+ if (MapUtils.isNotEmpty(r.getSubScores())) {
|
|
|
+ r.getSubScores().forEach((k, v) -> o.put(k, round6(v)));
|
|
|
+ }
|
|
|
+ return o;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** double → 保留 6 位小数 */
|
|
|
+ private static BigDecimal round6(double v) {
|
|
|
+ return BigDecimal.valueOf(v).setScale(6, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 内部 0-based 位置 → 对外展示 1-based。负值(如 -1 表"未通过")原样返回 */
|
|
|
+ private static int displayIndex(int zeroBased) {
|
|
|
+ return zeroBased < 0 ? zeroBased : zeroBased + 1;
|
|
|
+ }
|
|
|
+}
|