|
|
@@ -40,7 +40,6 @@ import com.tzld.videoVector.util.Md5Util;
|
|
|
import com.tzld.videoVector.util.RedisUtils;
|
|
|
import com.tzld.videoVector.util.VectorUtils;
|
|
|
import com.tzld.videoVector.model.po.pgVector.MaterialVector;
|
|
|
-import com.google.common.collect.Lists;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
@@ -54,11 +53,9 @@ 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.Objects;
|
|
|
import java.util.Set;
|
|
|
import java.util.Comparator;
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
@@ -486,18 +483,8 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
+ fillMaterialDetailVO(detail, basic, row, deconstructFlat, m.getSourceType());
|
|
|
+ fillMaterialDetailImageCount(detail, vo.getImageList());
|
|
|
vo.setMaterialDetail(detail);
|
|
|
|
|
|
applyCompatibilityFields(vo);
|
|
|
@@ -849,12 +836,82 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
if (images != null && !images.isEmpty()) {
|
|
|
meta.imagesJson = images.toJSONString();
|
|
|
}
|
|
|
+ meta.uploadTime = firstNonBlankString(
|
|
|
+ nestedString(raw, "target_post", "uploadTime"),
|
|
|
+ nestedString(raw, "target_post", "createTime"),
|
|
|
+ raw.getString("uploadTime"),
|
|
|
+ raw.getString("createTime"),
|
|
|
+ raw.getString("采集时间")
|
|
|
+ );
|
|
|
+ meta.usageCount = firstNonBlankString(
|
|
|
+ nestedString(raw, "target_post", "usageCount"),
|
|
|
+ raw.getString("usageCount"),
|
|
|
+ raw.getString("使用次数")
|
|
|
+ );
|
|
|
+ meta.tags = extractStringList(raw, "target_post", "tags");
|
|
|
+ if (meta.tags == null) {
|
|
|
+ meta.tags = extractStringList(raw, null, "tags");
|
|
|
+ }
|
|
|
+ if (meta.tags == null) {
|
|
|
+ String tagsStr = firstNonBlankString(raw.getString("标签"), nestedString(raw, "target_post", "label"));
|
|
|
+ if (StringUtils.hasText(tagsStr)) {
|
|
|
+ meta.tags = java.util.Arrays.stream(tagsStr.split("[,,]"))
|
|
|
+ .map(String::trim)
|
|
|
+ .filter(StringUtils::hasText)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+ }
|
|
|
if (!StringUtils.hasText(meta.title) && !StringUtils.hasText(meta.imagesJson)) {
|
|
|
return null;
|
|
|
}
|
|
|
return meta;
|
|
|
}
|
|
|
|
|
|
+ private List<String> extractStringList(JSONObject raw, String objKey, String fieldKey) {
|
|
|
+ JSONArray arr = objKey != null ? nestedArray(raw, objKey, fieldKey) : raw.getJSONArray(fieldKey);
|
|
|
+ if (arr == null || arr.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<String> list = new ArrayList<>(arr.size());
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ Object item = arr.get(i);
|
|
|
+ if (item == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String s = String.valueOf(item).trim();
|
|
|
+ if (StringUtils.hasText(s)) {
|
|
|
+ list.add(s);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return list.isEmpty() ? null : list;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillMaterialDetailVO(MaterialDetailVO detail, MaterialBasicMeta basic,
|
|
|
+ MaterialDeconstructResult row, Map<String, Object> deconstructFlat,
|
|
|
+ Short sourceTypeOverride) {
|
|
|
+ if (detail == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (basic != null) {
|
|
|
+ detail.setTitle(basic.title);
|
|
|
+ detail.setUploadTime(basic.uploadTime);
|
|
|
+ detail.setUsageCount(basic.usageCount);
|
|
|
+ detail.setTags(basic.tags);
|
|
|
+ }
|
|
|
+ Short sourceType = sourceTypeOverride;
|
|
|
+ if (sourceType == null && row != null) {
|
|
|
+ sourceType = row.getSourceType();
|
|
|
+ }
|
|
|
+ detail.setSource(mapSourceTypeToLabel(sourceType));
|
|
|
+ detail.setDeconstruct(deconstructFlat);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fillMaterialDetailImageCount(MaterialDetailVO detail, List<String> imageList) {
|
|
|
+ if (detail != null && imageList != null) {
|
|
|
+ detail.setImageCount(imageList.size());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* cover 取自 imagesJson JSON 数组的第一张图
|
|
|
*/
|
|
|
@@ -1058,7 +1115,7 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
|
|
|
List<String> configCodes;
|
|
|
if (StringUtils.hasText(param.getConfigCode())) {
|
|
|
- configCodes = Collections.singletonList(param.getConfigCode());
|
|
|
+ configCodes = Collections.singletonList(param.getConfigCode().trim());
|
|
|
} else {
|
|
|
configCodes = materialVectorStoreService.getDistinctConfigCodes(materialId);
|
|
|
if (configCodes.isEmpty()) {
|
|
|
@@ -1068,47 +1125,52 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
}
|
|
|
log.info("matchByMaterialId: materialId={}, topN={}, configCodes={}", materialId, topN, configCodes);
|
|
|
|
|
|
+ // 收集所有有效向量点(支持多点模式)
|
|
|
+ List<MaterialVectorQuery> vectorQueries = new ArrayList<>();
|
|
|
+ for (String configCode : configCodes) {
|
|
|
+ List<MaterialVector> vectors = materialVectorStoreService.getVectorsByMaterialId(materialId, configCode);
|
|
|
+ for (MaterialVector vector : vectors) {
|
|
|
+ if (vector != null && StringUtils.hasText(vector.getEmbedding())) {
|
|
|
+ vectorQueries.add(new MaterialVectorQuery(configCode, vector.getEmbedding()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (vectorQueries.isEmpty()) {
|
|
|
+ log.info("matchByMaterialId: materialId={} 无有效向量 embedding", materialId);
|
|
|
+ return empty;
|
|
|
+ }
|
|
|
+
|
|
|
int candidateTopN = Math.max(topN * VectorConstants.MULTI_POINT_RECALL_CANDIDATE_FACTOR,
|
|
|
VectorConstants.MULTI_POINT_RECALL_MIN_CANDIDATES);
|
|
|
|
|
|
List<VideoMatchEnrichedVO> allResults = Collections.synchronizedList(new ArrayList<>());
|
|
|
List<CompletableFuture<Void>> allFutures = new ArrayList<>();
|
|
|
|
|
|
- for (String configCode : configCodes) {
|
|
|
- List<MaterialVector> vectors = materialVectorStoreService.getVectorsByMaterialId(materialId, configCode);
|
|
|
- if (vectors.isEmpty()) continue;
|
|
|
-
|
|
|
- // 取第一个点向量的 raw embedding
|
|
|
- String rawEmbedding = vectors.get(0).getEmbedding();
|
|
|
- if (!StringUtils.hasText(rawEmbedding)) continue;
|
|
|
-
|
|
|
- List<Float> queryVector = VectorUtils.parseVectorString(rawEmbedding);
|
|
|
- if (queryVector == null || queryVector.isEmpty()) continue;
|
|
|
-
|
|
|
- final String cc = configCode;
|
|
|
- final List<Float> qv = queryVector;
|
|
|
+ for (MaterialVectorQuery query : vectorQueries) {
|
|
|
+ final String cc = query.configCode;
|
|
|
+ final String rawEmbedding = query.rawEmbedding;
|
|
|
final int ctn = candidateTopN;
|
|
|
final int tn = topN;
|
|
|
|
|
|
- // 视频召回:同维度搜索,不跨 configCode
|
|
|
allFutures.add(CompletableFuture.runAsync(() -> {
|
|
|
try {
|
|
|
- List<VideoMatch> matches = vectorStoreService.searchTopN(cc, qv, ctn);
|
|
|
+ List<VideoMatch> matches = vectorStoreService.searchTopNByRawVector(cc, rawEmbedding, ctn);
|
|
|
List<VideoMatch> deduped = deduplicateVideoMatches(matches, tn);
|
|
|
log.info("matchByMaterialId 视频搜索 cc={}: {} 条, 去重后 {} 条",
|
|
|
cc, matches != null ? matches.size() : 0, deduped.size());
|
|
|
if (!deduped.isEmpty()) {
|
|
|
- allResults.addAll(enrichVideoMatchesFromVectorStore(deduped, cc));
|
|
|
+ List<VideoMatchResult> videoResults = toVideoMatchResults(deduped, cc);
|
|
|
+ populateVideoMatchResultDetails(videoResults);
|
|
|
+ allResults.addAll(enrichVideoMatches(videoResults, cc));
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- log.error("matchByMaterialId 视频搜索失败 configCode={}: {}", cc, e.getMessage());
|
|
|
+ log.error("matchByMaterialId 视频搜索失败 configCode={}: {}", cc, e.getMessage(), e);
|
|
|
}
|
|
|
}, RECALL_EXECUTOR));
|
|
|
|
|
|
- // 素材召回(排除自身)
|
|
|
allFutures.add(CompletableFuture.runAsync(() -> {
|
|
|
try {
|
|
|
- List<MaterialMatch> matches = materialVectorStoreService.searchTopN(cc, qv, ctn);
|
|
|
+ List<MaterialMatch> matches = materialVectorStoreService.searchTopNByRawVector(cc, rawEmbedding, ctn);
|
|
|
matches = matches.stream()
|
|
|
.filter(m -> !materialId.equals(m.getMaterialId()))
|
|
|
.collect(Collectors.toList());
|
|
|
@@ -1117,37 +1179,33 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
allResults.addAll(enrichMaterialMatches(deduped, cc));
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- log.error("matchByMaterialId 素材搜索失败 configCode={}: {}", cc, e.getMessage());
|
|
|
+ log.error("matchByMaterialId 素材搜索失败 configCode={}: {}", cc, e.getMessage(), e);
|
|
|
}
|
|
|
}, RECALL_EXECUTOR));
|
|
|
|
|
|
- // 文章召回
|
|
|
allFutures.add(CompletableFuture.runAsync(() -> {
|
|
|
try {
|
|
|
- List<ArticleMatch> matches = articleVectorStoreService.searchTopN(cc, qv, ctn);
|
|
|
+ List<ArticleMatch> matches = articleVectorStoreService.searchTopNByRawVector(cc, rawEmbedding, ctn);
|
|
|
List<ArticleMatch> deduped = deduplicateArticleMatches(matches, tn);
|
|
|
if (!deduped.isEmpty()) {
|
|
|
allResults.addAll(enrichArticleMatches(deduped, cc));
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- log.error("matchByMaterialId 文章搜索失败 configCode={}: {}", cc, e.getMessage());
|
|
|
+ log.error("matchByMaterialId 文章搜索失败 configCode={}: {}", cc, e.getMessage(), e);
|
|
|
}
|
|
|
}, RECALL_EXECUTOR));
|
|
|
}
|
|
|
|
|
|
- // 等待并行搜索完成
|
|
|
for (CompletableFuture<Void> future : allFutures) {
|
|
|
try {
|
|
|
future.get(30, TimeUnit.SECONDS);
|
|
|
} catch (Exception e) {
|
|
|
- log.error("matchByMaterialId 并行搜索等待异常: {}", e.getMessage());
|
|
|
+ log.error("matchByMaterialId 并行搜索等待异常: {}", e.getMessage(), e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 跨 configCode 去重:同一 (id, modality) 保留最高分
|
|
|
List<VideoMatchEnrichedVO> merged = deduplicateCrossConfigCode(allResults);
|
|
|
|
|
|
- // 按模态拆分,各自独立排序截断到 topN(避免跨模态分数竞争,视频分数可能天然低于素材自身匹配)
|
|
|
List<VideoMatchEnrichedVO> videoItems = merged.stream()
|
|
|
.filter(it -> it.getModality() == Modality.VIDEO)
|
|
|
.sorted(Comparator.comparing(VideoMatchEnrichedVO::getScore,
|
|
|
@@ -1170,11 +1228,10 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
log.info("matchByMaterialId 按模态截断后: video={}, material={}, article={}",
|
|
|
videoItems.size(), materialItems.size(), articleItems.size());
|
|
|
|
|
|
- // 把输入素材自身加入结果(score=1.0, modality=MATERIAL)
|
|
|
- VideoMatchEnrichedVO selfItem = enrichSelfMaterial(materialId, configCodes.isEmpty() ? null : configCodes.get(0));
|
|
|
- if (selfItem != null) {
|
|
|
- materialItems.add(0, selfItem);
|
|
|
- }
|
|
|
+ String selfConfigCode = StringUtils.hasText(param.getConfigCode())
|
|
|
+ ? param.getConfigCode().trim()
|
|
|
+ : vectorQueries.get(0).configCode;
|
|
|
+ ensureSelfMaterialInResults(materialItems, materialId, selfConfigCode, topN);
|
|
|
|
|
|
RecallResultVO result = buildResult(videoItems, materialItems, articleItems);
|
|
|
log.info("matchByMaterialId 完成: total={}, video={}, material={}, article={}",
|
|
|
@@ -1223,110 +1280,97 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
return item.getModality() + ":" + idPart;
|
|
|
}
|
|
|
|
|
|
+ private List<VideoMatchResult> toVideoMatchResults(List<VideoMatch> matches, String configCode) {
|
|
|
+ if (CollectionUtils.isEmpty(matches)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<VideoMatchResult> results = new ArrayList<>(matches.size());
|
|
|
+ for (VideoMatch match : matches) {
|
|
|
+ if (match == null || match.getVideoId() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ results.add(new VideoMatchResult(configCode, match.getVideoId(), match.getScore(), match.getText()));
|
|
|
+ }
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 从 vectorStore(video_vectors)搜索结果 enrich,对齐 VideoSearchServiceImpl 的 enrichVideoDetail + enrichDeconstruct
|
|
|
+ * 填充 VideoMatchResult.videoDetail(运营指标 + 解构),对齐 VideoSearchServiceImpl。
|
|
|
*/
|
|
|
- private List<VideoMatchEnrichedVO> enrichVideoMatchesFromVectorStore(List<VideoMatch> matches, String configCode) {
|
|
|
- if (CollectionUtils.isEmpty(matches)) return Collections.emptyList();
|
|
|
-
|
|
|
- List<Long> videoIdList = matches.stream()
|
|
|
- .map(VideoMatch::getVideoId)
|
|
|
- .filter(Objects::nonNull)
|
|
|
- .distinct()
|
|
|
- .collect(Collectors.toList());
|
|
|
- if (videoIdList.isEmpty()) return Collections.emptyList();
|
|
|
-
|
|
|
- // 1. 视频基础信息(标题、封面、视频地址),按 100 条分批避免 API 超时
|
|
|
- Map<Long, VideoDetail> videoDetails = new HashMap<>();
|
|
|
- for (List<Long> batch : Lists.partition(videoIdList, 100)) {
|
|
|
- Map<Long, VideoDetail> batchResult = videoApiService.getVideoDetail(new HashSet<>(batch));
|
|
|
- if (batchResult != null) {
|
|
|
- videoDetails.putAll(batchResult);
|
|
|
- }
|
|
|
+ private void populateVideoMatchResultDetails(List<VideoMatchResult> results) {
|
|
|
+ if (CollectionUtils.isEmpty(results)) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- // 2. 批量读取视频运营指标: video:detail:{days}d:{videoId}
|
|
|
- Map<Long, Map<String, Object>> metricsCache = new HashMap<>();
|
|
|
try {
|
|
|
- List<String> metricsKeys = videoIdList.stream()
|
|
|
- .map(id -> VectorConstants.VIDEO_DETAIL_DAYS_KEY_PREFIX + metricsDays + "d:" + id)
|
|
|
+ List<String> metricsKeys = results.stream()
|
|
|
+ .map(r -> VectorConstants.VIDEO_DETAIL_DAYS_KEY_PREFIX + metricsDays + "d:" + r.getVideoId())
|
|
|
.collect(Collectors.toList());
|
|
|
List<String> metricsValues = redisUtils.mGet(metricsKeys);
|
|
|
if (metricsValues != null) {
|
|
|
- for (int i = 0; i < videoIdList.size() && i < metricsValues.size(); i++) {
|
|
|
+ for (int i = 0; i < results.size() && i < metricsValues.size(); i++) {
|
|
|
String json = metricsValues.get(i);
|
|
|
- if (StringUtils.hasText(json)) {
|
|
|
- try {
|
|
|
- Map<String, Object> detail = JSONObject.parseObject(json, Map.class);
|
|
|
- if (detail != null && !detail.isEmpty()) {
|
|
|
- metricsCache.put(videoIdList.get(i), detail);
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.debug("解析视频指标失败 videoId={}", videoIdList.get(i));
|
|
|
+ if (!StringUtils.hasText(json)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Map<String, Object> detail = JSONObject.parseObject(json, Map.class);
|
|
|
+ if (detail != null && !detail.isEmpty()) {
|
|
|
+ results.get(i).setVideoDetail(detail);
|
|
|
}
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("解析视频指标失败 videoId={}", results.get(i).getVideoId());
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- log.error("批量读取视频指标失败: {}", e.getMessage());
|
|
|
+ log.error("批量读取视频指标失败: {}", e.getMessage(), e);
|
|
|
}
|
|
|
|
|
|
- // 3. 批量读取解构缓存: recall:vid_decode:{videoId}
|
|
|
- Map<Long, Map<String, Object>> deconstructCache = new HashMap<>();
|
|
|
try {
|
|
|
- List<String> decodeKeys = videoIdList.stream()
|
|
|
- .map(id -> REDIS_KEY_DECODE_PREFIX + id)
|
|
|
+ List<String> decodeKeys = results.stream()
|
|
|
+ .map(r -> VectorConstants.VID_DECODE_KEY_PREFIX + r.getVideoId())
|
|
|
.collect(Collectors.toList());
|
|
|
List<String> decodeValues = redisUtils.mGet(decodeKeys);
|
|
|
- if (decodeValues != null) {
|
|
|
- for (int i = 0; i < videoIdList.size() && i < decodeValues.size(); i++) {
|
|
|
- String json = decodeValues.get(i);
|
|
|
- if (StringUtils.hasText(json)) {
|
|
|
- JSONObject obj = JSON.parseObject(json);
|
|
|
- Map<String, Object> flat = obj != null ? buildFlatDeconstruct(obj) : null;
|
|
|
- if (flat != null && !flat.isEmpty()) {
|
|
|
- deconstructCache.put(videoIdList.get(i), flat);
|
|
|
- }
|
|
|
+ if (decodeValues == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < results.size(); i++) {
|
|
|
+ String json = i < decodeValues.size() ? decodeValues.get(i) : null;
|
|
|
+ if (!StringUtils.hasText(json)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Map<String, Object> deconstructFlat = buildFlatDeconstruct(JSON.parseObject(json));
|
|
|
+ if (deconstructFlat == null || deconstructFlat.isEmpty()) {
|
|
|
+ continue;
|
|
|
}
|
|
|
+ VideoMatchResult result = results.get(i);
|
|
|
+ Map<String, Object> detailMap = result.getVideoDetail();
|
|
|
+ if (detailMap == null) {
|
|
|
+ detailMap = new LinkedHashMap<>();
|
|
|
+ result.setVideoDetail(detailMap);
|
|
|
+ }
|
|
|
+ detailMap.put("deconstruct", deconstructFlat);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("解析视频解构失败 videoId={}: {}", results.get(i).getVideoId(), e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- log.error("批量读取解构缓存失败: {}", e.getMessage());
|
|
|
+ log.error("批量读取解构缓存失败: {}", e.getMessage(), e);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 4. 组装 VideoMatchEnrichedVO
|
|
|
- List<VideoMatchEnrichedVO> items = new ArrayList<>(matches.size());
|
|
|
- for (VideoMatch m : matches) {
|
|
|
- if (m == null || m.getVideoId() == null) continue;
|
|
|
- VideoMatchEnrichedVO vo = new VideoMatchEnrichedVO();
|
|
|
- vo.setId(m.getVideoId());
|
|
|
- vo.setModality(Modality.VIDEO);
|
|
|
- vo.setConfigCode(configCode);
|
|
|
- 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());
|
|
|
- }
|
|
|
-
|
|
|
- // videoDetail map: 合并运营指标 + 解构(对齐 VideoSearchServiceImpl 输出格式)
|
|
|
- Map<String, Object> detailMap = new LinkedHashMap<>();
|
|
|
- Map<String, Object> metrics = metricsCache.get(m.getVideoId());
|
|
|
- if (metrics != null) {
|
|
|
- detailMap.putAll(metrics);
|
|
|
- }
|
|
|
- Map<String, Object> deconstruct = deconstructCache.get(m.getVideoId());
|
|
|
- if (deconstruct != null) {
|
|
|
- detailMap.put("deconstruct", deconstruct);
|
|
|
- }
|
|
|
- vo.setVideoDetail(detailMap.isEmpty() ? null : detailMap);
|
|
|
-
|
|
|
- applyCompatibilityFields(vo);
|
|
|
- items.add(vo);
|
|
|
+ /**
|
|
|
+ * 保证输入素材自身出现在 material 结果中(score=1.0),且 material 列表不超过 topN。
|
|
|
+ */
|
|
|
+ private void ensureSelfMaterialInResults(List<VideoMatchEnrichedVO> materialItems,
|
|
|
+ String materialId, String configCode, int topN) {
|
|
|
+ materialItems.removeIf(it -> materialId.equals(it.getMaterialId()));
|
|
|
+ materialItems.add(0, enrichSelfMaterial(materialId, configCode));
|
|
|
+ while (materialItems.size() > topN) {
|
|
|
+ materialItems.remove(materialItems.size() - 1);
|
|
|
}
|
|
|
- return items;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1356,15 +1400,8 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
}
|
|
|
|
|
|
MaterialDetailVO detail = new MaterialDetailVO();
|
|
|
- if (basic != null) {
|
|
|
- detail.setTitle(basic.title);
|
|
|
- }
|
|
|
- if (vo.getImageList() != null) {
|
|
|
- detail.setImageCount(vo.getImageList().size());
|
|
|
- }
|
|
|
- Short sourceType = row != null ? row.getSourceType() : null;
|
|
|
- detail.setSource(mapSourceTypeToLabel(sourceType));
|
|
|
- detail.setDeconstruct(deconstructFlat);
|
|
|
+ fillMaterialDetailVO(detail, basic, row, deconstructFlat, row != null ? row.getSourceType() : null);
|
|
|
+ fillMaterialDetailImageCount(detail, vo.getImageList());
|
|
|
vo.setMaterialDetail(detail);
|
|
|
|
|
|
applyCompatibilityFields(vo);
|
|
|
@@ -1539,6 +1576,19 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
private static class MaterialBasicMeta {
|
|
|
String title;
|
|
|
String imagesJson;
|
|
|
+ String uploadTime;
|
|
|
+ String usageCount;
|
|
|
+ List<String> tags;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class MaterialVectorQuery {
|
|
|
+ final String configCode;
|
|
|
+ final String rawEmbedding;
|
|
|
+
|
|
|
+ MaterialVectorQuery(String configCode, String rawEmbedding) {
|
|
|
+ this.configCode = configCode;
|
|
|
+ this.rawEmbedding = rawEmbedding;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private static class ArticleBasicMeta {
|
|
|
@@ -1551,3 +1601,4 @@ public class VectorRecallTestServiceImpl implements VectorRecallTestService {
|
|
|
List<String> images;
|
|
|
}
|
|
|
}
|
|
|
+
|