这个项目构建了一个企业级文档智能搜索系统,核心解决传统关键词搜索在语义理解上的不足。我们团队采用Spring Boot作为基础框架,结合PostgreSQL的pgvector扩展,实现了从文档上传、向量化存储到语义检索的全流程解决方案。
系统最显著的特点是实现了"检索增强生成"(Retrieval-Augmented Generation)架构。简单来说,当用户提问时,系统会先检索最相关的文档片段,再将它们作为上下文喂给大语言模型生成回答。这种方式既避免了传统搜索的"关键词匹配"局限,又解决了大模型容易"胡编乱造"的问题。
从技术选型来看,pgvector扩展让我们能在熟悉的PostgreSQL环境中直接处理向量数据,省去了引入专用向量数据库的运维成本。实测下来,在千万级文档规模下,配合HNSW索引的查询延迟能稳定控制在200ms以内,完全满足企业级应用的需求。
向量存储是整个系统的基石,我们通过PgVectorStoreBuilder进行定制化配置:
java复制@Bean
public VectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel dashboardEmbeddingModel) {
return PgVectorStore.builder(jdbcTemplate, dashboardEmbeddingModel)
.dimensions(1536) // 适配DashScope模型输出维度
.distanceType(COSINE_DISTANCE) // 文本场景最优解
.indexType(HNSW) // 高性能近似搜索
.initializeSchema(true)
.schemaName("public")
.vectorTableName("document_store")
.maxDocumentBatchSize(10000)
.build();
}
几个关键参数的设计考量:
实际部署中发现,当向量表超过500万条记录时,建议额外配置
hnsw.ef_search=200参数来平衡查询精度和延迟。
RAG的核心是将检索器与生成模型串联:
java复制@Bean
public VectorStoreDocumentRetriever vectorStoreDocumentRetriever(VectorStore vectorStore) {
return VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.7) // 过滤低质量结果
.topK(5) // 控制上下文长度
.build();
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
VectorStoreDocumentRetriever documentRetriever) {
return builder
.defaultSystem("你是一个专业的文档助手...")
.defaultAdvisors(advisor -> advisor
.param("context", documentRetriever))
.build();
}
这里有两个经验参数值得注意:
采用经典的MVC分层架构,控制器层只处理HTTP协议相关逻辑:
java复制@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadDocument(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "metadata", required = false) Map<String, Object> metadata) {
try {
documentEmbeddingService.uploadAndStoreDocuments(file, metadata);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "文档上传成功",
"filename", file.getOriginalFilename(),
"size", file.getSize()
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of(
"success", false,
"message", "文档上传失败: " + e.getMessage()
));
}
}
原始的空行分割算法在实际业务中遇到几个问题:
改进后的解析器增加了以下处理逻辑:
java复制private List<String> parseTextFile(MultipartFile file) throws Exception {
List<String> paragraphs = new ArrayList<>();
StringBuilder currentParagraph = new StringBuilder();
boolean inCodeBlock = false;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
// 处理代码块边界
if (line.startsWith("```")) {
inCodeBlock = !inCodeBlock;
currentParagraph.append(line).append("\n");
continue;
}
// 代码块内保持原样
if (inCodeBlock) {
currentParagraph.append(line).append("\n");
continue;
}
// 普通文本处理
if (line.isEmpty()) {
if (!currentParagraph.isEmpty()) {
paragraphs.add(currentParagraph.toString());
currentParagraph = new StringBuilder();
}
} else {
if (!currentParagraph.isEmpty()) {
currentParagraph.append(" ");
}
currentParagraph.append(line);
}
}
if (!currentParagraph.isEmpty()) {
paragraphs.add(currentParagraph.toString());
}
}
return paragraphs;
}
考虑到Embedding API的速率限制和超时风险,我们实现了带重试机制的批次处理:
java复制int batchSize = 25;
for (int i = 0; i < paragraphs.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, paragraphs.size());
List<String> batchContents = paragraphs.subList(i, endIndex);
List<Map<String, Object>> batchMetadata = metadataList.subList(i, endIndex);
List<Document> batchDocuments = new ArrayList<>();
for (int j = 0; j < batchContents.size(); j++) {
batchDocuments.add(new Document(batchContents.get(j), batchMetadata.get(j)));
}
// 带指数退避的重试机制
executeWithRetry(() -> vectorStore.add(batchDocuments),
3, 1000, ExponentialBackoffPolicy.INSTANCE);
}
其中重试策略的关键参数:
在压力测试中发现,默认配置下批量插入性能较差。通过以下调整显著提升吞吐量:
sql复制ALTER SYSTEM SET shared_buffers = '4GB';
ALTER SYSTEM SET effective_cache_size = '12GB';
ALTER SYSTEM SET maintenance_work_mem = '2GB';
ALTER SYSTEM SET work_mem = '128MB';
ALTER SYSTEM SET random_page_cost = 1.1;
ALTER SYSTEM SET max_parallel_workers_per_gather = 4;
特别注意:
random_page_cost需要根据SSD存储调整为较低值,默认的4.0适用于机械硬盘
通过调整HNSW的构造参数,在查询性能和构建耗时之间取得平衡:
sql复制CREATE INDEX ON document_store USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
参数选择经验:
实现两级缓存提升高频查询响应速度:
java复制@Bean
public CaffeineCacheManager cacheManager() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats();
return new CaffeineCacheManager("queryCache", caffeine);
}
现象:插入时报错"vector dimension does not match"
排查步骤:
pgvector_store.dimensions配置值ALTER TABLE document_store ALTER COLUMN embedding SET DATA TYPE vector(1536)现象:明显相关的内容相似度却很低
解决方案:
SELECT vector_norm(embedding) FROM document_store LIMIT 10优化方案:
properties复制spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
结合传统BM25和向量搜索的优势:
java复制@GetMapping("/hybrid-search")
public List<Document> hybridSearch(@RequestBody SearchRequest request) {
// 关键词搜索
List<Document> keywordResults = keywordSearch(request.query());
// 向量搜索
List<Document> vectorResults = vectorStore.similaritySearch(request.query());
// 融合算法
return HybridSearchFusion.reciprocalRankFusion(
keywordResults, vectorResults);
}
支持基于文档属性的条件检索:
java复制VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.7)
.topK(5)
.filterExpression("metadata->>'department' = 'engineering'")
.build();
在检索前对用户query进行改写和扩展:
java复制String expandedQuery = queryUnderstandingEngine.expandQuery(originalQuery);
List<Document> results = vectorStore.similaritySearch(expandedQuery);
这个项目从零开始构建到上线部署共耗时6周,期间最大的收获是认识到:在AI工程化落地的过程中,算法精度只是基础,如何设计健壮的工程架构、高效的数据流水线以及精密的参数调优,才是决定系统最终效果的关键因素。特别是在处理企业级文档时,对文本预处理和元数据管理的重视程度,往往直接决定了检索质量的上限。