Преглед на файлове

feat: V562/V565 各挂一路 dk_elements 召回 + RecallService 跟随 V566 模式收尾

新增 (3 文件):
- DkElementsUtils.parseElementKws: dk_elements JSON 串解析, 取 element kw 列表 (保 JSON 出现顺序)
- YearShareDkElementsRecallStrategy: 对齐 YearShareCate2 范式, 用户近期 share 行为 join
  videoBaseInfo.dk_elements 摊平, "最近 top-3 + 最频 top-3" 元素查 elements_ros_recall 倒排
- UserProfileDkElementsRecallStrategy: 用户元素画像 (alg_user_network_seq_feature 新增的
  s_z_y_s + zt_gyf), 按归一分 DESC 取正向 top-5, 上游正负向 UNION 后过滤 score<=0

V562/V565 各承载一个实验 (实验隔离便于独立归因):
- V562: rank PERSONAL 白名单 + RecallService 加法门 各加 YearShareDkElements 一路
- V565: 同上, 各加 UserProfileDkElements 一路

收尾上一个同步 commit (b8e3fc13) 遗留: RecallService 原 562/565 块的
add(all_rov 系列) + removeIf(老 region/city/priori) 跟 V566 cp 后的 NON_PERSONAL
白名单冲突 (rank 想消费的召回被提前剔除, add 的 all_rov 没人消费), 这次一起删掉.

Redis key elements_ros_recall:{原始元素} value vid1,vid2,...\\tscore1,score2,...
跟 UserDeconstructionKeywordsRecallStrategy / YearShareCate2 同款解析格式.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yangxiaohui преди 3 дни
родител
ревизия
8924312db6

+ 5 - 2
recommend-server-service/src/main/java/com/tzld/piaoquan/recommend/server/service/rank/strategy/RankStrategy4RegionMergeModelV562.java

