仓库向量搜索方法

工程 | Mark Paluch | 2025 年 5 月 23 日 | ...

大型语言模型 (LLM) 的出现推动了生成式 AI 的发展,并使其一个关键组件浮出水面,为广大受众所知:嵌入

嵌入是数据在高维空间中的向量表示,捕捉了它们的语义。向量表示可以更高效、更有效地搜索相似项目(向量搜索)。向量搜索通常用于构建检索增强生成 (RAG) 系统,因此对向量数据库的需求很大。

随着新型向量数据库的兴起,现有数据库引擎逐渐整合了向量搜索功能,从而形成了两种主要类型的数据库:

  • 专用向量数据库
  • 具有向量搜索功能的数据库引擎

专用向量数据库源于在高性能维度空间中搜索相似项的需求。它们为此目的进行了优化,通常使用专门的索引技术来提高搜索性能。示例包括 PineconeWeaviateMilvusQdrant。所有这些项目都出现在2020年代初期。

向量搜索通常需要一个向量(一个单精度float数字数组)、一个命名空间(类似于表或集合)和一个Top K(要返回的结果数量)参数。向量搜索然后运行近似最近邻(ANN)或k-最近邻(kNN)搜索。这些数据库允许额外的过滤相似性和元数据字段,但是,搜索的核心围绕向量表示。

现有数据库引擎,如Postgres (pgvector)、Oracle和MongoDB,已逐渐将向量搜索功能添加到其引擎中。它们不是专用向量数据库,而是具有向量搜索功能的通用数据库。它们的优势在于能够处理各种数据类型和查询,特别是在将向量搜索与传统查询结合时。它们还拥有悠久的历史,支持管理任务,并具有完善的备份和恢复、扩展和维护操作模型。另一个需要考虑的方面是,这些数据库已在生产中使用,包含大量现有数据。

显而易见的问题

Spring AI 与向量存储有广泛的集成。

显而易见的问题是:“为什么Spring AI支持向量搜索而Spring Data不支持?”这又有什么关系呢?

Spring AI的目标是通过提供一致的编程模型和抽象来简化构建AI驱动应用程序的过程。它专注于将AI功能集成到Spring应用程序中,并为使用各种AI模型和服务提供统一的API。

AI是一个热门话题:一些数据库供应商已将其向量搜索集成贡献给Spring AI,以实现检索增强生成等用例。这是开源如何推动数据库领域创新和协作的一个很好的例子。

当我们考虑AI炒作周期达到顶峰之后的情况时,我们面临的是第二天的运营。数据有生命周期,新的LLM模型不断出现和消失,有些比其他模型更适合某些任务或语言。虽然Spring AI的VectorStore在一定程度上能够反映数据生命周期,但这绝不是它的主要关注点。

这时Spring Data就发挥作用了。Spring Data的核心是数据模型、访问和数据生命周期。它提供了一致的编程模型来访问不同的数据存储,包括关系数据库、NoSQL数据库等。Spring Data的重点是简化数据访问和管理,使其更容易以一致的方式使用各种数据源。

那么,在Spring Data中拥有向量搜索功能不是很有意义吗?

是的,会的。

Spring Data 中的向量搜索

通过Spring Data 3.5,我们引入了Vector类型,以简化实体中向量数据的使用。

向量数据类型在典型的领域模型中并不常见。最接近向量数据的是地理空间数据类型,例如PointPolygon等,但即使是这些也不常见。领域模型更多地由反映其所用领域的原始类型和值类型组成。

向量属性要么使用供应商特定的类型(如Cassandra的CqlVector),要么使用某种数组,如float[]。在后一种情况下,使用数组会引入相当多的意外复杂性:Java数组是指针。它们的底层实际数组数据是可变的。携带数组也不是很常见。

您的领域模型可以利用Vector属性类型,从而降低意外修改底层数据的风险,并为原本的float[]赋予语义上下文。对于Spring Data处理对象映射的模块,Spring Data会处理Vector属性的持久化和检索。对于JPA,您将需要额外的转换器。

