向量搜索实现原理详解
概述
本文档详细分析了 VectorServiceImpl.searchQuestion 方法的实现原理,重点讲述文档过滤策略、内容截断问题的处理方案,以及如何保证检索结果的连贯性和准确性。
1. 整体架构流程
graph TD
A[用户问题] --> B[向量化编码]
B --> C[Milvus向量搜索]
C --> D[相似度计算]
D --> E[topK筛选]
E --> F[阈值过滤]
F --> G[多策略内容提取]
G --> H[内容去重与合并]
H --> I[返回最终结果]
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 标准方法
String content = 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 内容连贯性保证机制
方案一:上下文窗口扩展(推荐实现)
public List<String> searchQuestionWithContext(String question) {
// 1. 执行向量搜索
List<Document> documents = vectorStore.similaritySearch(searchRequest);
// 2. 为每个文档查找相邻的 chunk
List<String> enhancedResults = new ArrayList<>();
for (Document doc : documents) {
String chunkId = doc.getMetadata().get("chunk_id").toString();
String docId = doc.getMetadata().get("document_id").toString();
// 查找相邻的 chunk
List<Document> contextChunks = findAdjacentChunks(docId, chunkId);
// 合并内容
String mergedContent = mergeChunks(contextChunks);
enhancedResults.add(mergedContent);
}
return enhancedResults;
}
private List<Document> findAdjacentChunks(String docId, String chunkId) {
// 查找 chunk_id-1, chunk_id, chunk_id+1
SearchRequest contextRequest = SearchRequest.builder()
.filter(new Filter.Expression(
Filter.ExpressionType.AND,
List.of(
new Filter.Expression(Filter.ExpressionType.EQ, "document_id", docId),
new Filter.Expression(Filter.ExpressionType.IN, "chunk_id",
Arrays.asList(chunkId-1, chunkId, chunkId+1))
)
))
.build();
return vectorStore.similaritySearch(contextRequest);
}
方案二:重叠窗口策略
在文档切分时就考虑连贯性:
// 文档切分时使用重叠窗口
public List<String> splitDocumentWithOverlap(String document, int chunkSize, int overlapSize) {
List<String> chunks = new ArrayList<>();
int start = 0;
while (start < document.length()) {
int end = Math.min(start + chunkSize, document.length());
String chunk = document.substring(start, end);
// 确保在句子边界切分
if (end < document.length()) {
int lastPeriod = chunk.lastIndexOf('。');
int lastExclamation = chunk.lastIndexOf('!');
int lastQuestion = chunk.lastIndexOf('?');
int sentenceEnd = 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; // 重叠部分
}
return chunks;
}
4. 高级优化策略
4.1 智能去重机制
private List<String> deduplicateResults(List<String> results) {
Set<String> seen = new HashSet<>();
List<String> deduplicated = new ArrayList<>();
for (String content : results) {
// 计算内容的哈希值或使用编辑距离
String signature = calculateContentSignature(content);
if (!seen.contains(signature)) {
seen.add(signature);
deduplicated.add(content);
}
}
return deduplicated;
}
private String calculateContentSignature(String content) {
// 移除标点符号和空格,计算核心内容的哈希
String normalized = content.replaceAll("[\\p{Punct}\\s]+", "");
return Integer.toString(normalized.hashCode());
}
4.2 语义相关性增强
private List<String> enhanceSemanticRelevance(String question, List<String> results) {
return results.stream()
.map(content -> {
// 计算与问题的语义相关度
double relevanceScore = calculateSemanticRelevance(question, content);
return new ScoredContent(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 动态阈值调整
private double calculateDynamicThreshold(String question) {
// 根据问题的复杂度和长度动态调整阈值
int questionLength = question.length();
int complexityScore = calculateQuestionComplexity(question);
double baseThreshold = 0.3;
// 问题越复杂,阈值越低(更宽松)
if (complexityScore > 5) {
baseThreshold -= 0.1;
}
// 问题越短,阈值越高(更严格)
if (questionLength < 10) {
baseThreshold += 0.1;
}
return Math.max(0.1, Math.min(0.8, baseThreshold));
}
5. 性能优化与监控
5.1 缓存策略
@Cacheable(value = "vectorSearch", key = "#question")
public List<String> searchQuestion(String question) {
// 实际搜索逻辑
}
5.2 异步处理
@Async
public CompletableFuture<List<String>> searchQuestionAsync(String question) {
return CompletableFuture.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 生成提供高质量的上下文支持。

浙公网安备 33010602011771号