Spring Blog

Using Your Own Embeddings in Spring AI 2.1

8.5内容质量

TL;DR · AI 摘要

Spring AI 2.1 新增 upsert 方法,支持自定义嵌入向量提升搜索灵活性,适用于图像、视频等多模态数据场景。

核心要点

  • Spring AI 2.1 新增 upsert 方法可直接写入自定义嵌入向量,避免重复生成
  • similaritySearch 支持传入查询嵌入向量,实现精准匹配
  • 多模态场景下可将图片/视频嵌入向量与文本查询直接对比

结构提纲

按章节快速跳转。

  1. 介绍 Spring AI 2.1 新增 upsert 方法的核心价值

  2. 说明 upsert 如何绕过默认嵌入模型直接写入向量

  3. 通过家具目录示例展示图片嵌入向量的使用场景

  4. 描述 similaritySearch 重载方法对查询向量的支持

  5. 提供 VertexAiMultimodalEmbeddingModel 的代码示例

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Spring AI 2.1 新特性
    • upsert 方法
      • 绕过默认嵌入模型
      • 支持多模态数据
    • similaritySearch 优化
      • 接受查询嵌入向量
    • 应用场景
      • 图像搜索
      • 视频检索
      • 音频匹配

金句 / Highlights

值得收藏与分享的关键句。

#Spring AI#向量存储#嵌入向量#多模态搜索
打开原文

VectorStore.add() 方法接收文本文档,使用存储配置的模型生成嵌入向量,并保存两者。Spring AI 2.1 为已有嵌入向量或从图像、视频、音频生成嵌入向量的应用程序新增了 upsert 方法。该方法接受文档与应用程序提供的向量配对,无需调用存储的嵌入模型。新的 similaritySearch 方法重载也接受查询嵌入向量,使应用程序能够完全控制索引和搜索过程。

何时需要提供自定义嵌入向量

让我们看一个现实案例。假设有一个家具目录,其中一件产品标注为 "Armchair, model A42"。其照片显示绿色天鹅绒面料和木质腿,但如果描述中没有这些细节,搜索 "green velvet armchair" 可能会遗漏该产品。

多模态嵌入模型可以生成图像和文本嵌入向量,直接进行比较,使搜索能将用户的文字查询匹配到照片。应用程序使用 upsert 存储照片嵌入向量,然后使用相同模型对搜索文本进行嵌入,找到最接近的匹配项。

如果模型支持将视频/音频内容与文本查询进行比较,视频或音频搜索的工作方式类似。例如,为培训视频片段(04:00 到 04:30)关于更换滤芯的部分存储嵌入向量,视频 URL 和起止时间保存在元数据中。搜索 "how to replace the filter" 可返回一个结果,应用程序可将其定位到 04:00 开始播放。

现有文本嵌入向量也能受益,无论它们来自提供商的批量 API、其他团队还是数据库迁移。add() 方法会重新生成这些向量。直接写入数据库可以避免重复生成,但需要与 Spring AI 的字段名称和格式保持一致(包括 ID、文本、元数据和向量),以便其能够读取和搜索记录。upsert 会为您处理这种存储格式。

产品图片索引

对于家具目录,假设我们使用 Spring AI 的 VertexAiMultimodalEmbeddingModel,该模型支持文本、图像和视频。以下示例使用这两个字段:

code
private final VertexAiMultimodalEmbeddingModel multimodal;

private final VectorStore vectorStore;

首先,我们为产品照片生成嵌入向量,并将其与要存储的文档配对:

code
Document photo = Document.builder()
    .media(new Media(MimeTypeUtils.IMAGE_JPEG, product.photo()))
    .build();
float[] vector = this.multimodal.call(new DocumentEmbeddingRequest(photo)).getResult().getOutput();

Document row = Document.builder()
    .id(UUID.nameUUIDFromBytes(product.sku().getBytes(UTF_8)).toString())
    .text("")
    .metadata(DocumentMetadata.CONTENT_REF.value(), product.photoUrl())
    .metadata("sku", product.sku())
    .metadata("inStock", product.inStock())
    .build();

this.vectorStore.upsert(List.of(new EmbeddedDocument(row, vector)));

photo 携带发送到模型的图像;row 保存产品元数据和照片引用。EmbeddedDocument 将 row 与图像嵌入向量(即 vector)配对。

Document 需要文本或媒体内容。当前向量存储实现会持久化 ID、文本、元数据和向量,并利用这些字段重建搜索结果。它们会拒绝如 photo 这类包含媒体内容的文档,因为无法持久化媒体内容。使用 .text("") 可使 row 保持有效状态,同时照片保留在外部存储中;upsert 永远不会嵌入该空文本。若在 .text(...) 中提供产品描述,该描述会被保存并随搜索结果返回,但 upsert 仍会使用提供的图像嵌入,而不会嵌入描述内容。

