本文深入解析了Spring Data框架与向量数据库的集成方案,详细介绍了在PGvector和MongoDB中实现向量搜索的具体方法,为开发者构建AI应用提供了完整的技术指南。
在人工智能技术飞速发展的今天,向量搜索已经成为构建智能应用的核心技术之一。无论是电商平台的商品推荐,还是智能客服的问题匹配,背后都离不开高效的向量检索。
Spring Data的向量搜索革命
传统Spring Data仓库通过findBy前缀的方法名约定,让开发者能够轻松构建数据库查询。而现在,随着4.0.0-M6版本的推出,Spring Data正式进军向量搜索领域,引入了searchBy和searchTop等全新关键字,配合Within或Near等相似度限定词,使得向量搜索变得前所未有的简单。
这一创新意味着什么呢?想象一下,你正在开发一个智能文档检索系统。过去要实现基于语义的相似文档搜索,需要编写复杂的向量计算代码和数据库查询语句。而现在,只需要在Repository接口中定义一个名为searchByEmbeddingNear的方法,Spring Data就能自动生成相应的向量搜索查询,大大提升了开发效率。
核心概念解析
要真正理解Spring Data的向量搜索能力,我们需要先掌握几个关键概念:
- Vector类代表着查询向量本身,通常是一组浮点数构成的数组。
- SearchResults封装了搜索结果集合,而SearchResult则包装了单个匹配结果及其相似度分数。
- Score类表示存储向量与查询向量的接近程度,
- Similarity定义了向量比较的相似度度量方式,
- Range则用于限定查询的分数范围。
这些类共同构成了Spring Data向量搜索的基础架构,无论底层使用的是PGvector还是MongoDB,开发者都能使用统一的API进行操作,这种设计极大地降低了学习成本。
PGvector集成实战
PostgreSQL凭借其强大的扩展能力,通过pgvector扩展实现了向量存储和检索功能。在Spring Data中集成PGvector首先需要在pom.xml中添加相关依赖,包括Spring Boot JPA starter、PostgreSQL驱动和Hibernate vector库。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>4.0.0-M2</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.7</version> </dependency> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-vector</artifactId> <version>7.1.0.Final</version> </dependency> </dependencies>
|
数据库设置阶段需要先启用vector扩展,然后创建包含vector类型字段的表。
数据库初始化脚本
PGvector数据库初始化:
-- 启用vector扩展 CREATE EXTENSION IF NOT EXISTS vector;
-- 创建图书表 CREATE TABLE Book ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, embedding VECTOR(5) NOT NULL, year_published VARCHAR(10) NOT NULL );
-- 创建向量索引 CREATE INDEX book_embedding_idx ON book USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- 插入示例数据 INSERT INTO book (content, embedding, year_published) VALUES ('Spring Boot Basics', '[-0.49966827034950256, -0.025236541405320168, 0.736327588558197, -0.20225830376148224, 0.4081762731075287]'::vector, '2022'), ('Spring Boot Advanced', '[-0.20951677858829498, 0.17629066109657288, 0.7875414490699768, -0.13002122938632965, 0.5365606546401978]'::vector, '2022'), ('Django Web Development', '[0.14523187279701233, -0.30941757559776306, 0.6547893285751343, 0.28763412523269653, -0.19874526357650757]'::vector, '2023'), ('React Frontend Guide', '[0.3876124711036682, 0.4567893265991211, -0.23456788063049316, 0.12345679104328156, 0.3456789047716675]'::vector, '2023');
|
PGvector实体类定义:
@Entity(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;
private String content;
@Column(name = "year_published")
private String yearPublished;
@JdbcTypeCode(SqlTypes.VECTOR)
@Array(length = 5)
private float embedding;
// 构造函数
public Book() {}
public Book(String content, String yearPublished, float embedding) {
this.content = content;
this.yearPublished = yearPublished;
this.embedding = embedding;
}
// Getter和Setter方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getYearPublished() { return yearPublished; }
public void setYearPublished(String yearPublished) {
this.yearPublished = yearPublished;
}
public float getEmbedding() { return embedding; }
public void setEmbedding(float embedding) {
this.embedding = embedding;
}
}
在实体类定义中,需要使用@JdbcTypeCode(SqlTypes.VECTOR)和@Array注解来映射向量字段,确保Java对象与数据库表结构的正确对应。
Repository接口的定义展现了Spring Data的真正魅力。通过声明searchByYearPublishedAndEmbeddingNear等方法,开发者可以轻松组合传统字段过滤和向量相似度搜索。在执行查询时,只需传入年份、查询向量和相似度阈值,Spring Data就会自动生成优化的查询语句,返回符合条件的结果。
@Repository("pgvectorBookRepository") public interface PGvectorBookRepository extends JpaRepository<Book, String> { // 基于年份和向量相似度的搜索 SearchResults<Book> searchByYearPublishedAndEmbeddingNear( String yearPublished, Vector vector, Score scoreThreshold ); // 在指定相似度范围内的搜索 SearchResults<Book> searchByYearPublishedAndEmbeddingWithin( String yearPublished, Vector vector, Range<Similarity> range, Limit topK ); // 纯向量相似度搜索 SearchResults<Book> searchByEmbeddingNear(Vector vector, Score score); // 限制返回数量的向量搜索 SearchResults<Book> searchTop5ByEmbeddingNear(Vector vector, Score score); }
|
MongoDB向量搜索实现
与PGvector不同,MongoDB作为文档数据库,其向量搜索能力是内置的。在依赖方面,只需要引入Spring Boot MongoDB starter即可。数据准备阶段可以从CSV文件读取数据,并创建相应的向量索引。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> <version>4.0.0-M2</version> </dependency>
|
MongoDB实体类定义:
@Document(collection = "books")
public class Book {
@Id
private String id;
private String name;
private String yearPublished;
private Vector embedding;
public Book() {}
public Book(String id, String name, String yearPublished, Vector embedding) {
this.id = id;
this.name = name;
this.yearPublished = yearPublished;
this.embedding = embedding;
}
// Getter和Setter方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getYearPublished() { return yearPublished; }
public void setYearPublished(String yearPublished) {
this.yearPublished = yearPublished;
}
public Vector getEmbedding() { return embedding; }
public void setEmbedding(Vector embedding) {
this.embedding = embedding;
}
}
MongoDB的Repository定义需要使用@VectorSearch注解来指定向量索引名称和搜索参数,如结果数量限制和候选集大小。虽然底层实现与PGvector不同,但API设计保持了一致性,这种统一的设计理念让开发者能够轻松在不同数据存储方案间切换。
@Repository("mongoDbBookRepository") public interface MongoDbBookRepository extends MongoRepository<Book, String> { @VectorSearch(indexName = "book-vector-index", limit = "10", numCandidates = "200") SearchResults<Book> searchByYearPublishedAndEmbeddingNear( String yearPublished, Vector vector, Score score ); @VectorSearch(indexName = "book-vector-index", limit = "10", numCandidates = "200") SearchResults<Book> searchByYearPublishedAndEmbeddingWithin( String yearPublished, Vector vector, Range<Similarity> range ); @VectorSearch(indexName = "book-vector-index", limit = "5", numCandidates = "100") SearchResults<Book> searchTop5ByEmbeddingNear(Vector vector, Score score); }
|
业务逻辑层:实现智能搜索
向量搜索服务类:
@Service public class VectorSearchService { @Autowired @Qualifier("pgvectorBookRepository") private PGvectorBookRepository pgvectorRepository; @Autowired @Qualifier("mongoDbBookRepository") private MongoDbBookRepository mongoRepository; /<strong> * 使用PGvector进行混合条件搜索 */ public List<Book> searchBooksWithPGvector(String query, String year, double threshold) { // 将查询文本转换为向量 Vector queryVector = getEmbedding(query); // 执行向量搜索 SearchResults<Book> results = pgvectorRepository.searchByYearPublishedAndEmbeddingNear( year, queryVector, Score.of(threshold, ScoringFunction.cosine()) ); return results.getContent().stream() .map(SearchResult::getContent) .collect(Collectors.toList()); } /</strong> * 使用MongoDB进行范围相似度搜索 */ public List<Book> searchBooksWithMongoDB(String query, String year, double minScore, double maxScore) { Vector queryVector = getEmbedding(query); Range<Similarity> range = Range.closed( Similarity.of(minScore), Similarity.of(maxScore) ); SearchResults<Book> results = mongoRepository.searchByYearPublishedAndEmbeddingWithin( year, queryVector, range ); return results.getContent().stream() .map(SearchResult::getContent) .collect(Collectors.toList()); } /** * 模拟获取文本向量的方法 * 实际项目中这里会调用Embedding模型API */ private Vector getEmbedding(String text) { // 这里应该是调用OpenAI、Cohere等Embedding服务 // 暂时返回模拟向量数据 float vectorData = {-0.499668f, -0.025236f, 0.736327f, -0.202258f, 0.408176f}; return Vector.of(vectorData); } }
|
测试用例:验证搜索效果
PGvector搜索测试:
@SpringBootTest class PGvectorSearchTest { @Autowired private PGvectorBookRepository pgvectorBookRepository; @Test void whenSearchByYearPublishedAndEmbeddingNear_thenReturnRelevantResults() { // 准备测试数据 Vector embedding = getEmbedding("Which document has the details about Django?"); // 执行搜索 SearchResults<Book> results = pgvectorBookRepository.searchByYearPublishedAndEmbeddingNear( "2022", embedding, Score.of(0.9, ScoringFunction.euclidean()) ); // 验证结果 assertThat(results).isNotNull(); List<SearchResult<Book>> resultList = results.getContent(); assertThat(resultList.size()).isGreaterThan(0); resultList.forEach(book -> { assertThat(book.getContent().getYearPublished()).isEqualTo("2022"); assertThat(book.getScore().getValue()).isGreaterThanOrEqualTo(0.9); }); } @Test void whenSearchWithSimilarityRange_thenReturnFilteredResults() { Vector embedding = getEmbedding("Spring Boot tutorials"); Range<Similarity> range = Range.closed( Similarity.of(0.7, ScoringFunction.cosine()), Similarity.of(0.9, ScoringFunction.cosine()) ); SearchResults<Book> results = pgvectorBookRepository.searchByYearPublishedAndEmbeddingWithin( "2022", embedding, range, Limit.of(5) ); assertThat(results).isNotNull(); List<SearchResult<Book>> resultList = results.getContent(); // 验证结果数量和分数范围 assertThat(resultList.size()).isGreaterThan(0).isLessThanOrEqualTo(5); resultList.forEach(book -> { assertThat(book.getContent().getYearPublished()).isEqualTo("2022"); assertThat(book.getScore().getValue()).isBetween(0.7, 0.9); }); } private Vector getEmbedding(String text) { // 模拟向量生成 float vectorData = {-0.209516f, 0.176290f, 0.787541f, -0.130021f, 0.536560f}; return Vector.of(vectorData); } }
|
MongoDB搜索测试:
@SpringBootTest class MongoDBVectorSearchTest { @Autowired private MongoDbBookRepository mongoDbBookRepository; @Test void whenSearchWithMongoDB_thenReturnRelevantDocuments() { Vector embedding = getEmbedding("Which document has the details about Django?"); SearchResults<Book> results = mongoDbBookRepository.searchByYearPublishedAndEmbeddingNear( "2022", embedding, Score.of(0.9) ); List<SearchResult<Book>> resultList = results.getContent(); assertThat(resultList.size()).isGreaterThan(0); resultList.forEach(content -> { Book book = content.getContent(); assertThat(book.getYearPublished()).isEqualTo("2022"); System.out.println("找到匹配文档: " + book.getName() + ", 相似度: " + content.getScore().getValue()); }); } @Test void whenSearchWithinScoreRange_thenReturnPreciseResults() { Vector embedding = getEmbedding("Advanced Spring Boot features"); Range<Similarity> range = Range.closed(Similarity.of(0.7), Similarity.of(0.9)); SearchResults<Book> results = mongoDbBookRepository.searchByYearPublishedAndEmbeddingWithin( "2022", embedding, range ); assertThat(results).isNotNull(); List<SearchResult<Book>> resultList = results.getContent(); assertThat(resultList.size()).isGreaterThan(0).isLessThanOrEqualTo(10); resultList.forEach(book -> { assertThat(book.getContent().getYearPublished()).isEqualTo("2022"); assertThat(book.getScore().getValue()).isBetween(0.7, 0.9); }); } private Vector getEmbedding(String text) { float vectorData = {-0.499668f, -0.025236f, 0.736327f, -0.202258f, 0.408176f}; return Vector.of(vectorData); } }
|
实际应用场景示例
智能文档检索系统:
@RestController @RequestMapping("/api/search") public class DocumentSearchController { @Autowired private VectorSearchService vectorSearchService; @PostMapping("/semantic") public ResponseEntity<List<Book>> semanticSearch( @RequestBody SearchRequest request) { List<Book> results = vectorSearchService.searchBooksWithPGvector( request.getQuery(), request.getYearFilter(), 0.8 // 相似度阈值 ); return ResponseEntity.ok(results); } @PostMapping("/hybrid") public ResponseEntity<SearchResponse> hybridSearch( @RequestBody HybridSearchRequest request) { // 传统关键词搜索 List<Book> keywordResults = keywordSearch(request.getQuery()); // 向量语义搜索 List<Book> vectorResults = vectorSearchService.searchBooksWithMongoDB( request.getQuery(), request.getYearFilter(), 0.7, 0.95 ); // 结果融合与排序 List<Book> mergedResults = mergeAndRankResults(keywordResults, vectorResults); SearchResponse response = new SearchResponse(); response.setResults(mergedResults); response.setTotalCount(mergedResults.size()); return ResponseEntity.ok(response); } // 其他辅助方法... private List<Book> keywordSearch(String query) { // 实现传统关键词搜索逻辑 return new ArrayList<>(); } private List<Book> mergeAndRankResults(List<Book> keywordResults, List<Book> vectorResults) { // 实现结果融合和重排序逻辑 return vectorResults; // 简化实现 } }
|
技术细节深度剖析
在实际使用中,两种数据存储方案都表现出了优异的性能。以搜索“关于Django的文档”为例,系统会先将查询文本转换为向量表示,然后在数据库中寻找最相似的文档向量。通过设置合适的相似度阈值或范围,可以精确控制返回结果的相关性。
特别值得注意的是,Spring Data支持多种相似度计算函数,包括欧氏距离和余弦相似度等。开发者可以根据具体场景选择最适合的算法,欧氏距离更适合衡量绝对差异,而余弦相似度更关注方向一致性。
最佳实践与性能优化
在实际项目中,向量搜索的性能优化至关重要。合理设置向量维度、创建适当的索引、调整查询参数都能显著提升搜索效率。对于PGvector,需要注意向量维度的选择,过高的维度会增加计算开销,过低的维度可能影响搜索精度。对于MongoDB,numCandidates参数的设置会影响搜索质量和性能的平衡。
另一个重要考量是混合查询的优化。结合传统字段过滤和向量搜索时,查询顺序和索引设计都会影响最终性能。通常建议先使用高选择性字段过滤,再进行向量搜索,以减少需要比较的向量数量。
向量索引优化:
@Configuration public class VectorSearchConfig { @Bean public PGvectorBookRepositoryCustom pgvectorBookRepositoryCustom() { return new PGvectorBookRepositoryCustom() { // 自定义查询优化 }; } }
|
查询优化配置:
spring: data: jpa: repositories: enabled: true jpa: properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect show-sql: true vector: search: default-limit: 10 max-candidates: 200 similarity-threshold: 0.7
|
通过以上完整的代码实现,我们可以看到Spring Data为向量搜索提供了统一而强大的抽象层。无论底层使用PGvector还是MongoDB,开发者都能使用相似的API进行向量搜索操作,这极大地简化了AI应用的开发复杂度。
未来展望与应用场景
Spring Data对向量搜索的支持目前仍处于预览阶段,但已经展现出了巨大的潜力。随着人工智能应用的普及,向量搜索的需求必将持续增长。这项技术可以广泛应用于智能客服、内容推荐、图像检索、欺诈检测等多个领域。
以电商行业为例,通过向量搜索可以实现真正意义上的语义商品搜索。用户描述“适合海滩度假的连衣裙”,系统能够理解其语义,而非简单匹配关键词,从而提供更精准的搜索结果。在内容平台中,向量搜索可以实现智能内容去重和相似内容推荐,提升用户体验。
总结
这篇文章为我们全面展示了Spring Data在向量搜索领域的最新进展。通过统一的API设计,Spring Data让开发者能够以熟悉的方式操作不同的向量数据库,极大地降低了技术门槛。虽然目前还是预览功能,但其设计理念和实现方式已经为未来的企业级应用奠定了坚实基础。