1. 项目概述
最近在开发一个基于Spring的智能搜索系统时,遇到了传统关键词搜索的局限性问题。当用户用自然语言提问时,简单的关键词匹配往往无法准确理解查询意图。比如搜索"适合雨天穿的外套",传统搜索引擎可能会返回所有包含"雨天"和"外套"的结果,而无法理解"防水"、"轻便"等隐含需求。
这个项目探索了如何将向量数据库和RAG(检索增强生成)技术整合到Spring应用中,构建更智能的语义搜索能力。通过将文本转换为向量表示,系统能够理解查询的语义含义,而不仅仅是字面匹配。RAG技术则进一步增强了结果的相关性和丰富度。
2. 核心架构设计
2.1 技术选型考量
在选择向量数据库时,我们评估了几个主流选项:
- Pinecone:全托管服务,API友好,但成本较高
- Milvus:开源方案,功能强大但运维复杂
- Weaviate:开源且内置向量搜索,支持混合搜索
- PostgreSQL+pgvector:利用现有数据库,迁移成本低
最终选择了Weaviate,主要基于以下考虑:
- 开源方案可以控制成本
- 内置的混合搜索能力(关键词+向量)
- 与Spring生态集成相对简单
- 支持自定义模型嵌入
2.2 系统架构图
整个系统分为三个主要层次:
-
数据预处理层:
- 文档拆分器(按段落/句子分割)
- 文本清洗(去除噪音、标准化)
- 嵌入模型(生成向量表示)
-
存储检索层:
- Weaviate向量数据库
- 传统关系型数据库(存储元数据)
- 缓存层(Redis)
-
应用服务层:
- Spring Boot REST API
- 查询理解模块
- RAG结果生成模块
3. 核心实现细节
3.1 数据预处理流程
文本预处理是影响搜索质量的关键环节。我们实现了以下处理步骤:
java复制// 示例文档处理流程
public class DocumentProcessor {
public List<TextChunk> process(Document doc) {
// 1. 文本标准化
String normalized = TextNormalizer.normalize(doc.getContent());
// 2. 分块处理
List<TextChunk> chunks = new TextSplitter().split(normalized);
// 3. 元数据提取
chunks.forEach(chunk -> {
chunk.setMetadata(extractMetadata(doc));
chunk.setEmbedding(generateEmbedding(chunk.getText()));
});
return chunks;
}
}
关键参数选择:
- 分块大小:512 tokens(适配BERT类模型)
- 重叠区域:64 tokens(保持上下文连贯)
- 嵌入模型:all-MiniLM-L6-v2(平衡质量与性能)
3.2 Weaviate集成配置
Spring中集成Weaviate的主要配置:
yaml复制# application.yml
weaviate:
host: https://your-cluster.weaviate.network
api-key: ${WEAVIATE_API_KEY}
batch:
size: 50
dynamic: true
schema:
classes:
- name: "DocumentChunk"
properties:
- name: "text"
dataType: ["text"]
- name: "source"
dataType: ["string"]
- name: "embedding"
dataType: ["number[]"]
重要提示:批量插入时建议启用dynamic配置,Weaviate会自动优化插入性能。我们实测批量大小为50时吞吐量最佳。
3.3 混合搜索实现
结合关键词和向量搜索的混合查询实现:
java复制public SearchResults hybridSearch(String query, int limit) {
// 1. 生成查询向量
float[] queryVector = embeddingService.generate(query);
// 2. 构建混合查询
HybridQuery hybridQuery = WeaviateClient.build()
.withQuery(query)
.withVector(queryVector)
.withAlpha(0.5) // 平衡关键词和语义权重
.withLimit(limit)
.build();
// 3. 执行查询
return weaviateClient.hybrid(hybridQuery).get();
}
参数调优经验:
- alpha=0.5 通常是不错的起点
- 对事实型查询可提高关键词权重(alpha>0.7)
- 对探索型查询可提高向量权重(alpha<0.3)
4. RAG集成方案
4.1 检索增强生成流程
RAG的核心流程分为三步:
- 检索相关文档:使用混合搜索获取top-k相关片段
- 上下文组装:将检索结果与提示词模板组合
- 生成回答:调用LLM生成最终响应
java复制public String generateAnswer(String question) {
// 1. 检索相关文档
List<DocumentChunk> chunks = hybridSearch(question, 3);
// 2. 构建提示词
String prompt = buildRAGPrompt(question, chunks);
// 3. 调用LLM生成
return llmClient.generate(prompt);
}
private String build[RAG](https://taotoken.net?utm_source=hardware)Prompt(String question, List<DocumentChunk> chunks) {
StringBuilder context = new StringBuilder();
chunks.forEach(chunk ->
context.append("来源: ").append(chunk.getSource())
.append("\n内容: ").append(chunk.getText()).append("\n\n"));
return String.format("""
基于以下上下文信息回答问题。如果无法从上下文中得到答案,请回答"我不知道"。
上下文:
%s
问题:%s
答案:""", context, question);
}
4.2 提示工程优化
经过多次测试,我们发现以下提示词结构效果最佳:
- 明确指令:指示模型基于给定上下文回答
- 引用要求:要求标注答案来源
- 安全边界:设置未知问题的响应策略
- 格式规范:使用清晰的段落分隔
优化后的提示模板:
code复制请严格根据提供的上下文信息回答用户问题。遵守以下规则:
1. 答案必须来自给定上下文
2. 引用使用的上下文片段[来源X]
3. 如果上下文不相关,回答"根据已有信息无法确定"
上下文:
{{context}}
问题:{{question}}
5. 性能优化实践
5.1 缓存策略实现
为减少重复计算,我们实现了三级缓存:
- 查询结果缓存:缓存常见查询的向量结果
- 嵌入缓存:缓存文本到向量的映射
- LLM响应缓存:缓存相同上下文的生成结果
Spring Cache配置示例:
java复制@Cacheable(value = "embeddings", key = "#text.hashCode()")
public float[] get[Embedding](https://taotoken.net?utm_source=hardware)(String text) {
return embeddingClient.generate(text);
}
@Cacheable(value = "ragAnswers", key = "{#question, #chunks.hashCode()}")
public String generateAnswer(String question, List<DocumentChunk> chunks) {
// 生成逻辑
}
5.2 批量处理优化
当需要处理大量文档时,我们采用以下优化手段:
- 并行嵌入生成:使用虚拟线程并行处理
- 批量数据库操作:利用Weaviate的批量API
- 流水线处理:将读取、处理、写入操作重叠
java复制// 并行处理文档示例
List<Document> documents = loadDocuments();
List<CompletableFuture<Void>> futures = documents.stream()
.map(doc -> CompletableFuture.runAsync(() ->
processAndStoreDocument(doc), virtualThreadExecutor))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
6. 常见问题排查
6.1 搜索质量不佳
症状:相关文档排名靠后
排查步骤:
- 检查嵌入模型是否适合领域
- 调整混合搜索的alpha参数
- 验证文本分块策略是否合理
- 检查向量维度是否匹配
解决方案:
- 尝试领域特定的嵌入模型(如临床BERT用于医疗)
- 增加查询扩展(同义词、术语扩展)
- 优化分块大小和重叠区域
6.2 生成结果不准确
症状:LLM产生与上下文矛盾的答案
排查步骤:
- 检查提示词是否明确要求基于上下文
- 验证检��到的上下文是否相关
- 测试不同温度(temperature)参数
解决方案:
- 在提示词中添加更严格的约束
- 实现后处理校验逻辑
- 降低temperature参数(0.3以下)
7. 部署注意事项
7.1 生产环境配置
推荐的生产环境配置:
| 组件 | 规格要求 | 说明 |
|---|---|---|
| Weaviate节点 | 8核16GB内存,100GB SSD | 每100万向量约需1GB内存 |
| 嵌入模型服务 | 4核8GB内存,GPU可选 | 依赖模型大小和QPS需求 |
| Spring应用 | 4核8GB内存 | 根据并发量线性扩展 |
| Redis缓存 | 内存足够存储热点数据和嵌入 | 建议至少4GB |
7.2 监控指标
必须监控的关键指标:
-
搜索性能:
- 查询延迟(P99<500ms)
- 每秒查询量(QPS)
- 缓存命中率
-
质量指标:
- 点击率(CTR)
- 结果相关性评分
- 生成答案的准确性
-
资源使用:
- 向量数据库内存占用
- 嵌入模型推理延迟
- LLM调用成本
实现示例:
java复制@RestController
public class SearchController {
@PostMapping("/search")
public ResponseEntity<SearchResults> search(@RequestBody SearchRequest request) {
Timer.Sample timer = Timer.start(Metrics.globalRegistry);
try {
SearchResults results = searchService.execute(request);
timer.stop(Metrics.globalRegistry.timer("search.latency"));
Metrics.counter("search.requests").increment();
return ResponseEntity.ok(results);
} catch (Exception e) {
Metrics.counter("search.errors").increment();
throw e;
}
}
}
8. 扩展与演进
8.1 多模态搜索
当前架构可以扩展支持多模态搜索:
- 增加图像/音频嵌入模型
- 扩展Weaviate schema支持多模态向量
- 实现跨模态联合搜索
java复制// 多模态搜索示例
public SearchResults multiModalSearch(String textQuery, byte[] image) {
float[] textVector = textEmbedding.generate(textQuery);
float[] imageVector = imageEmbedding.generate(image);
// 合并向量
float[] combined = combineVectors(textVector, imageVector);
return weaviateClient.vector(combined).get();
}
8.2 增量更新策略
实时数据更新的几种方案:
- 变更数据捕获(CDC):监听数据库binlog
- 消息队列:通过Kafka传递更新事件
- 定时扫描:对更新频率低的场景
Spring集成CDC的示例:
java复制@EventListener
public void handleDataChange(DocumentChangeEvent event) {
if (event.getType() == ChangeType.UPDATE) {
embeddingService.update(event.getDocument());
}
}
在实际项目中,我们从传统关键词搜索升级到语义搜索后,用户满意度提升了40%。最关键的经验是:不要追求完美的初始实现,而应该快速迭代。我们从最简单的向量搜索开始,然后逐步添加混合搜索、RAG等能力,每步都进行A/B测试验证效果。