@@ -37,7 +37,9 @@ public class RankStrategy4RegionMergeModelV562 extends RankStrategy4RegionMergeM
     private FeatureService featureService;
 
     /**
-     * V562 个性化召回白名单 (6 路):召回 key 含 mid/uid,依赖该用户行为信号。
+     * V562 个性化召回白名单 (7 路: V566 基础 6 路 + 1 路 dk_elements 行为路实验):召回 key 含 mid/uid,
+     * 依赖该用户行为信号。
+     * V562 实验路径: YearShareDkElements (用户近期 share 行为 join dk_elements)
      * 注:YearReturnCate2 因线上效果不佳, 2026-06-04 起移到非个性化白名单。
      */
     private static final Set<String> PERSONAL_RECALL_PUSH_FROMS = new HashSet<>(Arrays.asList(
@@ -46,7 +48,8 @@ public class RankStrategy4RegionMergeModelV562 extends RankStrategy4RegionMergeM
             Return1Cate2RosRecallStrategy.PUSH_FORM,
             Return1Cate2StrRecallStrategy.PUSH_FORM,
             YearShareCate1RecallStrategy.PUSH_FROM,
-            YearShareCate2RecallStrategy.PUSH_FROM
+            YearShareCate2RecallStrategy.PUSH_FROM,
+            YearShareDkElementsRecallStrategy.PUSH_FROM
     ));
 
     /**

+ 5 - 2
recommend-server-service/src/main/java/com/tzld/piaoquan/recommend/server/service/rank/strategy/RankStrategy4RegionMergeModelV565.java

@@ -37,7 +37,9 @@ public class RankStrategy4RegionMergeModelV565 extends RankStrategy4RegionMergeM
     private FeatureService featureService;
 
     /**
-     * V565 个性化召回白名单 (6 路):召回 key 含 mid/uid,依赖该用户行为信号。
+     * V565 个性化召回白名单 (7 路: V566 基础 6 路 + 1 路 dk_elements 画像路实验):召回 key 含 mid/uid,
+     * 依赖该用户行为信号。
+     * V565 实验路径: UserProfileDkElements (用户元素画像 s_z_y_s/zt_gyf)
      * 注:YearReturnCate2 因线上效果不佳, 2026-06-04 起移到非个性化白名单。
      */
     private static final Set<String> PERSONAL_RECALL_PUSH_FROMS = new HashSet<>(Arrays.asList(
@@ -46,7 +48,8 @@ public class RankStrategy4RegionMergeModelV565 extends RankStrategy4RegionMergeM
             Return1Cate2RosRecallStrategy.PUSH_FORM,
             Return1Cate2StrRecallStrategy.PUSH_FORM,
             YearShareCate1RecallStrategy.PUSH_FROM,
-            YearShareCate2RecallStrategy.PUSH_FROM
+            YearShareCate2RecallStrategy.PUSH_FROM,
+            UserProfileDkElementsRecallStrategy.PUSH_FROM
     ));
 
     /**

+ 9 - 25
recommend-server-service/src/main/java/com/tzld/piaoquan/recommend/server/service/recall/RecallService.java

@@ -175,36 +175,20 @@ public class RecallService implements ApplicationContextAware {
             strategies.add(strategyMap.get(UserDeconstructionKeywordsRecallStrategy.class.getSimpleName()));
         }
 
+        // V562/V565 同步成 V566 模式 (rank 类完全 cp 自 V566, 粗排分统一截断 + 老 region/city 白名单),
+        // 召回侧不再做"追加 all_rov + removeIf 老 region/city"的对调 —— 与 V564/V566 一致行为, 让公共池
+        // 22 路召回都跑, rank 类 extractAllAndTruncateByCoarseRank 按白名单 + 粗排分挑选.
+        //
+        // V562/V565 各承载一个 dk_elements 实验, 互相隔离便于独立归因:
+        //   V562 → YearShareDkElements:   用户近期 share 行为 join dk_elements -> elements_ros_recall 倒排
+        //   V565 → UserProfileDkElements: 用户元素画像 (s_z_y_s/zt_gyf)     -> elements_ros_recall 倒排
         boolean isHit562Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "562");
         if (isHit562Exp) {
-            strategies.add(strategyMap.get(RegionRealtimeRecallStrategyV1AllRov.class.getSimpleName()));
-            strategies.add(strategyMap.get(CityRovnAllRovRecallStrategy.class.getSimpleName()));
-            // V562: rank 侧用 all_rov 系列替代 region_1h + city_rovn, 这里直接剔除老召回避免无效 OSS/Redis 调用
-            Set<String> v562RemoveSet = new HashSet<>(Arrays.asList(
-                    RegionRealtimeRecallStrategyV1.class.getSimpleName(),
-                    CityRovnRecallStrategy.class.getSimpleName()
-            ));
-            strategies.removeIf(s -> s != null && v562RemoveSet.contains(s.getClass().getSimpleName()));
+            strategies.add(strategyMap.get(YearShareDkElementsRecallStrategy.class.getSimpleName()));
         }
-
         boolean isHit565Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "565");
         if (isHit565Exp) {
-            strategies.add(strategyMap.get(RegionRealtimeRecallStrategyV1AllRov.class.getSimpleName()));
-            strategies.add(strategyMap.get(CityRovnAllRovRecallStrategy.class.getSimpleName()));
-            // V565: all_rov 替代 region_1h + city_rovn, 额外剔除 5 路 region 旧召回 (rank 侧已删 extractOldSpecial) + 3 路 priori province
-            Set<String> v565RemoveSet = new HashSet<>(Arrays.asList(
-                    RegionRealtimeRecallStrategyV1.class.getSimpleName(),
-                    CityRovnRecallStrategy.class.getSimpleName(),
-                    RegionHRecallStrategy.class.getSimpleName(),
-                    Region24HRecallStrategy.class.getSimpleName(),
-                    RegionHDupRecallStrategy.class.getSimpleName(),
-                    RegionRelative24HRecallStrategy.class.getSimpleName(),
-                    RegionRelative24HDupRecallStrategy.class.getSimpleName(),
-                    PrioriProvinceRovnRecallStrategy.class.getSimpleName(),
-                    PrioriProvinceStrRecallStrategy.class.getSimpleName(),
-                    PrioriProvinceRosRecallStrategy.class.getSimpleName()
-            ));
-            strategies.removeIf(s -> s != null && v565RemoveSet.contains(s.getClass().getSimpleName()));
+            strategies.add(strategyMap.get(UserProfileDkElementsRecallStrategy.class.getSimpleName()));
         }
 
         // V564 实验:召回侧不做任何剔除/新增——让所有公共池召回都跑,

+ 169 - 0
recommend-server-service/src/main/java/com/tzld/piaoquan/recommend/server/service/recall/strategy/UserProfileDkElementsRecallStrategy.java

@@ -0,0 +1,169 @@
+package com.tzld.piaoquan.recommend.server.service.recall.strategy;
+
+import com.tzld.piaoquan.recommend.server.model.Video;
+import com.tzld.piaoquan.recommend.server.service.filter.FilterParam;
+import com.tzld.piaoquan.recommend.server.service.filter.FilterResult;
+import com.tzld.piaoquan.recommend.server.service.filter.FilterService;
+import com.tzld.piaoquan.recommend.server.service.recall.FilterParamFactory;
+import com.tzld.piaoquan.recommend.server.service.recall.RecallParam;
+import com.tzld.piaoquan.recommend.server.service.recall.RecallStrategy;
+import com.tzld.piaoquan.recommend.server.util.FeatureUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 用户画像 实质元素 ros 召回
+ *   数据源: param.userNetworkSeqFeature 里的 s_z_y_s (元素列表) + zt_gyf (归一分列表)
+ *           上游 alg_user_network_seq_feature 已新增, 来自 user_element_profile_hot
+ *           top_elements = UNION ALL(positive_ranked, negative_ranked), 所以归一分可能为负
+ *   逻辑: (element, score) pair 按 score DESC 取前 topN 正向元素 -> 查 elements_ros_recall 倒排
+ *   只取正向 (score > 0), 避免召回用户厌恶元素
+ *
+ *   跟 YearShareDkElementsRecallStrategy 共用 Redis 倒排 key, 仅用户兴趣源不同
+ */
+@Slf4j
+@Component
+public class UserProfileDkElementsRecallStrategy implements RecallStrategy {
+
+    @Autowired
+    @Qualifier("redisTemplate")
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private FilterService filterService;
+
+    private final String CLASS_NAME = this.getClass().getSimpleName();
+
+    public static final int topN = 5;
+    public static final String PUSH_FROM = "recall_user_profile_dk_elements";
+    public static final String redisKeyPrefix = "elements_ros_recall";
+
+    public static final String KEY_ELEMENTS = "s_z_y_s";
+    public static final String KEY_SCORES = "zt_gyf";
+
+    @Override
+    public String pushFrom() {
+        return PUSH_FROM;
+    }
+
+    @Override
+    public List<Video> recall(RecallParam param) {
+        List<Video> videosResult = new ArrayList<>();
+        try {
+            if (MapUtils.isEmpty(param.getUserNetworkSeqFeature())) {
+                return videosResult;
+            }
+
+            List<String> elements = FeatureUtils.extractVidsFromUserNetworkSeqFeature(param.getUserNetworkSeqFeature(), KEY_ELEMENTS);
+            List<String> scores = FeatureUtils.extractVidsFromUserNetworkSeqFeature(param.getUserNetworkSeqFeature(), KEY_SCORES);
+            if (CollectionUtils.isEmpty(elements) || elements.size() != scores.size()) {
+                return videosResult;
+            }
+
+            List<String> topElements = pickTopPositiveElements(elements, scores);
+            if (CollectionUtils.isEmpty(topElements)) {
+                return videosResult;
+            }
+
+            List<String> keys = getRedisKey(topElements);
+            List<String> values = redisTemplate.opsForValue().multiGet(keys);
+            List<Long> ids = recall(param.getVideoId(), values);
+
+            Map<Long, Double> scoresMap = FilterParamFactory.positionScores(ids);
+            FilterParam filterParam = FilterParamFactory.create(param, ids, pushFrom(), scoresMap);
+            FilterResult filterResult = filterService.filter(filterParam);
+            if (filterResult != null && CollectionUtils.isNotEmpty(filterResult.getVideoIds())) {
+                for (Long vid : filterResult.getVideoIds()) {
+                    Video video = new Video();
+                    video.setVideoId(vid);
+                    video.setRovScore(scoresMap.getOrDefault(vid, 0.0));
+                    video.setPushFrom(pushFrom());
+                    videosResult.add(video);
+                }
+            }
+        } catch (Exception e) {
+            log.error("recall is wrong in {}, error={}", CLASS_NAME, e);
+        }
+        return videosResult;
+    }
+
+    /** 组对 + 过滤负向 + 按归一分降序 + 取前 topN 个 element */
+    private List<String> pickTopPositiveElements(List<String> elements, List<String> scoreStrs) {
+        List<Pair<String, Double>> pairs = new ArrayList<>();
+        for (int i = 0; i < elements.size(); i++) {
+            String element = elements.get(i);
+            if (StringUtils.isBlank(element)) {
+                continue;
+            }
+            double score = NumberUtils.toDouble(scoreStrs.get(i), 0.0);
+            if (score <= 0) {
+                continue;
+            }
+            pairs.add(Pair.of(element, score));
+        }
+        if (pairs.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return pairs.stream()
+                .sorted(Comparator.comparingDouble((Pair<String, Double> p) -> p.getValue()).reversed())
+                .map(Pair::getKey)
+                .distinct()
+                .limit(topN)
+                .collect(Collectors.toList());
+    }
+
+    private List<String> getRedisKey(List<String> elementList) {
+        List<String> keys = new ArrayList<>();
+        for (String element : elementList) {
+            keys.add(String.format("%s:%s", redisKeyPrefix, element));
+        }
+        return keys;
+    }
+
+    private List<Long> recall(Long headVid, List<String> values) {
+        List<Long> vidList = new ArrayList<>();
+        if (null != values && !values.isEmpty()) {
+            Set<Long> hits = new HashSet<>();
+            hits.add(headVid);
+            List<org.apache.commons.math3.util.Pair<Long, Double>> list = new ArrayList<>();
+            for (String value : values) {
+                if (null != value && !value.isEmpty()) {
+                    String[] cells = value.split("\t");
+                    if (2 == cells.length) {
+                        List<Long> ids = Arrays.stream(cells[0].split(",")).map(Long::valueOf).collect(Collectors.toList());
+                        List<Double> scores = Arrays.stream(cells[1].split(",")).map(Double::valueOf).collect(Collectors.toList());
+                        if (!ids.isEmpty() && ids.size() == scores.size()) {
+                            for (int i = 0; i < ids.size(); ++i) {
+                                long id = ids.get(i);
+                                double score = scores.get(i);
+                                if (hits.contains(id)) {
+                                    continue;
+                                }
+                                hits.add(id);
+                                list.add(org.apache.commons.math3.util.Pair.create(id, score));
+                            }
+                        }
+                    }
+                }
+            }
+            if (!list.isEmpty()) {
+                list.sort(Comparator.comparingDouble(o -> -o.getSecond()));
+                for (org.apache.commons.math3.util.Pair<Long, Double> pair : list) {
+                    vidList.add(pair.getFirst());
+                }
+            }
+        }
+        return vidList;
+    }
+}

+ 195 - 0
recommend-server-service/src/main/java/com/tzld/piaoquan/recommend/server/service/recall/strategy/YearShareDkElementsRecallStrategy.java

@@ -0,0 +1,195 @@
+package com.tzld.piaoquan.recommend.server.service.recall.strategy;
+
+import com.tzld.piaoquan.recommend.server.model.Video;
+import com.tzld.piaoquan.recommend.server.service.filter.FilterParam;
+import com.tzld.piaoquan.recommend.server.service.filter.FilterResult;
+import com.tzld.piaoquan.recommend.server.service.filter.FilterService;
+import com.tzld.piaoquan.recommend.server.service.recall.FilterParamFactory;
+import com.tzld.piaoquan.recommend.server.service.recall.RecallParam;
+import com.tzld.piaoquan.recommend.server.service.recall.RecallStrategy;
+import com.tzld.piaoquan.recommend.server.util.DkElementsUtils;
+import com.tzld.piaoquan.recommend.server.util.FeatureUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 视频解构 实质元素 ros 召回 (用户近期 share 行为 -> dk_elements)
+ *   范式: 完全对齐 YearShareCate2RecallStrategy, 把"取 merge_second_level_cate"换成"取 dk_elements 摊平"
+ *   每个 share vid 一般有多个 element, parseUserActionVideoAndElements 返回扁平的 (vid, element) pair 列表
+ *
+ *   上游 ODPS: alg_recsys_recall_elements_ros (原始元素 -> top-N vid + ros 得分)
+ *   Redis key: elements_ros_recall:{原始元素}
+ *   value: vid1,vid2,...\tscore1,score2,...
+ */
+@Slf4j
+@Component
+public class YearShareDkElementsRecallStrategy implements RecallStrategy {
+
+    @Autowired
+    @Qualifier("redisTemplate")
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private FilterService filterService;
+
+    private final String CLASS_NAME = this.getClass().getSimpleName();
+
+    public static final String PUSH_FROM = "recall_user_year_share_dk_elements";
+    public static final String redisKeyPrefix = "elements_ros_recall";
+
+    @Override
+    public List<Video> recall(RecallParam param) {
+
+        List<Video> videosResult = new ArrayList<>();
+        try {
+
+            if (MapUtils.isEmpty(param.getUserNetworkSeqVideoInfoMap())) {
+                return videosResult;
+            }
+
+            List<Pair<Long, String>> userNetworkVideoElement = this.parseUserActionVideoAndElements(param.getUserNetworkSeqFeature(), param.getUserNetworkSeqVideoInfoMap());
+            if (CollectionUtils.isEmpty(userNetworkVideoElement)) {
+                return videosResult;
+            }
+            int limit = Math.min(userNetworkVideoElement.size(), 3);
+            List<String> lastTopNElement = userNetworkVideoElement.stream()
+                    .map(Pair::getValue)
+                    .distinct()
+                    .limit(limit)
+                    .collect(Collectors.toList());
+
+            List<String> freqTopNElement = userNetworkVideoElement.stream()
+                    .map(Pair::getValue)
+                    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).entrySet()
+                    .stream()
+                    .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
+                    .limit(limit)
+                    .map(Map.Entry::getKey)
+                    .collect(Collectors.toList());
+
+
+            List<String> allElements = Stream.of(lastTopNElement, freqTopNElement)
+                    .flatMap(Collection::stream)
+                    .distinct()
+                    .filter(StringUtils::isNotBlank)
+                    .collect(Collectors.toList());
+
+            List<String> keys = this.getRedisKey(allElements);
+            List<String> values = redisTemplate.opsForValue().multiGet(keys);
+            List<Long> ids = recall(param.getVideoId(), values);
+
+            Map<Long, Double> scoresMap = FilterParamFactory.positionScores(ids);
+            FilterParam filterParam = FilterParamFactory.create(param, ids, pushFrom(), scoresMap);
+            FilterResult filterResult = filterService.filter(filterParam);
+            if (filterResult != null && CollectionUtils.isNotEmpty(filterResult.getVideoIds())) {
+                for (Long vid : filterResult.getVideoIds()) {
+                    Video video = new Video();
+                    video.setVideoId(vid);
+                    video.setRovScore(scoresMap.getOrDefault(vid, 0.0));
+                    video.setPushFrom(pushFrom());
+                    videosResult.add(video);
+                }
+            }
+        } catch (Exception e) {
+            log.error("recall is wrong in {}, error={}", CLASS_NAME, e);
+        }
+
+        return videosResult;
+    }
+
+    /**
+     * 摊平: 每个 share vid 一般有多个 dk_element, 输出 (vid, element) pair 序列, 按 vid 时间序保留
+     */
+    private List<Pair<Long, String>> parseUserActionVideoAndElements(Map<String, String> userNetworkSeqFeature, Map<Long, Map<String, String>> userNetworkSeqVideoInfoMap) {
+        List<Pair<Long, String>> result = new ArrayList<>();
+        List<String> actVidSeq = FeatureUtils.extractVidsFromUserNetworkSeqFeature(userNetworkSeqFeature, "a_v_s");
+        List<String> actTypeSeq = FeatureUtils.extractVidsFromUserNetworkSeqFeature(userNetworkSeqFeature, "a_t_s");
+        if (actVidSeq.size() != actTypeSeq.size()) {
+            return new ArrayList<>();
+        }
+
+        for (int i = 0; i < actVidSeq.size(); i++) {
+            long videoIdL = NumberUtils.toLong(actVidSeq.get(i), -1);
+            if (videoIdL <= 0) {
+                continue;
+            }
+            String type = actTypeSeq.get(i);
+            if (!"share".equals(type)) {
+                continue;
+            }
+
+            Map<String, String> videoBaseInfo = userNetworkSeqVideoInfoMap.getOrDefault(videoIdL, new HashMap<>());
+            String dkElementsStr = videoBaseInfo.get("dk_elements");
+            if (StringUtils.isBlank(dkElementsStr)) {
+                continue;
+            }
+            List<String> kws = DkElementsUtils.parseElementKws(dkElementsStr);
+            for (String kw : kws) {
+                result.add(Pair.of(videoIdL, kw));
+            }
+        }
+        return result;
+    }
+
+    private List<String> getRedisKey(List<String> elementList) {
+        List<String> keys = new ArrayList<>();
+        for (String element : elementList) {
+            keys.add(String.format("%s:%s", redisKeyPrefix, element));
+        }
+        return keys;
+    }
+
+    private List<Long> recall(Long headVid, List<String> values) {
+        List<Long> vidList = new ArrayList<>();
+        if (null != values && !values.isEmpty()) {
+            Set<Long> hits = new HashSet<>();
+            hits.add(headVid);
+            List<org.apache.commons.math3.util.Pair<Long, Double>> list = new ArrayList<>();
+            for (String value : values) {
+                if (null != value && !value.isEmpty()) {
+                    String[] cells = value.split("\t");
+                    if (2 == cells.length) {
+                        List<Long> ids = Arrays.stream(cells[0].split(",")).map(Long::valueOf).collect(Collectors.toList());
+                        List<Double> scores = Arrays.stream(cells[1].split(",")).map(Double::valueOf).collect(Collectors.toList());
+                        if (!ids.isEmpty() && ids.size() == scores.size()) {
+                            for (int i = 0; i < ids.size(); ++i) {
+                                long id = ids.get(i);
+                                double score = scores.get(i);
+                                if (hits.contains(id)) {
+                                    continue;
+                                }
+                                hits.add(id);
+                                list.add(org.apache.commons.math3.util.Pair.create(id, score));
+                            }
+                        }
+                    }
+                }
+            }
+            if (!list.isEmpty()) {
+                list.sort(Comparator.comparingDouble(o -> -o.getSecond()));
+                for (org.apache.commons.math3.util.Pair<Long, Double> pair : list) {
+                    vidList.add(pair.getFirst());
+                }
+            }
+        }
+        return vidList;
+    }
+
+    @Override
+    public String pushFrom() {
+        return PUSH_FROM;
+    }
+}

