|
@@ -30,6 +30,7 @@ import org.springframework.util.StringUtils;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
|
+import java.util.concurrent.*;
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
import static com.tzld.videoVector.common.constant.VectorConstants.DEFAULT_CONFIG_CODE;
|
|
import static com.tzld.videoVector.common.constant.VectorConstants.DEFAULT_CONFIG_CODE;
|
|
@@ -38,6 +39,12 @@ import static com.tzld.videoVector.common.constant.VectorConstants.DEFAULT_CONFI
|
|
|
@Service
|
|
@Service
|
|
|
public class VideoSearchServiceImpl implements VideoSearchService {
|
|
public class VideoSearchServiceImpl implements VideoSearchService {
|
|
|
|
|
|
|
|
|
|
+ private static final ExecutorService MATCH_EXECUTOR = new ThreadPoolExecutor(
|
|
|
|
|
+ 10, 10, 60L, TimeUnit.SECONDS,
|
|
|
|
|
+ new LinkedBlockingQueue<>(32),
|
|
|
|
|
+ new ThreadPoolExecutor.CallerRunsPolicy());
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private DeconstructService deconstructService;
|
|
private DeconstructService deconstructService;
|
|
|
|
|
|
|
@@ -598,7 +605,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
|
|
|
* 匹配TopN视频
|
|
* 匹配TopN视频
|
|
|
*/
|
|
*/
|
|
|
@Override
|
|
@Override
|
|
|
- public List<VideoMatchResult> matchTopNVideo(MatchTopNVideoParam param) {
|
|
|
|
|
|
|
+ public List<VideoMatchResult> matchTopNVideo(MatchTopNVideoParam param, Boolean withDetail) {
|
|
|
if (param == null) {
|
|
if (param == null) {
|
|
|
log.error("matchTopNVideo 参数为空");
|
|
log.error("matchTopNVideo 参数为空");
|
|
|
return Collections.emptyList();
|
|
return Collections.emptyList();
|
|
@@ -638,56 +645,90 @@ public class VideoSearchServiceImpl implements VideoSearchService {
|
|
|
// 缓存 embeddingModel -> queryVector,避免相同模型重复 embedding
|
|
// 缓存 embeddingModel -> queryVector,避免相同模型重复 embedding
|
|
|
Map<String, List<Float>> embeddingCache = new HashMap<>();
|
|
Map<String, List<Float>> embeddingCache = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
+ // 先串行解析各配置的查询向量(涉及缓存共享,不适合并发)
|
|
|
|
|
+ Map<DeconstructVectorConfig, List<Float>> configVectorMap = new LinkedHashMap<>();
|
|
|
for (DeconstructVectorConfig config : searchConfigs) {
|
|
for (DeconstructVectorConfig config : searchConfigs) {
|
|
|
|
|
+ List<Float> queryVector = resolveQueryVectorForConfig(param, config, embeddingCache);
|
|
|
|
|
+ if (queryVector == null || queryVector.isEmpty()) {
|
|
|
|
|
+ log.error("配置 {} 无法获取查询向量,跳过", config.getConfigCode());
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ configVectorMap.put(config, queryVector);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (configVectorMap.isEmpty()) {
|
|
|
|
|
+ log.info("所有配置均无法获取查询向量,返回空结果");
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 多configCode并发搜索
|
|
|
|
|
+ int finalTopN = topN;
|
|
|
|
|
+ List<CompletableFuture<List<VideoMatchResult>>> futures = new ArrayList<>();
|
|
|
|
|
+ for (Map.Entry<DeconstructVectorConfig, List<Float>> entry : configVectorMap.entrySet()) {
|
|
|
|
|
+ DeconstructVectorConfig config = entry.getKey();
|
|
|
|
|
+ List<Float> queryVector = entry.getValue();
|
|
|
String cfgCode = config.getConfigCode();
|
|
String cfgCode = config.getConfigCode();
|
|
|
- try {
|
|
|
|
|
- // 解析查询向量(按配置的 embeddingModel 分组缓存)
|
|
|
|
|
- List<Float> queryVector = resolveQueryVectorForConfig(param, config, embeddingCache);
|
|
|
|
|
- if (queryVector == null || queryVector.isEmpty()) {
|
|
|
|
|
- log.error("配置 {} 无法获取查询向量,跳过", cfgCode);
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- log.info("配置 {} 开始搜索 Top-{},向量维度: {}", cfgCode, candidateSize, queryVector.size());
|
|
|
|
|
- List<VideoMatch> matches = vectorStoreService.searchTopN(cfgCode, queryVector, candidateSize);
|
|
|
|
|
|
|
+ CompletableFuture<List<VideoMatchResult>> future = CompletableFuture.supplyAsync(() -> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ log.info("配置 {} 开始搜索 Top-{},向量维度: {}", cfgCode, candidateSize, queryVector.size());
|
|
|
|
|
+ List<VideoMatch> matches = vectorStoreService.searchTopN(cfgCode, queryVector, candidateSize);
|
|
|
|
|
|
|
|
- if (matches == null || matches.isEmpty()) {
|
|
|
|
|
- log.info("配置 {} 无匹配结果", cfgCode);
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (matches == null || matches.isEmpty()) {
|
|
|
|
|
+ log.info("配置 {} 无匹配结果", cfgCode);
|
|
|
|
|
+ return Collections.<VideoMatchResult>emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 多点模式:同一videoId去重保留最高分
|
|
|
|
|
- boolean multiPoint = VectorUtils.isMultiPointConfig(config);
|
|
|
|
|
- if (multiPoint) {
|
|
|
|
|
- matches = deduplicateMultiPointMatches(matches, cfgCode);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 单点模式:直接设置 configCode
|
|
|
|
|
- for (VideoMatch m : matches) {
|
|
|
|
|
- m.setConfigCode(cfgCode);
|
|
|
|
|
|
|
+ // 多点模式:同一videoId去重保留最高分
|
|
|
|
|
+ boolean multiPoint = VectorUtils.isMultiPointConfig(config);
|
|
|
|
|
+ if (multiPoint) {
|
|
|
|
|
+ matches = deduplicateMultiPointMatches(matches, cfgCode);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 单点模式:直接设置 configCode
|
|
|
|
|
+ for (VideoMatch m : matches) {
|
|
|
|
|
+ m.setConfigCode(cfgCode);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // 每个配置独立进行审核过滤并取各自的 topN
|
|
|
|
|
- List<VideoMatch> filteredMatches = filterByAuditStatus(matches, topN);
|
|
|
|
|
|
|
+ // 每个配置独立进行审核过滤并取各自的 topN
|
|
|
|
|
+ List<VideoMatch> filteredMatches = filterByAuditStatus(matches, finalTopN);
|
|
|
|
|
+
|
|
|
|
|
+ // 转化为强类型返回格式
|
|
|
|
|
+ List<VideoMatchResult> configResult = new ArrayList<>(filteredMatches.size());
|
|
|
|
|
+ for (VideoMatch match : filteredMatches) {
|
|
|
|
|
+ configResult.add(new VideoMatchResult(cfgCode, match.getVideoId(), match.getScore(), match.getText()));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 转化为强类型返回格式
|
|
|
|
|
- for (VideoMatch match : filteredMatches) {
|
|
|
|
|
- result.add(new VideoMatchResult(cfgCode, match.getVideoId(), match.getScore(), match.getText()));
|
|
|
|
|
|
|
+ log.info("配置 {} 搜索完成,返回 {} 条结果", cfgCode, configResult.size());
|
|
|
|
|
+ return configResult;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("配置 {} 搜索失败: {}", cfgCode, e.getMessage(), e);
|
|
|
|
|
+ return Collections.<VideoMatchResult>emptyList();
|
|
|
}
|
|
}
|
|
|
|
|
+ }, MATCH_EXECUTOR);
|
|
|
|
|
|
|
|
- log.info("配置 {} 搜索完成,返回 {} 条结果", cfgCode, filteredMatches.size());
|
|
|
|
|
|
|
+ futures.add(future);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // 等待所有配置搜索完成并汇总
|
|
|
|
|
+ for (CompletableFuture<List<VideoMatchResult>> future : futures) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ result.addAll(future.get(30, TimeUnit.SECONDS));
|
|
|
|
|
+ } catch (TimeoutException e) {
|
|
|
|
|
+ log.error("配置搜索超时: {}", e.getMessage());
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("配置 {} 搜索失败: {}", cfgCode, e.getMessage(), e);
|
|
|
|
|
|
|
+ log.error("配置搜索异常: {}", e.getMessage(), e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
log.info("匹配完成,configCode: {},共返回 {} 条结果", param.getConfigCode(), result.size());
|
|
log.info("匹配完成,configCode: {},共返回 {} 条结果", param.getConfigCode(), result.size());
|
|
|
|
|
|
|
|
- // 从 Redis 获取视频基础信息并填充到结果中
|
|
|
|
|
- enrichVideoDetail(result);
|
|
|
|
|
- // 从 Redis 获取视频解构(选题 + 高价值实质点)并填充
|
|
|
|
|
- enrichDeconstruct(result);
|
|
|
|
|
|
|
+ if (withDetail) {
|
|
|
|
|
+ // 从 Redis 获取视频基础信息并填充到结果中
|
|
|
|
|
+ enrichVideoDetail(result);
|
|
|
|
|
+ // 从 Redis 获取视频解构(选题 + 高价值实质点)并填充
|
|
|
|
|
+ enrichDeconstruct(result);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
@@ -1356,7 +1397,7 @@ public class VideoSearchServiceImpl implements VideoSearchService {
|
|
|
matchParam.setQueryVector(param.getQueryVector());
|
|
matchParam.setQueryVector(param.getQueryVector());
|
|
|
matchParam.setTopN(param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 10);
|
|
matchParam.setTopN(param.getTopN() != null && param.getTopN() > 0 ? param.getTopN() : 10);
|
|
|
|
|
|
|
|
- List<VideoMatchResult> rawMatches = matchTopNVideo(matchParam);
|
|
|
|
|
|
|
+ List<VideoMatchResult> rawMatches = matchTopNVideo(matchParam, false);
|
|
|
if (rawMatches == null || rawMatches.isEmpty()) {
|
|
if (rawMatches == null || rawMatches.isEmpty()) {
|
|
|
log.info("recallWithScore: 召回结果为空");
|
|
log.info("recallWithScore: 召回结果为空");
|
|
|
return emptyScoreResult(alpha, rovP95, rovP5, simMin);
|
|
return emptyScoreResult(alpha, rovP95, rovP5, simMin);
|