|
|
@@ -1,141 +1,256 @@
|
|
|
package com.tzld.piaoquan.recommend.server.service.funnel;
|
|
|
|
|
|
-import com.google.common.base.Strings;
|
|
|
-import com.tzld.piaoquan.recommend.server.gen.recommend.RecommendRequest;
|
|
|
-import com.tzld.piaoquan.recommend.server.model.RecommendParam;
|
|
|
-import com.tzld.piaoquan.recommend.server.model.Video;
|
|
|
-import com.tzld.piaoquan.recommend.server.service.rank.RankResult;
|
|
|
-import com.tzld.piaoquan.recommend.server.service.recall.RecallResult;
|
|
|
-import com.tzld.piaoquan.recommend.server.util.JSONUtils;
|
|
|
+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.util.ArrayList;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.util.HashMap;
|
|
|
-import java.util.HashSet;
|
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
-import java.util.Set;
|
|
|
|
|
|
/**
|
|
|
- * 把 4 阶段(raw / filtered / ranked / preview)合并成每 (videoId, pushFrom) 一行。
|
|
|
- * 下游 BI: group by videoId max(各 flag) 出整体漏斗;group by pushFrom 出分策略漏斗。
|
|
|
+ * 把 FunnelContext 序列化成一条 SLS 日志(8 个 step JSON 字段 + filter reasons)。
|
|
|
*/
|
|
|
public class FunnelAggregator {
|
|
|
|
|
|
- private static class Row {
|
|
|
- boolean inRecall;
|
|
|
- boolean inFiltered;
|
|
|
- double recallScore;
|
|
|
- int strategyRank;
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
- public static List<Map<String, String>> build(
|
|
|
- String traceId,
|
|
|
- RecommendRequest request,
|
|
|
- RecommendParam param,
|
|
|
- Map<String, List<FunnelRawItem>> funnelSink,
|
|
|
- RecallResult recallResult,
|
|
|
- RankResult rankResult,
|
|
|
- List<Video> returnedVideos) {
|
|
|
-
|
|
|
- // rank 阶段:被 mergeAndRankRovRecall 挑中进打分的视频 id 集合
|
|
|
- Set<Long> rankedSet = new HashSet<>();
|
|
|
- if (rankResult != null && rankResult.getCandidateVideoIds() != null) {
|
|
|
- rankedSet.addAll(rankResult.getCandidateVideoIds());
|
|
|
- }
|
|
|
+ // ===== 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;
|
|
|
+ }
|
|
|
|
|
|
- // rank 最终位置 + 最终 score (rankResult.videos 是 rank 截断前的整序列)
|
|
|
- Map<Long, Integer> rankPosMap = new HashMap<>();
|
|
|
- Map<Long, Double> rankScoreMap = new HashMap<>();
|
|
|
- if (rankResult != null && CollectionUtils.isNotEmpty(rankResult.getVideos())) {
|
|
|
- List<Video> rv = rankResult.getVideos();
|
|
|
- for (int i = 0; i < rv.size(); i++) {
|
|
|
- Video v = rv.get(i);
|
|
|
- rankPosMap.put(v.getVideoId(), i + 1);
|
|
|
- rankScoreMap.put(v.getVideoId(), v.getSortScore());
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- // preview = 截断后下发
|
|
|
- Map<Long, Integer> previewPosMap = new HashMap<>();
|
|
|
- if (CollectionUtils.isNotEmpty(returnedVideos)) {
|
|
|
- for (int i = 0; i < returnedVideos.size(); i++) {
|
|
|
- previewPosMap.put(returnedVideos.get(i).getVideoId(), i + 1);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- // (pushFrom -> videoId -> Row)
|
|
|
- Map<String, Map<Long, Row>> table = new LinkedHashMap<>();
|
|
|
-
|
|
|
- // raw
|
|
|
- if (funnelSink != null) {
|
|
|
- funnelSink.forEach((pushFrom, items) -> {
|
|
|
- Map<Long, Row> byVid = table.computeIfAbsent(pushFrom, k -> new LinkedHashMap<>());
|
|
|
- for (FunnelRawItem item : items) {
|
|
|
- Row r = byVid.computeIfAbsent(item.getVideoId(), k -> new Row());
|
|
|
- r.inRecall = true;
|
|
|
- r.recallScore = item.getScore();
|
|
|
- r.strategyRank = item.getStrategyRank();
|
|
|
- }
|
|
|
- });
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- // filtered
|
|
|
- if (recallResult != null && CollectionUtils.isNotEmpty(recallResult.getData())) {
|
|
|
- for (RecallResult.RecallData d : recallResult.getData()) {
|
|
|
- if (CollectionUtils.isEmpty(d.getVideos())) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- String pushFrom = d.getPushFrom();
|
|
|
- Map<Long, Row> byVid = table.computeIfAbsent(pushFrom, k -> new LinkedHashMap<>());
|
|
|
- for (int i = 0; i < d.getVideos().size(); i++) {
|
|
|
- Video v = d.getVideos().get(i);
|
|
|
- Row r = byVid.computeIfAbsent(v.getVideoId(), k -> new Row());
|
|
|
- r.inFiltered = true;
|
|
|
- // 兜底:strategy 不走 FilterService 时 raw 没记,从 Video 补
|
|
|
- if (!r.inRecall) {
|
|
|
- r.recallScore = v.getRovScore();
|
|
|
- r.strategyRank = i;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- List<Map<String, String>> rows = new ArrayList<>();
|
|
|
- Map<String, String> base = buildBase(traceId, request, param);
|
|
|
- table.forEach((pushFrom, byVid) -> byVid.forEach((vid, r) -> {
|
|
|
- Map<String, String> row = new HashMap<>(base);
|
|
|
- row.put("videoId", String.valueOf(vid));
|
|
|
- row.put("pushFrom", pushFrom);
|
|
|
- row.put("recallScore", String.valueOf(r.recallScore));
|
|
|
- row.put("strategyRank", String.valueOf(r.strategyRank));
|
|
|
- row.put("inRecall", r.inRecall ? "1" : "0");
|
|
|
- row.put("inFiltered", r.inFiltered ? "1" : "0");
|
|
|
- row.put("inRanked", rankedSet.contains(vid) ? "1" : "0");
|
|
|
- row.put("inPreview", previewPosMap.containsKey(vid) ? "1" : "0");
|
|
|
- row.put("rankScore", String.valueOf(rankScoreMap.getOrDefault(vid, 0.0)));
|
|
|
- row.put("rankPos", String.valueOf(rankPosMap.getOrDefault(vid, 0)));
|
|
|
- row.put("previewPos", String.valueOf(previewPosMap.getOrDefault(vid, 0)));
|
|
|
- rows.add(row);
|
|
|
- }));
|
|
|
- return rows;
|
|
|
+ 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 Map<String, String> buildBase(String traceId, RecommendRequest request, RecommendParam param) {
|
|
|
- Map<String, String> base = new HashMap<>();
|
|
|
- base.put("traceId", Strings.nullToEmpty(traceId));
|
|
|
- if (request != null) {
|
|
|
- base.put("recommendTraceId", Strings.nullToEmpty(request.getRecommendTraceId()));
|
|
|
- base.put("sessionId", Strings.nullToEmpty(request.getSessionId()));
|
|
|
- base.put("rootSessionId", Strings.nullToEmpty(request.getRootSessionId()));
|
|
|
- base.put("mid", Strings.nullToEmpty(request.getMid()));
|
|
|
- base.put("appType", String.valueOf(request.getAppType()));
|
|
|
- base.put("newexpgroup", Strings.nullToEmpty(request.getNewExpGroup()));
|
|
|
+ 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);
|
|
|
}
|
|
|
- if (param != null) {
|
|
|
- base.put("abExpCode", JSONUtils.toJson(param.getAbExpCodes()));
|
|
|
+ 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 base;
|
|
|
+ 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;
|
|
|
}
|
|
|
}
|