+ 50 - 0
recommend-server-service/src/main/java/com/tzld/piaoquan/recommend/server/util/DkElementsUtils.java

@@ -0,0 +1,50 @@
+package com.tzld.piaoquan.recommend.server.util;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * dk_elements JSON 串解析
+ * value 形如 {"党的二十大":{"d":"事件","p":"主题","c":2.1},"民生":{"d":"事件","p":"主题","c":1.4}}
+ *
+ * 当前只需要 element kw 列表 (LinkedHashMap 保 JSON 出现顺序), 未来若要 c 加权再扩展
+ * kw 清理跟 dk_keywords 老逻辑对齐: 去掉空白/制表/冒号
+ */
+@Slf4j
+public class DkElementsUtils {
+
+    public static List<String> parseElementKws(String dkElementsStr) {
+        if (StringUtils.isBlank(dkElementsStr)) {
+            return Collections.emptyList();
+        }
+        try {
+            JSONObject obj = JSONObject.parseObject(dkElementsStr);
+            if (obj == null || obj.isEmpty()) {
+                return Collections.emptyList();
+            }
+            List<String> kws = new ArrayList<>(obj.size());
+            for (String raw : obj.keySet()) {
+                String kw = cleanKw(raw);
+                if (!kw.isEmpty()) {
+                    kws.add(kw);
+                }
+            }
+            return kws;
+        } catch (Exception e) {
+            log.error("parseElementKws error, value=[{}]", dkElementsStr, e);
+            return Collections.emptyList();
+        }
+    }
+
+    private static String cleanKw(String kw) {
+        if (kw == null) {
+            return "";
+        }
+        return kw.replaceAll("(\\s+|\\t|:)", "");
+    }
+}