概述
本文档详细分析了VectorServiceImpl.searchQuestion方法的实现原理,重点讲述文档过滤策略、内容截断问题的处理方案,以及如何保证检索结果的连贯性和准确性。
1. 整体架构流程
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 文档排序与优先级
搜索结果按以下优先级排序:
- 相似度得分(主要排序依据)
- 文档完整性(优先选择完整文档)
- 内容长度(适中长度的文档更有价值)
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();}每种策略的适用场景:
| 策略 | 方法 | 适用场景 | 优势 |
|---|---|---|---|
| 1 | doc.getText() | 标准 Spring AI 文档 | 性能最优,API 标准 |
| 2 | metadata.content | 自定义存储格式 | 灵活性高,支持自定义字段 |
| 3 | metadata.text | 文本类文档 | 兼容性好,通用性强 |
| 4 | metadata.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 董事长寄语查询示例
用户问题: “董事长寄语是什么”
处理流程:
问题向量化:
[0.1, 0.8, -0.3, 0.5, ...]向量搜索: 在 Milvus 中查找相似文档
结果筛选:
文档1: 相似度 0.85 - "董事长寄语:我们公司秉承..." 文档2: 相似度 0.72 - "创新、精细、品牌、诚信是我们的核心理念..." 文档3: 相似度 0.45 - "公司发展历程中,董事长多次强调..."内容提取与合并
返回完整答案
6.2 技术问题查询示例
用户问题: “如何实现 Spring Boot 自动配置”
处理策略:
- 使用更严格的阈值 (0.4) 确保技术准确性
- 优先返回代码示例和配置文件
- 合并相关的多个技术文档片段
7. 总结
我们的向量搜索实现通过以下机制保证了高质量的检索结果:
- 多层过滤: 相似度阈值 + topK限制 + 内容有效性检查
- 容错机制: 多策略内容提取,适应不同数据格式
- 连贯性保证: 上下文窗口扩展 + 重叠切分策略
- 智能优化: 去重、语义增强、动态阈值调整
- 性能监控: 缓存、异步处理、关键指标记录
这种设计确保了即使在复杂的企业知识库环境中,也能准确、快速地检索到用户需要的信息,为后续的 LLM 生成提供高质量的上下文支持。