Vector vector = Vector.of(0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f);

Vector.of(…)创建不可变的Vector实例并复制给定的输入数组。虽然这适用于大多数场景,但希望减少GC压力的性能敏感型安排可以保留对原始数组的引用

Vector vector = Vector.unsafe(new float[]{0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f});

如果您想使用double[],可以通过调用toFloatArray()toDoubleArray()来获取float[]数组的安全(复制)变体。或者,您可以通过getSource()访问Vector的源。

根据您的数据存储,您可能需要为您的数据模型配备额外的注解,以指示例如维度数量或其精度。

当运行向量搜索操作时,每个数据库都使用非常不同的API。让我们看看MongoDB和Apache Cassandra。

在MongoDB中,向量搜索通过其聚合框架使用,需要一个聚合阶段

class Comment {

  String id;
  String language;
  String comment;
  Vector embedding;

  // getters, setters, …
}

VectorSearchOperation vectorSearch = VectorSearchOperation.search("euclidean-index")
  .path("embedding")
  .vector(0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f) // float[], Vector, or Mongo's BinaryVector
  .limit(10) // Top-K
  .numCandidates(200)
  .filter(where("language").is("DE"))
  .searchType(SearchType.ANN)
  .withSearchScore();

AggregationResults<Document> results = template.aggregate(newAggregation(vectorSearch),
  Comment.class, Document.class);

VectorSearchOperation提供了一个流畅的API,以方便的方式引导您完成操作的每个步骤,反映底层的MongoDB API。

我们来看看Apache Cassandra。Cassandra使用CQL (Cassandra Query Language) 对数据库运行查询。Cassandra向量搜索使用相同的方法。使用Cassandra,Spring Data用户可以选择使用Spring Data Cassandra的Query API或原生CQL API来运行向量搜索

@Table("comments")
class CommentVectorSearchResult {

  @Id String id;
  double score;
  String language;
  String comment;
  
  // getters, setters, …
}

CassandraTemplate template = …
Query query = Query.select(Columns.from("id", "language", "comment")
                                  .select("embedding", it -> it.similarity(Vector.of(1.1f, 2.2f)).cosine().as("score")))
  .sort(VectorSort.ann("embedding", Vector.of(1.1f, 2.2f)))
  .limit(10);

List<VectorSearchResult> select = template.select(query, VectorSearchResult.class);

CQL变体如下所示

CassandraTemplate template = …

List<Map<String, Object>> result = template.getCqlOperations().queryForList("""
    SELECT id, language, comment, similarity_cosine(embedding, ?0) AS score
    FROM my_table 
    ORDER BY embedding ANN OF ?0 
    LIMIT ?1
    """, CqlVector.newInstance(1.1f, 2.2f), 10);

总而言之,这些是Spring Data中如何使用向量搜索的小而简单的例子。

从领域模型的角度来看,表示搜索结果与表示领域模型本身是不同的。显然,人们可能更喜欢Java Records而不是类,但这并不是差异所在。您是否注意到额外的CommentVectorSearchResult类或List<Document>(MongoDB的原生文档类型)?Cassandra没有可用于消费结果的独立原始类型,因此我们需要CommentVectorSearchResult作为专用类型来映射此特定的Cassandra搜索结果。我们不仅想访问领域数据,还想访问分数。MongoDB的Java驱动程序附带了一个本质上是MapDocument类型。

这与我们设想的现代编程模型不符。

搜索项目时,结果不是领域对象的列表,而是搜索结果的列表。我们如何表示它们?我们不能有一个统一的编程模型,将表达我想要的简单性与底层数据库的强大功能结合起来吗?

向量搜索方法

如果它是搜索结果,那么它就是SearchResult<T>。如果存储库方法可以返回SearchResults<T>会怎样?搜索是一个与查询(查找)实体略有不同的概念。除此之外,搜索方法的运作方式将类似于现有的查询方法。

interface CommentRepository extends Repository<Comment, String> {

  @VectorSearch(indexName = "euclidean-index")
  SearchResults<Comment> searchTop10ByLanguageAndEmbeddingNear(String language, Vector vector,
      Similarity similarityThreshold);

