向量搜索实现原理详解
2026/9/24 15:14:19 网站建设 项目流程

概述

本文档详细分析了VectorServiceImpl.searchQuestion方法的实现原理,重点讲述文档过滤策略、内容截断问题的处理方案,以及如何保证检索结果的连贯性和准确性。

1. 整体架构流程

用户问题

向量化编码

Milvus向量搜索

相似度计算

topK筛选

阈值过滤

多策略内容提取

内容去重与合并

返回最终结果

2. 文档过滤机制详解

2.1 多层过滤策略

我们的向量搜索采用了多层渐进式过滤机制:

第一层:相似度阈值过滤
.similarityThreshold(0.3)// 只保留相似度 ≥ 0.3 的文档

设计原理:

  • 0.3 阈值选择:经过实际测试,0.3 是一个平衡点

    • 太高(如0.7):可能过滤掉语义相关但表达方式不同的文档
    • 太低(如0.1):会引入大量噪音文档
  • 向量相似度计算:使用余弦相似度公式

    similarity = (A · B) / (||A|| × ||B||)
第二层:topK 数量控制
.topK(5)// 最多返回5个最相似的文档

为什么选择5个?

  • 性能考虑:避免处理过多文档影响响应速度
  • 质量保证:前5个文档通常包含最相关的信息
  • 上下文限制:LLM 的 context window 有限制
第三层:内容有效性过滤
.filter(content->content!=null&&!content.trim().isEmpty())

过滤条件:

  • 非空检查:content != null
  • 空白字符检查:!content.trim().isEmpty()
  • 确保每个返回的内容都是有意义的文本

2.2 文档排序与优先级

搜索结果按以下优先级排序:

  1. 相似度得分(主要排序依据)
  2. 文档完整性(优先选择完整文档)
  3. 内容长度(适中长度的文档更有价值)

3. 内容截断问题的解决方案

3.1 截断问题的产生原因

在向量化过程中,长文档会被切分成多个 chunk:

原始文档: "董事长寄语:我们公司秉承创新、精细、品牌、诚信的理念,致力于为客户提供优质服务..." 切分后: Chunk1: "董事长寄语:我们公司秉承创新、精细、品牌、诚信的理念" Chunk2: "致力于为客户提供优质服务,不断追求卓越" Chunk3: "在未来的发展中,我们将继续坚持这一理念"

3.2 多策略内容提取机制

我们实现了渐进式内容提取策略:

// 策略1:优先使用 Spring AI 标准方法Stringcontent=doc.getText();// 策略2:尝试 metadata 中的 content 字段if(content==null||content.trim().isEmpty()){content=doc.getMetadata().getOrDefault("content","").toString();}// 策略3:尝试 metadata 中的 text 字段if(content==null||content.trim().isEmpty()){content=doc.getMetadata().getOrDefault("text","").toString();}// 策略4:尝试 metadata 中的 data 字段if(content==null||content.trim().isEmpty()){content=doc.getMetadata().getOrDefault("data","").toString();}

每种策略的适用场景:

策略方法适用场景优势
1doc.getText()标准 Spring AI 文档性能最优,API 标准
2metadata.content自定义存储格式灵活性高,支持自定义字段
3metadata.text文本类文档兼容性好,通用性强
4metadata.data结构化数据支持复杂数据结构

3.3 内容连贯性保证机制