DocumentMetadata.CONTENT_REF 在元数据中保存照片的 URL。应用程序使用该 URL 显示搜索结果;Spring AI 不会获取照片本身。

在运行此示例之前,请确保向量存储的维度与模型输出匹配。例如,对于 1408 维嵌入,需设置 PGvector 的 spring.ai.vectorstore.pgvector.dimensions=1408,或在构建器中使用 dimensions(1408)。现有表的向量列也必须具有兼容维度。

[使用查询嵌入进行搜索](https://spring.io/blog/2026/10/28/spring-ai-upsert-own-embeddings#searching-with-a-query-embedding)

使用相同的多模态模型对客户搜索文本进行嵌入:

code
float[] query = this.multimodal.call(new DocumentEmbeddingRequest(new Document("green velvet armchair")))
    .getResult()
    .getOutput();

List<Document> chairs = this.vectorStore.similaritySearch(query,
        SearchRequest.builder().topK(12).filterExpression("inStock == true").build());

similaritySearch(float[], SearchRequest) 会直接使用提供的向量,而不会调用存储的嵌入模型。请求控制 topK、相似度阈值和元数据过滤条件;任何查询文本都会被忽略。

使用专门用于比较图像和文本的模型生成产品图像和查询文本的嵌入。使用无关模型进行查询可能会产生无意义的结果,即使其嵌入具有相同维度。使用接受查询文本的重载方法时请牢记这一点:它使用存储配置的嵌入模型。

遵循模型推荐的输入设置来索引内容和嵌入查询;使用错误设置可能会降低搜索相关性。如果之后更换模型,可能需要重新生成已存储的嵌入。在元数据中保留模型名称和版本有助于识别需要更新的文档。

[更新现有文档](https://spring.io/blog/2026/10/28/spring-ai-upsert-own-embeddings#updating-existing-documents)

upsert 会插入具有新 ID 的文档,并替换具有现有 ID 的文档,因此像示例中从产品 SKU 派生的稳定 ID 可让索引任务更新照片而不会创建重复项。此替换协议适用于所有支持的存储;使用 add() 时,重复 ID 的行为取决于具体存储。

每次 upsert 调用接受文档列表及其对应的嵌入。空列表不会写入任何内容。单次调用中的写入操作不保证事务性:如果其中一个失败,调用会抛出异常,但部分文档可能已写入。在解决问题后,需使用相同 ID 和输入重新尝试整个列表。

[向量验证](https://spring.io/blog/2026/10/28/spring-ai-upsert-own-embeddings#vector-validation)

EmbeddedDocument 在构造时会拒绝空向量和非有限值(NaN 或无穷大)。当存储知道其索引维度时,它会在写入前检查列表中的每个向量,因此不匹配会阻止任何写入操作。这些检查无法检测到不兼容的嵌入空间。

Spring AI 从配置或通过读取现有索引、表或集合获取预期的向量大小。只有在创建索引且未明确指定大小时,才会咨询嵌入模型以获取维度信息。

如果 Spring AI 无法读取索引的预期向量大小且未进行配置,除 Redis 以外的存储仍会将向量发送到数据库,数据库会应用自己的大小检查。Spring AI 会在下次调用时再次尝试读取预期大小。

Redis 在无法确定预期大小时写入错误大小的向量时不会报错,但该文档不会出现在向量搜索结果中。为避免此问题,RedisVectorStore 在无法确定预期大小时会在写入前抛出异常。请配置 dimensions 以匹配 Redis 索引,或确保索引存在且可读,然后重试。

[向量存储支持](https://spring.io/blog/2026/10/28/spring-ai-upsert-own-embeddings#vector-store-support)

VectorStore.upsert(List<EmbeddedDocument>) 和 VectorStoreRetriever.similaritySearch(float[], SearchRequest) 是需要显式启用的默认方法,未实现时会抛出 UnsupportedOperationException。现有存储实现仍可正常编译。

从 Spring AI 2.1.0-RC1 开始,PGvector、Redis、Elasticsearch、Qdrant 和 OpenSearch 都支持 upsert 操作以及使用查询嵌入进行搜索。

对于继承自 AbstractObservationVectorStore 的存储,需实现 doUpsert 和 doSimilaritySearch(float[], SearchRequest),并重写 supportsUpsert() 和 supportsQueryEmbeddingSearch() 以返回 true。基类会在创建观测值前检查功能支持,并在向量存储指标中将 upsert 记录为独立操作。

现有应用仍可继续使用 add() 方法,让存储为文本文档生成嵌入表示。

[入门指南](https://spring.io/blog/2026/10/28/spring-ai-upsert-own-embeddings#getting-started)

更多细节请参见参考文档中的编写自定义嵌入和使用查询嵌入进行搜索。感谢 Aniket Gupta 提出的 #3541 建议,使这项工作得以启动。

请尝试这些 API,并通过 GitHub issues 提交反馈。