  @VectorSearch(indexName = "euclidean-index")
  SearchResults<Comment> searchByLanguageAndEmbeddingWithin(String language, Vector vector,
      Range<Similarity> range, Limit topK);
}

SearchResults<Comment> results = repository.searchTop10ByLanguageAndEmbeddingNear("DE", Vector.of(0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f), Similarity.of(0.9));

for (SearchResult<Comment> result : results) {
  Score score= result.getScore();
  Comment comment = result.getContent();
  // …
}

上面的示例展示了一个运行向量搜索的搜索方法。在MongoDB中搜索需要一个特定于MongoDB的索引提示。除此之外,查询派生会创建预过滤器以按language进行过滤。通过利用NearWithin关键字,Spring Data MongoDB能够将给定的VectorSimilarity谓词与实际的向量搜索操作相关联。结果以SearchResults的形式返回,提供对找到的实体及其分数或相似度值的访问。

使用Postgres或Oracle进行向量搜索甚至更简单。以下示例通过Hibernate的hibernate-vector模块展示了Spring Data JPA存储库中的向量搜索方法

interface CommentRepository extends Repository<Comment, String> {

  SearchResults<Comment> searchTop10ByLanguageAndEmbeddingNear(String language, Vector vector,
      Score scoreThreshold);

  SearchResults<Comment> searchByLanguageAndEmbeddingWithin(String language, Vector vector,
      Range<Similarity> range, Limit topK);
}

NearWithin搜索方法需要一个Vector和一个ScoreSimilarity(Score的子类型)或Range<Score>参数来确定相似度/距离的计算方式。传统上,查询方法旨在表达查询的谓词,而典型的向量搜索更多地是关于Top-K限制。这是我们将来必须考虑的问题。

搜索方法也可以像查询方法一样利用注解。以下示例展示了Spring Data JPA中的搜索方法

interface CommentRepository extends Repository<Comment, Integer> {

  @Query("""
      SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c
      WHERE c.language = :language
          AND cosine_distance(c.embedding, :embedding) <= :distance
      ORDER BY cosine_distance(c.embedding, :embedding) asc
      """)
    SearchResults<Comment> searchAnnotatedByLanguage(String language, Vector embedding, Score distance);
}

虽然不明显,但pgvector和Oracle计算距离以计算分数。Spring Data允许直接消费原生分数,或者在使用具有适当ScoringFunctionSimilarity.of(…)作为参数时,Spring Data将原生分数标准化为0到1之间的相似度范围。

// Using a native score
SearchResults<Comment> results = repository.searchAnnotatedByLanguage(…, Score.of(0.1, ScoringFunction.cosine()));

// SearchResult.score will be an instance of Similarity
SearchResults<Comment> results = repository.searchAnnotatedByLanguage(…, Similarity.of(0.9, ScoringFunction.cosine()));

关于JPA中的Vector的最后一点说明。在使用JPA时,您可以在领域模型中使用Vector来存储向量,前提是您已配置AttributeConverterVector转换为数据库类型。但是,当使用Hibernate的距离方法(如cosine_distance)时,Hibernate不考虑任何Attribute Converter,因此您的模型必须使用float[]double[]作为嵌入类型

@Table
class Comment {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;
  
  private String language;
  private String comment;
  
  @JdbcTypeCode(SqlTypes.VECTOR)
  @Array(length = 5)
  private float[] embedding;
}

结论

我们探索了向量搜索的世界,以及它如何与Spring AI中的向量搜索起源一起融入Spring生态系统。对Vector类型的支持将随Spring Data 3.5 (2025.0) 的2025年5月版本一起发布。

向量搜索方法是Spring Data 4.0 (2025.1) 发布列车的预览功能,首批实现在JPA中通过Hibernate Vector、MongoDB和Apache Cassandra。我们很高兴听到您对向量搜索方法的看法以及我们如何进一步改进它们。

您可以在参考文档中找到有关向量搜索的文档,包括JPAMongoDBCassandra

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有