方案一:上下文窗口扩展(推荐实现)
publicList<String>searchQuestionWithContext(Stringquestion){// 1. 执行向量搜索List<Document>documents=vectorStore.similaritySearch(searchRequest);// 2. 为每个文档查找相邻的 chunkList<String>enhancedResults=newArrayList<>();for(Documentdoc:documents){StringchunkId=doc.getMetadata().get("chunk_id").toString();StringdocId=doc.getMetadata().get("document_id").toString();// 查找相邻的 chunkList<Document>contextChunks=findAdjacentChunks(docId,chunkId);// 合并内容StringmergedContent=mergeChunks(contextChunks);enhancedResults.add(mergedContent);}returnenhancedResults;}privateList<Document>findAdjacentChunks(StringdocId,StringchunkId){// 查找 chunk_id-1, chunk_id, chunk_id+1SearchRequestcontextRequest=SearchRequest.builder().filter(newFilter.Expression(Filter.ExpressionType.AND,List.of(newFilter.Expression(Filter.ExpressionType.EQ,"document_id",docId),newFilter.Expression(Filter.ExpressionType.IN,"chunk_id",Arrays.asList(chunkId-1,chunkId,chunkId+1))))).build();returnvectorStore.similaritySearch(contextRequest);}
方案二:重叠窗口策略

在文档切分时就考虑连贯性:

// 文档切分时使用重叠窗口publicList<String>splitDocumentWithOverlap(Stringdocument,intchunkSize,intoverlapSize){List<String>chunks=newArrayList<>();intstart=0;while(start<document.length()){intend=Math.min(start+chunkSize,document.length());Stringchunk=document.substring(start,end);// 确保在句子边界切分if(end<document.length()){intlastPeriod=chunk.lastIndexOf('。');intlastExclamation=chunk.lastIndexOf('!');intlastQuestion=chunk.lastIndexOf('?');intsentenceEnd=Math.max(Math.max(lastPeriod,lastExclamation),lastQuestion);if(sentenceEnd>chunk.length()*0.7){// 如果句子边界在后70%位置end=start+sentenceEnd+1;chunk=document.substring(start,end);}}chunks.add(chunk);start=end-overlapSize;// 重叠部分}returnchunks;}

4. 高级优化策略

4.1 智能去重机制

privateList<String>deduplicateResults(List<String>results){Set<String>seen=newHashSet<>();List<String>deduplicated=newArrayList<>();for(Stringcontent:results){// 计算内容的哈希值或使用编辑距离Stringsignature=calculateContentSignature(content);if(!seen.contains(signature)){seen.add(signature);deduplicated.add(content);}}returndeduplicated;}privateStringcalculateContentSignature(Stringcontent){// 移除标点符号和空格,计算核心内容的哈希Stringnormalized=content.replaceAll("[\\p{Punct}\\s]+","");returnInteger.toString(normalized.hashCode());}

4.2 语义相关性增强

privateList<String>enhanceSemanticRelevance(Stringquestion,List<String>results){returnresults.stream().map(content->{// 计算与问题的语义相关度doublerelevanceScore=calculateSemanticRelevance(question,content);returnnewScoredContent(content,relevanceScore);}).filter(scored->scored.score>0.5)// 过滤低相关度内容.sorted((a,b)->Double.compare(b.score,a.score))// 按相关度排序.map(scored->scored.content).collect(Collectors.toList());}

4.3 动态阈值调整

privatedoublecalculateDynamicThreshold(Stringquestion){// 根据问题的复杂度和长度动态调整阈值intquestionLength=question.length();intcomplexityScore=calculateQuestionComplexity(question);doublebaseThreshold=0.3;// 问题越复杂,阈值越低(更宽松)if(complexityScore>5){baseThreshold-=0.1;}// 问题越短,阈值越高(更严格)if(questionLength<10){baseThreshold+=0.1;}returnMath.max(0.1,Math.min(0.8,baseThreshold));}

5. 性能优化与监控

5.1 缓存策略

@Cacheable(value="vectorSearch",key="#question")publicList<String>searchQuestion(Stringquestion){// 实际搜索逻辑}

5.2 异步处理

@AsyncpublicCompletableFuture<List<String>>searchQuestionAsync(Stringquestion){returnCompletableFuture.completedFuture(searchQuestion(question));}

5.3 监控指标

// 记录关键性能指标log.info("向量搜索性能指标 - 问题长度: {}, 返回文档数: {}, 耗时: {}ms",question.length(),documents.size(),duration);// 记录搜索质量指标log.info("搜索质量指标 - 平均相似度: {}, 内容完整率: {}%",averageSimilarity,contentCompletenessRate);

6. 实际应用场景

6.1 董事长寄语查询示例

用户问题: “董事长寄语是什么”

处理流程:

  1. 问题向量化:[0.1, 0.8, -0.3, 0.5, ...]

  2. 向量搜索: 在 Milvus 中查找相似文档

  3. 结果筛选:

    文档1: 相似度 0.85 - "董事长寄语:我们公司秉承..." 文档2: 相似度 0.72 - "创新、精细、品牌、诚信是我们的核心理念..." 文档3: 相似度 0.45 - "公司发展历程中,董事长多次强调..."
  4. 内容提取与合并

  5. 返回完整答案

6.2 技术问题查询示例

用户问题: “如何实现 Spring Boot 自动配置”

处理策略:

  • 使用更严格的阈值 (0.4) 确保技术准确性
  • 优先返回代码示例和配置文件
  • 合并相关的多个技术文档片段

7. 总结

我们的向量搜索实现通过以下机制保证了高质量的检索结果:

  1. 多层过滤: 相似度阈值 + topK限制 + 内容有效性检查
  2. 容错机制: 多策略内容提取,适应不同数据格式
  3. 连贯性保证: 上下文窗口扩展 + 重叠切分策略
  4. 智能优化: 去重、语义增强、动态阈值调整
  5. 性能监控: 缓存、异步处理、关键指标记录

这种设计确保了即使在复杂的企业知识库环境中,也能准确、快速地检索到用户需要的信息,为后续的 LLM 生成提供高质量的上下文支持。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询