Ohhnews

分类导航

$ cd ..
Spring Blog原文

Spring AI 2.1 支持使用自定义嵌入向量

#spring ai#向量嵌入#向量搜索#多模态#向量数据库

VectorStore.add() 接收文本文档,使用存储配置的模型生成嵌入,并同时保存两者。Spring AI 2.1 新增了 upsert,适用于已经拥有嵌入,或从图像、视频、音频生成嵌入的应用。它接受文档与应用提供的向量配对,而不会调用存储的嵌入模型。新的 similaritySearch 重载还接受查询嵌入,让应用可以同时控制索引和搜索。

何时自行提供嵌入

我们来看一个真实示例。假设有一个家具目录,其中一件产品列为“扶手椅,型号 A42”。它的照片显示绿色天鹅绒面料和木质椅腿,但如果描述中缺少这些细节,搜索“绿色天鹅绒扶手椅”时可能会漏掉它。

多模态嵌入模型会生成可直接比较的图像嵌入和文本嵌入,让搜索能够将客户输入的词与照片匹配起来。应用使用 upsert 存储照片嵌入,然后用同一个模型嵌入搜索文本,以找到最接近的匹配项。

如果模型支持将视频或音频内容与文本查询进行比较,视频或音频搜索也以类似方式工作。例如,为一段关于更换过滤器的培训视频片段(04:00 到 04:30)存储嵌入,并在元数据中保存视频 URL 以及开始和结束时间。这样,搜索“如何更换过滤器”时,就可以返回一条结果,应用会从 04:00 开始播放。

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

为产品图像建立索引

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

$ java
private final VertexAiMultimodalEmbeddingModel multimodal;

private final VectorStore vectorStore;

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

$ java
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。应用使用它来显示搜索结果;Spring AI 不会获取照片。

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

使用查询嵌入进行搜索

使用同一个多模态模型嵌入客户的搜索文本:

$ java
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、相似度阈值和元数据过滤器;任何查询文本都会被忽略。

请使用专为比较图像和文本而设计的模型来生成产品图像嵌入和查询文本嵌入。即使嵌入维度相同,使用无关模型处理查询也可能产生无意义的结果。使用接受查询文本的重载时请注意:它会使用存储配置的嵌入模型。

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

更新现有文档

upsert 会插入具有新 ID 的文档,并替换具有现有 ID 的文档。因此,像上面的示例那样,根据产品 SKU 派生稳定 ID,可以让索引任务更新照片而不会创建重复项。这一替换约定适用于所有支持该功能的存储;使用 add() 时,重复 ID 的行为取决于具体存储。

每次 upsert 调用都接受一个文档列表,文档与其嵌入配对。空列表会直接返回,不写入任何内容。单次调用中的写入不保证具有事务性:如果其中一个写入失败,该调用会抛出异常,但部分文档可能已经写入。排查原因后,请使用相同的 ID 和输入重试整个列表。

向量校验

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

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

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

Redis 可以保存大小错误的向量而不报错,但该文档不会出现在向量搜索结果中。为防止这种情况,RedisVectorStore 在无法确定预期大小时会在写入前抛出异常。请配置 dimensions 使其与 Redis 索引匹配,或确保索引存在且可读,然后重试。

向量存储支持

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(),让存储为文本文档生成嵌入。

开始使用

有关更多详细信息,请参阅关于自行写入嵌入和使用查询嵌入进行搜索的参考文档。感谢 Aniket Gupta,他在 #3541 中提出的方案启动了这项工作。

欢迎试用这些 API,并通过 GitHub issues 分享反馈。