|
|
@@ -6,22 +6,28 @@ import com.alibaba.fastjson.JSONObject;
|
|
|
import com.tzld.videoVector.api.VideoApiService;
|
|
|
import com.tzld.videoVector.common.constant.VectorConstants;
|
|
|
import com.tzld.videoVector.common.enums.Modality;
|
|
|
-import com.tzld.videoVector.dao.mapper.videoVector.deconstruct.MysqlDeconstructContentMapper;
|
|
|
-import com.tzld.videoVector.dao.mapper.pgVector.ext.VideoDeconstructResultMapperExt;
|
|
|
+import com.tzld.videoVector.dao.mapper.pgVector.DeconstructVectorConfigMapper;
|
|
|
+import com.tzld.videoVector.dao.mapper.pgVector.ext.MaterialDeconstructResultMapperExt;
|
|
|
+import com.tzld.videoVector.model.entity.MaterialMatch;
|
|
|
import com.tzld.videoVector.model.entity.VideoDetail;
|
|
|
import com.tzld.videoVector.model.param.MatchTopNVideoParam;
|
|
|
import com.tzld.videoVector.model.param.recall.MatchByTextParam;
|
|
|
import com.tzld.videoVector.model.param.recall.MatchByVideoIdParam;
|
|
|
-import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContent;
|
|
|
-import com.tzld.videoVector.model.po.videoVector.deconstruct.MysqlDeconstructContentExample;
|
|
|
+import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
|
|
|
+import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfigExample;
|
|
|
+import com.tzld.videoVector.model.po.pgVector.MaterialDeconstructResult;
|
|
|
import com.tzld.videoVector.model.vo.VideoMatchResult;
|
|
|
import com.tzld.videoVector.model.vo.recall.AIUnderstandingVO;
|
|
|
import com.tzld.videoVector.model.vo.recall.DeconstructPointsVO;
|
|
|
+import com.tzld.videoVector.model.vo.recall.MaterialDetailVO;
|
|
|
import com.tzld.videoVector.model.vo.recall.RecallResultVO;
|
|
|
import com.tzld.videoVector.model.vo.recall.VideoBasicVO;
|
|
|
import com.tzld.videoVector.model.vo.recall.VideoMatchEnrichedVO;
|
|
|
+import com.tzld.videoVector.service.EmbeddingService;
|
|
|
+import com.tzld.videoVector.service.MaterialVectorStoreService;
|
|
|
import com.tzld.videoVector.service.VideoSearchService;
|
|
|
import com.tzld.videoVector.service.recall.VectorRecallTestService;
|
|
|
+import com.tzld.videoVector.util.Md5Util;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
@@ -29,14 +35,21 @@ import org.springframework.stereotype.Service;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
|
import org.springframework.util.StringUtils;
|
|
|
|
|
|
+import javax.annotation.PreDestroy;
|
|
|
import javax.annotation.Resource;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
|
import java.util.HashMap;
|
|
|
-import java.util.HashSet;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
+import java.util.Comparator;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.LinkedBlockingQueue;
|
|
|
+import java.util.concurrent.ThreadPoolExecutor;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
@@ -54,14 +67,47 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
@Resource
|
|
|
private VideoApiService videoApiService;
|
|
|
|
|
|
+ @Autowired(required = false)
|
|
|
+ private StringRedisTemplate stringRedisTemplate;
|
|
|
+
|
|
|
@Autowired
|
|
|
- private MysqlDeconstructContentMapper mysqlDeconstructContentMapper;
|
|
|
+ private MaterialVectorStoreService materialVectorStoreService;
|
|
|
|
|
|
@Autowired
|
|
|
- private VideoDeconstructResultMapperExt videoDeconstructResultMapperExt;
|
|
|
+ private MaterialDeconstructResultMapperExt materialDeconstructResultMapperExt;
|
|
|
|
|
|
- @Autowired(required = false)
|
|
|
- private StringRedisTemplate stringRedisTemplate;
|
|
|
+ @Autowired
|
|
|
+ private DeconstructVectorConfigMapper deconstructVectorConfigMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private EmbeddingService embeddingService;
|
|
|
+
|
|
|
+ private static final String SOURCE_AIGC = "aigc_deconstruct";
|
|
|
+
|
|
|
+ /** source_type → 中文来源标签 */
|
|
|
+ private static final short SOURCE_TYPE_EXTERNAL = 1;
|
|
|
+ private static final short SOURCE_TYPE_INTERNAL = 2;
|
|
|
+ private static final String SOURCE_LABEL_EXTERNAL = "外部合作";
|
|
|
+ private static final String SOURCE_LABEL_INTERNAL = "内部素材";
|
|
|
+
|
|
|
+ /** 并行召回线程池:视频和素材召回独立异步执行 */
|
|
|
+ private static final ExecutorService RECALL_EXECUTOR = new ThreadPoolExecutor(
|
|
|
+ 8, 16, 60L, TimeUnit.SECONDS,
|
|
|
+ new LinkedBlockingQueue<>(128),
|
|
|
+ new ThreadPoolExecutor.CallerRunsPolicy());
|
|
|
+
|
|
|
+ @PreDestroy
|
|
|
+ public void shutdownRecallExecutor() {
|
|
|
+ RECALL_EXECUTOR.shutdown();
|
|
|
+ try {
|
|
|
+ if (!RECALL_EXECUTOR.awaitTermination(10, TimeUnit.SECONDS)) {
|
|
|
+ RECALL_EXECUTOR.shutdownNow();
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ RECALL_EXECUTOR.shutdownNow();
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
private static final String PLACEHOLDER = "--";
|
|
|
|
|
|
@@ -100,19 +146,581 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
return empty;
|
|
|
}
|
|
|
|
|
|
- // 1. 调用现有召回 Service
|
|
|
- MatchTopNVideoParam matchParam = new MatchTopNVideoParam();
|
|
|
- matchParam.setQueryText(param.getQueryText());
|
|
|
- matchParam.setConfigCode(param.getConfigCode());
|
|
|
- matchParam.setTopN(param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 10);
|
|
|
+ int defaultTopN = param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 50;
|
|
|
+ int videoTopN = param.getVideoTopN() != null && param.getVideoTopN() > 0 ? param.getVideoTopN() : defaultTopN;
|
|
|
+ int materialTopN = param.getMaterialTopN() != null && param.getMaterialTopN() > 0
|
|
|
+ ? param.getMaterialTopN() : defaultTopN;
|
|
|
+ String configCode = StringUtils.hasText(param.getConfigCode())
|
|
|
+ ? param.getConfigCode() : VectorConstants.DEFAULT_CONFIG_CODE;
|
|
|
+
|
|
|
+ // 并行召回:视频、素材各自独立 topN
|
|
|
+ final int finalVideoTopN = videoTopN;
|
|
|
+ final int finalMaterialTopN = materialTopN;
|
|
|
+ final String finalConfigCode = configCode;
|
|
|
+ CompletableFuture<List<VideoMatchResult>> videoFuture = CompletableFuture.supplyAsync(() -> {
|
|
|
+ try {
|
|
|
+ MatchTopNVideoParam videoParam = new MatchTopNVideoParam();
|
|
|
+ videoParam.setQueryText(param.getQueryText());
|
|
|
+ videoParam.setConfigCode(configCode);
|
|
|
+ videoParam.setTopN(finalVideoTopN);
|
|
|
+ List<VideoMatchResult> matches = videoSearchService.matchTopNVideo(videoParam, true);
|
|
|
+ return limitVideoMatchesByScore(matches, finalVideoTopN);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("视频召回失败 queryText={}, error={}", param.getQueryText(), e.getMessage(), e);
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }, RECALL_EXECUTOR);
|
|
|
|
|
|
- List<VideoMatchResult> rawMatches = videoSearchService.matchTopNVideo(matchParam, true);
|
|
|
- if (CollectionUtils.isEmpty(rawMatches)) {
|
|
|
- return empty;
|
|
|
+ CompletableFuture<List<VideoMatchEnrichedVO>> materialFuture = CompletableFuture.supplyAsync(
|
|
|
+ () -> recallMaterialItems(param.getQueryText(), finalConfigCode, finalMaterialTopN),
|
|
|
+ RECALL_EXECUTOR);
|
|
|
+
|
|
|
+ List<VideoMatchResult> videoMatches;
|
|
|
+ List<VideoMatchEnrichedVO> materialItems;
|
|
|
+ try {
|
|
|
+ videoMatches = videoFuture.get(30, TimeUnit.SECONDS);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("视频召回等待超时/异常: {}", e.getMessage(), e);
|
|
|
+ videoMatches = Collections.emptyList();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ materialItems = materialFuture.get(30, TimeUnit.SECONDS);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("素材召回等待超时/异常: {}", e.getMessage(), e);
|
|
|
+ materialItems = Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<VideoMatchEnrichedVO> videoItems = enrichVideoMatches(videoMatches, configCode);
|
|
|
+ return buildResult(videoItems, materialItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VideoMatchResult> limitVideoMatchesByScore(List<VideoMatchResult> matches, int topN) {
|
|
|
+ if (CollectionUtils.isEmpty(matches) || topN <= 0 || matches.size() <= topN) {
|
|
|
+ return matches == null ? Collections.emptyList() : matches;
|
|
|
+ }
|
|
|
+ return matches.stream()
|
|
|
+ .sorted(Comparator.comparing(VideoMatchResult::getScore,
|
|
|
+ Comparator.nullsLast(Comparator.reverseOrder())))
|
|
|
+ .limit(topN)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<VideoMatchEnrichedVO> limitEnrichedItemsByScore(List<VideoMatchEnrichedVO> items, int topN) {
|
|
|
+ if (CollectionUtils.isEmpty(items) || topN <= 0 || items.size() <= topN) {
|
|
|
+ return items == null ? Collections.emptyList() : items;
|
|
|
+ }
|
|
|
+ return items.stream()
|
|
|
+ .sorted(Comparator.comparing(VideoMatchEnrichedVO::getScore,
|
|
|
+ Comparator.nullsLast(Comparator.reverseOrder())))
|
|
|
+ .limit(topN)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 素材文本召回:material_vectors → material_deconstruct_result
|
|
|
+ */
|
|
|
+ private List<VideoMatchEnrichedVO> recallMaterialItems(String queryText, String configCode, int topN) {
|
|
|
+ try {
|
|
|
+ int candidate = Math.max(topN * VectorConstants.MULTI_POINT_RECALL_CANDIDATE_FACTOR,
|
|
|
+ VectorConstants.MULTI_POINT_RECALL_MIN_CANDIDATES);
|
|
|
+
|
|
|
+ // 优先尝试 text_hash 缓存:直接用 PG 返回的原始 embedding 字符串搜索,
|
|
|
+ // 绕过 Java Float.parseFloat/Float.toString 回环的精度损失
|
|
|
+ String textHash = Md5Util.encoderByMd5(queryText);
|
|
|
+ if (StringUtils.hasText(textHash)) {
|
|
|
+ String rawVector = materialVectorStoreService.getRawVectorByTextHash(textHash, configCode);
|
|
|
+ if (rawVector != null && !rawVector.isEmpty()) {
|
|
|
+ log.info("素材召回 使用缓存的原始向量字符串,跳过 Float 回环, configCode={}", configCode);
|
|
|
+ List<MaterialMatch> raw = materialVectorStoreService.searchTopNByRawVector(
|
|
|
+ configCode, rawVector, candidate);
|
|
|
+ List<MaterialMatch> matches = deduplicateMaterialMatches(raw, topN);
|
|
|
+ if (!CollectionUtils.isEmpty(matches)) {
|
|
|
+ List<String> matchSample = new ArrayList<>();
|
|
|
+ for (MaterialMatch m : matches) {
|
|
|
+ matchSample.add(m.getMaterialId() + ":" + String.format("%.4f", m.getScore()));
|
|
|
+ }
|
|
|
+ log.info("素材召回(rawVector) 去重后({}条): {}, configCode={}",
|
|
|
+ matches.size(), matchSample, configCode);
|
|
|
+ return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode), topN);
|
|
|
+ }
|
|
|
+ log.info("素材召回(rawVector) 无结果, configCode={}", configCode);
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ log.info("素材召回 text_hash 缓存未命中, textHash={}, 降级到 embedding API", textHash);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 降级:embedding API → Float 向量 → 搜索(非缓存路径,容忍精度损失)
|
|
|
+ List<Float> queryVector = resolveQueryVectorForMaterial(queryText, configCode);
|
|
|
+ if (queryVector == null || queryVector.isEmpty()) {
|
|
|
+ log.info("素材召回: 无法获取查询向量, queryText={}", queryText);
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ log.info("素材召回 使用 embedding API 向量, dim={}", queryVector.size());
|
|
|
+ List<MaterialMatch> raw = materialVectorStoreService.searchTopN(configCode, queryVector, candidate);
|
|
|
+ List<MaterialMatch> matches = deduplicateMaterialMatches(raw, topN);
|
|
|
+ if (CollectionUtils.isEmpty(matches)) {
|
|
|
+ log.info("素材召回 material_vectors 无结果, configCode={}", configCode);
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<String> matchSample = new ArrayList<>();
|
|
|
+ for (MaterialMatch m : matches) {
|
|
|
+ matchSample.add(m.getMaterialId() + ":" + String.format("%.4f", m.getScore()));
|
|
|
+ }
|
|
|
+ log.info("素材召回(embedding API) 去重后({}条): {}, configCode={}", matches.size(), matchSample, configCode);
|
|
|
+ return limitEnrichedItemsByScore(enrichMaterialMatches(matches, configCode), topN);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("素材召回 material_vectors 异常: {}", e.getMessage(), e);
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为素材召回单独解析查询向量(复用 text_hash 缓存 + embedding API)
|
|
|
+ */
|
|
|
+ private List<Float> resolveQueryVectorForMaterial(String queryText, String configCode) {
|
|
|
+ if (!StringUtils.hasText(queryText)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ DeconstructVectorConfig config = getVectorConfigByCode(configCode);
|
|
|
+ if (config == null) {
|
|
|
+ config = new DeconstructVectorConfig();
|
|
|
+ config.setConfigCode(configCode);
|
|
|
+ }
|
|
|
+ log.info("resolveQueryVectorForMaterial: queryText={}, configCode={}, model={}, dim={}",
|
|
|
+ queryText, configCode, config.getEmbeddingModel(), config.getDimension());
|
|
|
+
|
|
|
+ // 1. 先查 material_vectors 的 text_hash 缓存
|
|
|
+ String textHash = Md5Util.encoderByMd5(queryText);
|
|
|
+ if (StringUtils.hasText(textHash)) {
|
|
|
+ log.info("resolveQueryVectorForMaterial textHash={}, 开始查 text_hash 缓存", textHash);
|
|
|
+ List<Float> cached = materialVectorStoreService.getVectorByTextHash(textHash, configCode);
|
|
|
+ if (cached != null && !cached.isEmpty()) {
|
|
|
+ log.info("resolveQueryVectorForMaterial 命中 text_hash 缓存,dim={}", cached.size());
|
|
|
+ return cached;
|
|
|
+ }
|
|
|
+ log.info("resolveQueryVectorForMaterial text_hash 缓存未命中,降级到 embedding API");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 调用 embedding API(与入库时相同的 model / dimension)
|
|
|
+ try {
|
|
|
+ log.info("resolveQueryVectorForMaterial 调用 embedding API: text={}, model={}, dim={}",
|
|
|
+ queryText, config.getEmbeddingModel(), config.getDimension());
|
|
|
+ List<Float> result = embeddingService.embed(queryText, config);
|
|
|
+ log.info("resolveQueryVectorForMaterial embedding API 返回, dim={}", result != null ? result.size() : 0);
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("素材召回 embedding 失败: queryText={}, error={}", queryText, e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 多点向量去重:同一 materialId 保留最高分
|
|
|
+ */
|
|
|
+ private List<MaterialMatch> deduplicateMaterialMatches(List<MaterialMatch> matches, int topN) {
|
|
|
+ if (CollectionUtils.isEmpty(matches)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ Map<String, MaterialMatch> deduped = new LinkedHashMap<>();
|
|
|
+ for (MaterialMatch m : matches) {
|
|
|
+ if (m == null || !StringUtils.hasText(m.getMaterialId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ MaterialMatch existing = deduped.get(m.getMaterialId());
|
|
|
+ if (existing == null || m.getScore() > existing.getScore()) {
|
|
|
+ deduped.put(m.getMaterialId(), m);
|
|
|
+ }
|
|
|
}
|
|
|
+ return deduped.values().stream().limit(topN).collect(Collectors.toList());
|
|
|
+ }
|
|
|
|
|
|
- // 2. 解析并 enrich
|
|
|
- return enrich(rawMatches);
|
|
|
+ private DeconstructVectorConfig getVectorConfigByCode(String configCode) {
|
|
|
+ if (!StringUtils.hasText(configCode)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ DeconstructVectorConfigExample example = new DeconstructVectorConfigExample();
|
|
|
+ example.createCriteria().andConfigCodeEqualTo(configCode);
|
|
|
+ List<DeconstructVectorConfig> configs = deconstructVectorConfigMapper.selectByExample(example);
|
|
|
+ if (!CollectionUtils.isEmpty(configs)) {
|
|
|
+ return configs.get(0);
|
|
|
+ }
|
|
|
+ log.info("未找到 configCode={} 的向量配置,将使用默认 embedding 参数", configCode);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询向量配置失败,configCode={}, error={}", configCode, e.getMessage(), e);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 视频召回结果 enrich
|
|
|
+ */
|
|
|
+ private List<VideoMatchEnrichedVO> enrichVideoMatches(List<VideoMatchResult> matches, String requestConfigCode) {
|
|
|
+ if (CollectionUtils.isEmpty(matches)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ Set<Long> videoIds = matches.stream()
|
|
|
+ .map(VideoMatchResult::getVideoId)
|
|
|
+ .filter(java.util.Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+
|
|
|
+ Map<Long, VideoDetail> videoDetails = videoIds.isEmpty()
|
|
|
+ ? Collections.emptyMap()
|
|
|
+ : videoApiService.getVideoDetail(videoIds);
|
|
|
+
|
|
|
+ List<VideoMatchEnrichedVO> items = new ArrayList<>(matches.size());
|
|
|
+ for (VideoMatchResult m : matches) {
|
|
|
+ if (m == null || m.getVideoId() == null) continue;
|
|
|
+
|
|
|
+ VideoMatchEnrichedVO vo = new VideoMatchEnrichedVO();
|
|
|
+ vo.setId(m.getVideoId());
|
|
|
+ vo.setModality(Modality.VIDEO);
|
|
|
+ vo.setConfigCode(requestConfigCode);
|
|
|
+ vo.setScore(m.getScore());
|
|
|
+
|
|
|
+ VideoDetail vd = videoDetails.get(m.getVideoId());
|
|
|
+ if (vd != null) {
|
|
|
+ vo.setTitle(vd.getTitle());
|
|
|
+ vo.setVideoUrl(vd.getVideoPath());
|
|
|
+ vo.setCover(vd.getCover());
|
|
|
+ }
|
|
|
+
|
|
|
+ vo.setVideoDetail(m.getVideoDetail());
|
|
|
+ applyCompatibilityFields(vo);
|
|
|
+ items.add(vo);
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 素材召回结果 enrich
|
|
|
+ * 数据源:material_deconstruct_result.result(dataContent JSON)+ source_type
|
|
|
+ * - title / cover / imageList:从 dataContent 提取
|
|
|
+ * - materialDetail.deconstruct:解析 dataContent 得到 topic + 灵感点/关键点/目的点
|
|
|
+ * - materialDetail.source:source_type → 中文标签
|
|
|
+ */
|
|
|
+ private List<VideoMatchEnrichedVO> enrichMaterialMatches(List<MaterialMatch> matches, String requestConfigCode) {
|
|
|
+ if (CollectionUtils.isEmpty(matches)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<String> materialIds = matches.stream()
|
|
|
+ .map(MaterialMatch::getMaterialId)
|
|
|
+ .filter(java.util.Objects::nonNull)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ Map<String, MaterialDeconstructResult> rowByMaterialId = loadMaterialDeconstructRows(materialIds);
|
|
|
+
|
|
|
+ List<VideoMatchEnrichedVO> items = new ArrayList<>(matches.size());
|
|
|
+ for (MaterialMatch m : matches) {
|
|
|
+ if (m == null || m.getMaterialId() == null) continue;
|
|
|
+ VideoMatchEnrichedVO vo = new VideoMatchEnrichedVO();
|
|
|
+ vo.setMaterialId(m.getMaterialId());
|
|
|
+ try {
|
|
|
+ vo.setId(Long.parseLong(m.getMaterialId()));
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ // 外部 MD5 素材无数值型 id,仅 materialId 字段下发
|
|
|
+ }
|
|
|
+ vo.setModality(Modality.MATERIAL);
|
|
|
+ vo.setConfigCode(requestConfigCode);
|
|
|
+ vo.setScore(m.getScore());
|
|
|
+
|
|
|
+ MaterialDeconstructResult row = rowByMaterialId.get(m.getMaterialId());
|
|
|
+ JSONObject raw = parseResultJson(row);
|
|
|
+ MaterialBasicMeta basic = raw != null ? extractMaterialBasicMeta(raw) : null;
|
|
|
+ Map<String, Object> deconstructFlat = raw != null ? buildDeconstructFromRaw(raw) : null;
|
|
|
+
|
|
|
+ if (basic != null) {
|
|
|
+ vo.setTitle(basic.title);
|
|
|
+ applyMaterialImagesAndCover(vo, basic.imagesJson);
|
|
|
+ }
|
|
|
+
|
|
|
+ MaterialDetailVO detail = new MaterialDetailVO();
|
|
|
+ if (basic != null) {
|
|
|
+ detail.setTitle(basic.title);
|
|
|
+ }
|
|
|
+ if (vo.getImageList() != null) {
|
|
|
+ detail.setImageCount(vo.getImageList().size());
|
|
|
+ }
|
|
|
+ Short sourceType = m.getSourceType();
|
|
|
+ if (sourceType == null && row != null) {
|
|
|
+ sourceType = row.getSourceType();
|
|
|
+ }
|
|
|
+ detail.setSource(mapSourceTypeToLabel(sourceType));
|
|
|
+ detail.setDeconstruct(deconstructFlat);
|
|
|
+ vo.setMaterialDetail(detail);
|
|
|
+
|
|
|
+ applyCompatibilityFields(vo);
|
|
|
+ items.add(vo);
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String mapSourceTypeToLabel(Short sourceType) {
|
|
|
+ if (sourceType == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (sourceType == SOURCE_TYPE_EXTERNAL) {
|
|
|
+ return SOURCE_LABEL_EXTERNAL;
|
|
|
+ }
|
|
|
+ if (sourceType == SOURCE_TYPE_INTERNAL) {
|
|
|
+ return SOURCE_LABEL_INTERNAL;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量加载 material_deconstruct_result 原始行(保留 source_type / result)
|
|
|
+ */
|
|
|
+ private Map<String, MaterialDeconstructResult> loadMaterialDeconstructRows(List<String> materialIds) {
|
|
|
+ if (CollectionUtils.isEmpty(materialIds)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<String, MaterialDeconstructResult> result = new HashMap<>();
|
|
|
+ try {
|
|
|
+ List<MaterialDeconstructResult> rows = materialDeconstructResultMapperExt
|
|
|
+ .selectResultsByMaterialIds(SOURCE_AIGC, materialIds);
|
|
|
+ if (CollectionUtils.isEmpty(rows)) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ for (MaterialDeconstructResult row : rows) {
|
|
|
+ if (row == null || !StringUtils.hasText(row.getMaterialId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ result.putIfAbsent(row.getMaterialId(), row);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量加载 material_deconstruct_result 失败: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject parseResultJson(MaterialDeconstructResult row) {
|
|
|
+ if (row == null || !StringUtils.hasText(row.getResult())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return JSON.parseObject(row.getResult());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.info("解析 material_deconstruct_result.result 失败 materialId={}: {}",
|
|
|
+ row.getMaterialId(), e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildDeconstructFromRaw(JSONObject raw) {
|
|
|
+ if (raw == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ JSONObject decoded = videoSearchService.parseDecodeResult(raw);
|
|
|
+ JSONObject base = decoded != null ? decoded : raw;
|
|
|
+ Map<String, Object> flat = buildFlatDeconstruct(base);
|
|
|
+ return (flat != null && !flat.isEmpty()) ? flat : null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.info("解析素材解构失败: {}", e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private MaterialBasicMeta extractMaterialBasicMeta(JSONObject raw) {
|
|
|
+ if (raw == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ MaterialBasicMeta meta = new MaterialBasicMeta();
|
|
|
+ meta.title = firstNonBlankString(
|
|
|
+ nestedString(raw, "target_post", "title"),
|
|
|
+ raw.getString("title"),
|
|
|
+ raw.getString("标题"),
|
|
|
+ raw.getString("contentTitle"),
|
|
|
+ raw.getString("素材标题"),
|
|
|
+ nestedString(raw, "input", "title"),
|
|
|
+ nestedString(raw, "content", "title"),
|
|
|
+ nestedString(raw, "最终选题", "name"),
|
|
|
+ nestedString(raw, "最终选题", "title")
|
|
|
+ );
|
|
|
+ JSONArray images = nestedArray(raw, "target_post", "images");
|
|
|
+ if (images == null) {
|
|
|
+ images = nestedArray(raw, "target_post", "imageList");
|
|
|
+ }
|
|
|
+ if (images == null) {
|
|
|
+ images = raw.getJSONArray("images");
|
|
|
+ }
|
|
|
+ if (images == null) {
|
|
|
+ images = raw.getJSONArray("imageList");
|
|
|
+ }
|
|
|
+ if (images == null) {
|
|
|
+ JSONObject input = raw.getJSONObject("input");
|
|
|
+ if (input != null) {
|
|
|
+ images = input.getJSONArray("images");
|
|
|
+ if (images == null) {
|
|
|
+ images = input.getJSONArray("imageList");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (images != null && !images.isEmpty()) {
|
|
|
+ meta.imagesJson = images.toJSONString();
|
|
|
+ }
|
|
|
+ if (!StringUtils.hasText(meta.title) && !StringUtils.hasText(meta.imagesJson)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return meta;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * cover 取自 imagesJson JSON 数组的第一张图
|
|
|
+ */
|
|
|
+ private void applyMaterialImagesAndCover(VideoMatchEnrichedVO vo, String imagesJson) {
|
|
|
+ List<String> imageList = parseImages(imagesJson);
|
|
|
+ if (CollectionUtils.isEmpty(imageList)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ vo.setImageList(imageList);
|
|
|
+ vo.setCover(imageList.get(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String nestedString(JSONObject parent, String objKey, String fieldKey) {
|
|
|
+ if (parent == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject nested = parent.getJSONObject(objKey);
|
|
|
+ return nested != null ? nested.getString(fieldKey) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONArray nestedArray(JSONObject parent, String objKey, String fieldKey) {
|
|
|
+ if (parent == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject nested = parent.getJSONObject(objKey);
|
|
|
+ return nested != null ? nested.getJSONArray(fieldKey) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String firstNonBlankString(String... values) {
|
|
|
+ if (values == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ for (String v : values) {
|
|
|
+ if (StringUtils.hasText(v)) {
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 把解构 JSON 转成扁平结构 (topic + 灵感点/关键点/目的点 及其实质)
|
|
|
+ * 与 VideoSearchServiceImpl#buildFlatDeconstruct 输出一致
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildFlatDeconstruct(JSONObject src) {
|
|
|
+ if (src == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Map<String, Object> out = new LinkedHashMap<>();
|
|
|
+ String topic = src.getString("topic");
|
|
|
+ if (topic != null) {
|
|
|
+ out.put("topic", topic);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, List<String>> nameByType = new LinkedHashMap<>();
|
|
|
+ Map<String, List<Map<String, Object>>> essenceByType = new LinkedHashMap<>();
|
|
|
+ for (String t : new String[]{"灵感点", "关键点", "目的点"}) {
|
|
|
+ nameByType.put(t, new ArrayList<>());
|
|
|
+ essenceByType.put(t, new ArrayList<>());
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONArray points = src.getJSONArray("highValuePoints");
|
|
|
+ if (points != null) {
|
|
|
+ for (int j = 0; j < points.size(); j++) {
|
|
|
+ JSONObject p = points.getJSONObject(j);
|
|
|
+ if (p == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String type = p.getString("type");
|
|
|
+ if (type == null || !nameByType.containsKey(type)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String name = p.getString("name");
|
|
|
+ if (name != null && !name.isEmpty()) {
|
|
|
+ nameByType.get(type).add(name);
|
|
|
+ }
|
|
|
+ JSONArray essences = p.getJSONArray("essences");
|
|
|
+ if (essences != null) {
|
|
|
+ for (int k = 0; k < essences.size(); k++) {
|
|
|
+ JSONObject e = essences.getJSONObject(k);
|
|
|
+ if (e == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("word", e.getString("word"));
|
|
|
+ item.put("score", e.getDouble("score"));
|
|
|
+ essenceByType.get(type).add(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (String t : new String[]{"灵感点", "关键点", "目的点"}) {
|
|
|
+ out.put(t, nameByType.get(t));
|
|
|
+ out.put(t + "-实质", essenceByType.get(t));
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组装返回结果:视频 + 素材合并为 items。
|
|
|
+ * 各模态在前置链路已按 videoTopN / materialTopN 各自截断,此处仅拼接 + 计数,不做合并截断。
|
|
|
+ */
|
|
|
+ private RecallResultVO buildResult(List<VideoMatchEnrichedVO> videoItems,
|
|
|
+ List<VideoMatchEnrichedVO> materialItems) {
|
|
|
+ if (videoItems == null) {
|
|
|
+ videoItems = Collections.emptyList();
|
|
|
+ }
|
|
|
+ if (materialItems == null) {
|
|
|
+ materialItems = Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<VideoMatchEnrichedVO> all = new ArrayList<>(videoItems.size() + materialItems.size());
|
|
|
+ all.addAll(videoItems);
|
|
|
+ all.addAll(materialItems);
|
|
|
+
|
|
|
+ int videoCount = 0;
|
|
|
+ int materialCount = 0;
|
|
|
+ int articleCount = 0;
|
|
|
+ for (VideoMatchEnrichedVO item : all) {
|
|
|
+ if (item.getModality() == Modality.VIDEO) {
|
|
|
+ videoCount++;
|
|
|
+ } else if (item.getModality() == Modality.ARTICLE) {
|
|
|
+ articleCount++;
|
|
|
+ } else {
|
|
|
+ materialCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ RecallResultVO vo = new RecallResultVO();
|
|
|
+ vo.setItems(all);
|
|
|
+ vo.setVideoCount(videoCount);
|
|
|
+ vo.setMaterialCount(materialCount);
|
|
|
+ vo.setArticleCount(articleCount);
|
|
|
+ vo.setTotal(videoCount + materialCount + articleCount);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applyCompatibilityFields(VideoMatchEnrichedVO vo) {
|
|
|
+ vo.setPlayCount(PLACEHOLDER);
|
|
|
+ vo.setExposure(PLACEHOLDER);
|
|
|
+ vo.setCtr(PLACEHOLDER);
|
|
|
+ vo.setReadCount(PLACEHOLDER);
|
|
|
+ vo.setRov(extractRovString(vo.getVideoDetail()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractRovString(Map<String, Object> videoDetail) {
|
|
|
+ if (videoDetail == null) {
|
|
|
+ return PLACEHOLDER;
|
|
|
+ }
|
|
|
+ Object rovObj = videoDetail.get("rov");
|
|
|
+ if (rovObj == null) {
|
|
|
+ return PLACEHOLDER;
|
|
|
+ }
|
|
|
+ String rovStr = String.valueOf(rovObj).trim();
|
|
|
+ return StringUtils.hasText(rovStr) ? rovStr : PLACEHOLDER;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -126,13 +734,16 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
MatchTopNVideoParam matchParam = new MatchTopNVideoParam();
|
|
|
matchParam.setChannelContentId(String.valueOf(param.getVideoId()));
|
|
|
matchParam.setConfigCode(param.getConfigCode());
|
|
|
- matchParam.setTopN(param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 10);
|
|
|
+ matchParam.setTopN(param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 50);
|
|
|
|
|
|
List<VideoMatchResult> rawMatches = videoSearchService.matchTopNVideo(matchParam, true);
|
|
|
if (CollectionUtils.isEmpty(rawMatches)) {
|
|
|
return empty;
|
|
|
}
|
|
|
- return enrich(rawMatches);
|
|
|
+ String configCode = StringUtils.hasText(param.getConfigCode())
|
|
|
+ ? param.getConfigCode() : VectorConstants.DEFAULT_CONFIG_CODE;
|
|
|
+ List<VideoMatchEnrichedVO> videoItems = enrichVideoMatches(rawMatches, configCode);
|
|
|
+ return buildResult(videoItems, Collections.emptyList());
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -221,178 +832,32 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 召回结果模态感知 enrich
|
|
|
- *
|
|
|
- * 流程:
|
|
|
- * - 提取所有 id
|
|
|
- * - 查 deconstruct_content WHERE channel_content_id IN (...) 拿 content_type
|
|
|
- * - 视频走 VideoApiService 取权威详情
|
|
|
- * - 素材/长文用 deconstruct_content 数据
|
|
|
- * - 默认按视频处理(用户确认 content_type 缺省语义)
|
|
|
- */
|
|
|
- private RecallResultVO enrich(List<VideoMatchResult> rawMatches) {
|
|
|
- // 转成内部 MatchItem(过滤 null/无效 id)
|
|
|
- List<MatchItem> matches = new ArrayList<>(rawMatches.size());
|
|
|
- for (VideoMatchResult r : rawMatches) {
|
|
|
- if (r == null || r.getVideoId() == null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- MatchItem mi = new MatchItem();
|
|
|
- mi.id = r.getVideoId();
|
|
|
- mi.configCode = r.getConfigCode();
|
|
|
- mi.score = r.getScore();
|
|
|
- mi.videoDetail = r.getVideoDetail();
|
|
|
- mi.text = r.getText();
|
|
|
- matches.add(mi);
|
|
|
- }
|
|
|
-
|
|
|
- if (matches.isEmpty()) {
|
|
|
- return emptyResult();
|
|
|
- }
|
|
|
-
|
|
|
- // 提取 id 列表(string形式,用于查 channel_content_id)
|
|
|
- Set<Long> allIds = matches.stream().map(m -> m.id).collect(Collectors.toSet());
|
|
|
- List<String> idStrings = allIds.stream().map(String::valueOf).collect(Collectors.toList());
|
|
|
-
|
|
|
- // 查 deconstruct_content
|
|
|
- Map<String, MysqlDeconstructContent> contentByCcid = queryDeconstructContent(idStrings);
|
|
|
-
|
|
|
- // 收集需要走 VideoApiService 的视频id
|
|
|
- Set<Long> videoIds = new HashSet<>();
|
|
|
- Map<Long, Modality> modalityMap = new HashMap<>();
|
|
|
-
|
|
|
- for (MatchItem m : matches) {
|
|
|
- MysqlDeconstructContent c = contentByCcid.get(String.valueOf(m.id));
|
|
|
- Modality modality = (c == null) ? Modality.VIDEO : Modality.fromContentType(c.getContentType());
|
|
|
- modalityMap.put(m.id, modality);
|
|
|
- if (modality == Modality.VIDEO) {
|
|
|
- videoIds.add(m.id);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 批量取视频详情
|
|
|
- Map<Long, VideoDetail> videoDetails = videoIds.isEmpty()
|
|
|
- ? Collections.emptyMap()
|
|
|
- : videoApiService.getVideoDetail(videoIds);
|
|
|
-
|
|
|
- // 组装 VO
|
|
|
- List<VideoMatchEnrichedVO> items = new ArrayList<>(matches.size());
|
|
|
- int videoCount = 0;
|
|
|
- int materialCount = 0;
|
|
|
- int articleCount = 0;
|
|
|
-
|
|
|
- for (MatchItem m : matches) {
|
|
|
- Modality modality = modalityMap.get(m.id);
|
|
|
- VideoMatchEnrichedVO vo = new VideoMatchEnrichedVO();
|
|
|
- vo.setId(m.id);
|
|
|
- vo.setModality(modality);
|
|
|
- vo.setConfigCode(m.configCode);
|
|
|
- vo.setScore(m.score);
|
|
|
- vo.setPlayCount(PLACEHOLDER);
|
|
|
- vo.setExposure(PLACEHOLDER);
|
|
|
- vo.setCtr(PLACEHOLDER);
|
|
|
- vo.setReadCount(PLACEHOLDER);
|
|
|
- vo.setRov(PLACEHOLDER);
|
|
|
-
|
|
|
- MysqlDeconstructContent content = contentByCcid.get(String.valueOf(m.id));
|
|
|
-
|
|
|
- switch (modality) {
|
|
|
- case VIDEO:
|
|
|
- VideoDetail vd = videoDetails.get(m.id);
|
|
|
- if (vd != null) {
|
|
|
- vo.setTitle(vd.getTitle());
|
|
|
- vo.setVideoUrl(vd.getVideoPath());
|
|
|
- vo.setCover(vd.getCover());
|
|
|
- } else if (content != null) {
|
|
|
- // 长视频API查不到,降级用本地 deconstruct_content
|
|
|
- vo.setTitle(content.getTitle());
|
|
|
- vo.setVideoUrl(content.getVideoUrl());
|
|
|
- }
|
|
|
- videoCount++;
|
|
|
- break;
|
|
|
- case MATERIAL:
|
|
|
- if (content != null) {
|
|
|
- vo.setTitle(content.getTitle());
|
|
|
- vo.setImageList(parseImages(content.getImages()));
|
|
|
- if (!CollectionUtils.isEmpty(vo.getImageList())) {
|
|
|
- vo.setCover(vo.getImageList().get(0));
|
|
|
- }
|
|
|
- }
|
|
|
- materialCount++;
|
|
|
- break;
|
|
|
- case ARTICLE:
|
|
|
- if (content != null) {
|
|
|
- vo.setTitle(content.getTitle());
|
|
|
- vo.setBodyText(content.getBodyText());
|
|
|
- }
|
|
|
- articleCount++;
|
|
|
- break;
|
|
|
- default:
|
|
|
- videoCount++;
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- // 填充 videoDetail (已由 VideoSearchServiceImpl 嵌入 deconstruct 子对象)
|
|
|
- vo.setVideoDetail(m.videoDetail);
|
|
|
- vo.setText(m.text);
|
|
|
-
|
|
|
- items.add(vo);
|
|
|
- }
|
|
|
-
|
|
|
- RecallResultVO result = new RecallResultVO();
|
|
|
- result.setItems(items);
|
|
|
- result.setVideoCount(videoCount);
|
|
|
- result.setMaterialCount(materialCount);
|
|
|
- result.setArticleCount(articleCount);
|
|
|
- result.setTotal(items.size());
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 按 channelContentId 批量查 deconstruct_content
|
|
|
- */
|
|
|
- private Map<String, MysqlDeconstructContent> queryDeconstructContent(List<String> channelContentIds) {
|
|
|
- if (CollectionUtils.isEmpty(channelContentIds)) {
|
|
|
- return Collections.emptyMap();
|
|
|
- }
|
|
|
- try {
|
|
|
- MysqlDeconstructContentExample example = new MysqlDeconstructContentExample();
|
|
|
- example.createCriteria().andChannelContentIdIn(channelContentIds);
|
|
|
- List<MysqlDeconstructContent> list = mysqlDeconstructContentMapper.selectByExample(example);
|
|
|
- // channel_content_id 可能重复(同一内容多次解构),保留最新一条
|
|
|
- Map<String, MysqlDeconstructContent> map = new HashMap<>();
|
|
|
- for (MysqlDeconstructContent c : list) {
|
|
|
- String ccid = c.getChannelContentId();
|
|
|
- if (ccid == null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- MysqlDeconstructContent prev = map.get(ccid);
|
|
|
- if (prev == null || (c.getId() != null && (prev.getId() == null || c.getId() > prev.getId()))) {
|
|
|
- map.put(ccid, c);
|
|
|
- }
|
|
|
- }
|
|
|
- return map;
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("queryDeconstructContent error: {}", e.getMessage(), e);
|
|
|
- return Collections.emptyMap();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
private List<String> parseImages(String imagesJson) {
|
|
|
if (!StringUtils.hasText(imagesJson)) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
try {
|
|
|
- JSONArray arr = JSON.parseArray(imagesJson);
|
|
|
- if (arr == null) {
|
|
|
+ String normalized = imagesJson.trim();
|
|
|
+ // 兼容双重 JSON 编码,临时兜底;根本修复应在数据写入 material_deconstruct_result.result 时做一次 JSON 规范化
|
|
|
+ if (normalized.startsWith("\"") && normalized.endsWith("\"")) {
|
|
|
+ Object unquoted = JSON.parse(normalized);
|
|
|
+ if (unquoted instanceof String) {
|
|
|
+ normalized = ((String) unquoted).trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JSONArray arr = JSON.parseArray(normalized);
|
|
|
+ if (arr == null || arr.isEmpty()) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
List<String> result = new ArrayList<>(arr.size());
|
|
|
for (int i = 0; i < arr.size(); i++) {
|
|
|
- String s = arr.getString(i);
|
|
|
- if (StringUtils.hasText(s)) {
|
|
|
- result.add(s);
|
|
|
+ Object item = arr.get(i);
|
|
|
+ if (item == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String url = item instanceof String ? (String) item : String.valueOf(item);
|
|
|
+ if (StringUtils.hasText(url)) {
|
|
|
+ result.add(url.trim());
|
|
|
}
|
|
|
}
|
|
|
return result;
|
|
|
@@ -412,14 +877,9 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
return vo;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 解析后的单条 match
|
|
|
- */
|
|
|
- private static class MatchItem {
|
|
|
- Long id;
|
|
|
- String configCode;
|
|
|
- Double score;
|
|
|
- Map<String, Object> videoDetail;
|
|
|
- String text;
|
|
|
+ /** 从 AIGC dataContent 提取的素材基础元数据 */
|
|
|
+ private static class MaterialBasicMeta {
|
|
|
+ String title;
|
|
|
+ String imagesJson;
|
|
|
}
|
|
|
}
|