Spring Data向量搜索全解析!轻松构建智能检索系统

本文深入解析了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让开发者能够以熟悉的方式操作不同的向量数据库,极大地降低了技术门槛。虽然目前还是预览功能,但其设计理念和实现方式已经为未来的企业级应用奠定了坚实基础。