فهرست منبع

title 搜索改走 demand 池白名单 + 向量召回交集:仅 type∈{2 自营,3 代理} 且渠道∈{小程序投流-稳定,公众号投流-稳定} 放开,候选限 prior+posterior 池(demand_strategy∈(人群需求,优质相似))+rov≥0.02,小程序投流按 crowdPackage、公众号投流按 ghName 过滤;切掉管理平台关键词 fallback;排序向量 score DESC 兜底 sceneSumRov DESC;一次全量计算+按 pageNum/pageSize 切片返回。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
刘立冬 10 ساعت پیش
والد
کامیت
a36b486976

+ 12 - 0
api-module/src/main/java/com/tzld/piaoquan/api/dao/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.java

@@ -62,6 +62,18 @@ public interface ContentPlatformDemandVideoMapperExt {
 
     List<String> selectDistinctCrowdPackages(@Param("dt") String dt, @Param("channelName") String channelName);
 
+    /**
+     * 搜索候选白名单:dt 最新分区下,该入口(channel + 入口维度)所有 demand_strategy IN ('人群需求', '优质相似')
+     * 且 rov >= 0.02 的视频行。仅用于 title 搜索时与向量召回结果取交集。
+     * 不做组内 topK / 分位过滤(选项A:搜索语义是"在我能用的范围内找视频",非推荐排序)。
+     * 小程序投流:crowdSegment=#{crowdPackage},channelLevel3=null
+     * 公众号投流-稳定:crowdSegment=null,channelLevel3=#{ghName}
+     */
+    List<ContentPlatformDemandVideo> selectSearchWhitelist(@Param("dt") String dt,
+                                                            @Param("channelName") String channelName,
+                                                            @Param("crowdSegment") String crowdSegment,
+                                                            @Param("channelLevel3") String channelLevel3);
+
     List<ContentPlatformDemandVideo> selectActiveVideos(@Param("dt") String dt);
 
     int updateStatusByVideoId(@Param("videoId") Long videoId, @Param("dt") String dt, @Param("status") Integer status, @Param("updateTimestamp") Long updateTimestamp);

+ 101 - 2
api-module/src/main/java/com/tzld/piaoquan/api/service/contentplatform/impl/ContentPlatformPlanServiceImpl.java

@@ -646,8 +646,16 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     /** channel_name 映射:企微/小程序 type 直推,公众号入口按 ghName 反查 demand 表(见 resolveChannelName)。 */
     private static final String CHANNEL_NAME_QW           = "群/企微合作-稳定";
     private static final String CHANNEL_NAME_XCX          = "小程序投流-稳定";
+    private static final String CHANNEL_NAME_GZH_TOULIU   = "公众号投流-稳定";
     private static final String CHANNEL_NAME_GZH_JIZHUAN  = "公众号合作-即转-稳定";
     private static final String CHANNEL_NAME_GZH_DAILY    = "公众号合作-Daily-自选";
+    /** 搜索入口仅对这两个渠道放开:小程序投流(按人群包)+ 公众号投流-稳定(按公众号)。 */
+    private static final Set<String> CHANNELS_ALLOW_SEARCH = new HashSet<>(Arrays.asList(
+            CHANNEL_NAME_XCX, CHANNEL_NAME_GZH_TOULIU));
+    /** 搜索入口仅对 INTERNAL(2 自营)/ AGENT(3 代理) 类型账号放开。 */
+    private static final Set<Integer> USER_TYPES_ALLOW_SEARCH = new HashSet<>(Arrays.asList(2, 3));
+    /** 搜索向量召回 topN:取较大值,确保与 demand 白名单交集后仍有足量结果。 */
+    private static final int SEARCH_VECTOR_TOP_N = 500;
     /**
      * 合作类渠道:demand.crowd_segment 语义=合作方代码(yy/szhx/cdjh/...),与登录账号 ContentPlatformAccount.channel 同义,可作为过滤条件。
      * 其余渠道(投流-稳定/服务号投流):demand.crowd_segment 语义=人群分组标签(R50*xx/回流xx),与登录账号合作方无关,绝不可用 user.channel 过滤。
@@ -706,9 +714,9 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
     @Override
     public Page<VideoContentItemVO> getVideoContentList(VideoContentListParam param) {
         ContentPlatformAccount user = LoginUserContext.getUser();
-        // 如果 title 有内容,调用 manager 平台接口搜索
+        // title 非空 → 走 demand 池白名单 + 向量召回交集搜索(切掉管理平台关键词 fallback,只对 type∈{2,3} + 投流类渠道放开)
         if (StringUtils.hasText(param.getTitle())) {
-            return getVideoContentListByTitleV2(param);
+            return searchByTitleInDemandPool(param, user);
         }
         String source = param.getSource();
         if (SOURCE_PRIOR.equalsIgnoreCase(source)) {
@@ -2016,6 +2024,97 @@ public class ContentPlatformPlanServiceImpl implements ContentPlatformPlanServic
         return result;
     }
 
+    /**
+     * 搜索:在当前入口的 demand 白名单(prior+posterior 池,rov>=0.02)内对向量召回结果做交集 + 排序。
+     * 门控:
+     *   - 用户身份 type ∈ {2 自营, 3 代理},否则返回空
+     *   - 渠道 ∈ {小程序投流-稳定, 公众号投流-稳定},否则返回空
+     *   - 小程序投流必填 crowdPackage,公众号投流必填 ghName,否则返回空
+     * 流程:
+     *   1. demand 白名单(channel + crowdSegment/channelLevel3 + demand_strategy IN (人群需求,优质相似) + rov>=0.02)
+     *   2. 同 video_id 取 sceneSumRov 最大的代表行
+     *   3. 向量召回 topN=500
+     *   4. 交集:仅保留向量召回中出现在白名单的 video_id
+     *   5. 排序:向量 score DESC,缺失/相同时 sceneSumRov DESC
+     *   6. 一次性全量返回(不分页;白名单+召回交集量级小)
+     */
+    private Page<VideoContentItemVO> searchByTitleInDemandPool(VideoContentListParam param, ContentPlatformAccount user) {
+        Page<VideoContentItemVO> empty = new Page<>(param.getPageNum(), param.getPageSize());
+        empty.setTotalSize(0);
+        empty.setObjs(new ArrayList<>());
+
+        if (user == null || !USER_TYPES_ALLOW_SEARCH.contains(user.getType())) {
+            return empty;
+        }
+        String channelName = resolveChannelName(param);
+        if (channelName == null || !CHANNELS_ALLOW_SEARCH.contains(channelName)) {
+            return empty;
+        }
+        boolean isXcx = CHANNEL_NAME_XCX.equals(channelName);
+        String crowdPackage = StringUtils.hasText(param.getCrowdPackage()) ? param.getCrowdPackage() : null;
+        String ghName = StringUtils.hasText(param.getGhName()) ? param.getGhName() : null;
+        if (isXcx && crowdPackage == null) return empty;
+        if (!isXcx && ghName == null) return empty;
+
+        String dt = demandVideoMapperExt.getMaxDt(channelName);
+        if (!StringUtils.hasText(dt)) return empty;
+
+        List<ContentPlatformDemandVideo> whitelist = demandVideoMapperExt.selectSearchWhitelist(
+                dt, channelName, isXcx ? crowdPackage : null, isXcx ? null : ghName);
+        if (CollectionUtils.isEmpty(whitelist)) return empty;
+
+        // video_id 去重:取 sceneSumRov 最大的代表行
+        Map<Long, ContentPlatformDemandVideo> bestPerVideo = new HashMap<>();
+        for (ContentPlatformDemandVideo r : whitelist) {
+            if (r.getVideoId() == null) continue;
+            ContentPlatformDemandVideo cur = bestPerVideo.get(r.getVideoId());
+            double rSum = r.getSceneSumRov() == null ? 0d : r.getSceneSumRov();
+            double curSum = cur == null ? -1d : (cur.getSceneSumRov() == null ? 0d : cur.getSceneSumRov());
+            if (cur == null || rSum > curSum) {
+                bestPerVideo.put(r.getVideoId(), r);
+            }
+        }
+
+        JSONObject vectorData = managerApiService.recallVideoWithScore(param.getTitle(), SEARCH_VECTOR_TOP_N);
+        if (vectorData == null) return empty;
+        JSONArray items = vectorData.getJSONArray("items");
+        if (items == null || items.isEmpty()) return empty;
+
+        // 交集:按向量召回顺序遍历,留下 whitelist 命中的 video_id;同 video_id 去重
+        List<VideoContentItemVO> hits = new ArrayList<>();
+        Set<Long> seen = new HashSet<>();
+        for (int i = 0; i < items.size(); i++) {
+            JSONObject item = items.getJSONObject(i);
+            Long videoId = item.getLong("videoId");
+            if (videoId == null || !bestPerVideo.containsKey(videoId) || !seen.add(videoId)) continue;
+            ContentPlatformDemandVideo demand = bestPerVideo.get(videoId);
+            VideoContentItemVO vo = buildDemandVideoContentItemVOList(Collections.singletonList(demand)).get(0);
+            vo.setSearchSource("vector");
+            vo.setScore(item.getDouble("score"));  // 用向量 score 覆盖 demand.score,主排序键
+            hits.add(vo);
+        }
+
+        // 排序:向量 score DESC,缺失/相同时 sceneSumRov DESC
+        hits.sort((a, b) -> {
+            double sa = a.getScore() == null ? 0d : a.getScore();
+            double sb = b.getScore() == null ? 0d : b.getScore();
+            int c = Double.compare(sb, sa);
+            if (c != 0) return c;
+            double ra = a.getSceneSumRov() == null ? 0d : a.getSceneSumRov();
+            double rb = b.getSceneSumRov() == null ? 0d : b.getSceneSumRov();
+            return Double.compare(rb, ra);
+        });
+
+        Page<VideoContentItemVO> page = new Page<>(param.getPageNum(), param.getPageSize());
+        page.setTotalSize(hits.size());
+        // 全量在后端计算好,按 pageNum/pageSize 切片(单次召回,翻页不再调向量服务)
+        int pageSize = param.getPageSize() > 0 ? param.getPageSize() : 10;
+        int from = Math.min((param.getPageNum() - 1) * pageSize, hits.size());
+        int to = Math.min(param.getPageNum() * pageSize, hits.size());
+        page.setObjs(new ArrayList<>(hits.subList(from, to)));
+        return page;
+    }
+
     private List<VideoContentItemVO> buildDemandVideoContentItemVOList(List<ContentPlatformDemandVideo> videoList) {
         if (CollectionUtils.isEmpty(videoList)) {
             return new ArrayList<>();

+ 23 - 0
api-module/src/main/resources/mapper/contentplatform/ext/ContentPlatformDemandVideoMapperExt.xml

@@ -160,6 +160,29 @@
         LIMIT #{limit}
     </select>
 
+    <select id="selectSearchWhitelist" resultType="com.tzld.piaoquan.api.model.po.contentplatform.ContentPlatformDemandVideo">
+        SELECT id, dt, channel_name, channel_level3, crowd_segment, dimension, point_type, standard_element,
+               category_name, category, demand_id, crowd_package, conversion_target, partner, account, scene_value,
+               demand_strategy, drive_dimension_time, demand_filter_sort_strategy, demand_type,
+               demand_content_id, demand_content_title, demand_content_topic,
+               crowd_count, video_count, visit_uv, uv_ratio, total_rov, online_action, match_experiment_id,
+               match_method, match_video_filter, match_sort,
+               video_id, config_code, score, sim, rov,
+               match_text, title, cover, video, experiment_id, scene_sum_rov, status, create_timestamp, update_timestamp
+        FROM content_platform_demand_video
+        WHERE dt = #{dt} AND status = 1
+          AND channel_name = #{channelName}
+          AND demand_strategy IN ('人群需求', '优质相似')
+          AND rov IS NOT NULL AND rov &gt;= 0.02
+          AND video_id IS NOT NULL
+        <if test="crowdSegment != null and crowdSegment != ''">
+            AND crowd_segment = #{crowdSegment}
+        </if>
+        <if test="channelLevel3 != null and channelLevel3 != ''">
+            AND channel_level3 = #{channelLevel3}
+        </if>
+    </select>
+
     <select id="selectDistinctCrowdPackages" resultType="java.lang.String">
         SELECT DISTINCT
             CASE