Get ahead
VMware offers training and certification to turbo-charge your progress.
Learn moreVectorStore.add() takes text documents, generates embeddings with the store's configured model, and saves both.
Spring AI 2.1 adds upsert for applications that already have embeddings or generate them from images, video, or audio.
It accepts documents paired with application-supplied vectors, without calling the store's embedding model.
A new similaritySearch overload also accepts a query embedding, giving applications control over both indexing and searching.
Let's look at a real-world example. Consider a furniture catalog with a product listed as "Armchair, model A42". Its photo shows green velvet upholstery and wooden legs, but a search for "green velvet armchair" could miss it if those details are absent from the description.
A multimodal embedding model produces image and text embeddings that can be compared directly, letting the search match the customer's words to the photo.
The application stores photo embeddings with upsert, then embeds search text with the same model to find the closest matches.
Video or audio search works similarly if the model supports comparing that content with text queries. For example, store an embedding for a training video segment about replacing a filter (04:00 to 04:30), with the video URL and start and end times in metadata. A search for "how to replace the filter" can then return a result that the application opens at 04:00.
Existing text embeddings benefit too, whether they come from a provider's batch API, another team, or a database migration.
add() would generate them again.
Writing directly to the database avoids that, but requires matching Spring AI's field names and formats for IDs, text, metadata, and vectors so it can read and search the records.
upsert handles that storage format for you.
For the furniture catalog, let's assume that we are using Spring AI's VertexAiMultimodalEmbeddingModel, which supports text, images, and video.
The examples below use these two fields:
private final VertexAiMultimodalEmbeddingModel multimodal;
private final VectorStore vectorStore;
First, we generate an embedding for a product photo and pair it with the document we want to store:
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 carries the image sent to the model; row holds the product metadata and photo reference.
EmbeddedDocument pairs row with the image embedding (the vector).
Document requires text or media.
Current vector store implementations persist an ID, text, metadata, and a vector, and use those fields to reconstruct search results.
They reject media documents such as photo because they cannot persist its media content.
Using .text("") above keeps row valid while the photo stays in external storage; upsert never embeds that empty text.
A product description in .text(...) would be saved and returned with search results, but upsert would still use the supplied image embedding without embedding the description.
DocumentMetadata.CONTENT_REF holds the photo's URL in metadata.
The application uses it to display search results; Spring AI does not fetch the photo.
Before running this example, make sure the vector store's dimensions match the model's output.
For 1408-dimensional embeddings, for example, set PGvector's spring.ai.vectorstore.pgvector.dimensions=1408, or use dimensions(1408) on its builder.
An existing table's vector column must also have a compatible dimension.
Embed the customer's search text with the same multimodal model:
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) uses the supplied vector without calling the store's embedding model.
The request controls topK, the similarity threshold, and metadata filters; any query text is ignored.
Generate the product image and query text embeddings with a model designed to compare images and text. Using an unrelated model for queries can produce meaningless results, even if its embeddings have the same number of dimensions. Keep this in mind when using the overload that accepts query text: it uses the store's configured embedding model.
Follow the model's recommended input settings for indexing content and embedding queries; using the wrong setting can reduce search relevance. If you later change models, you may need to regenerate stored embeddings. Keeping the model name and version in metadata helps identify which documents need updating.
upsert inserts documents with new IDs and replaces those with existing IDs, so stable IDs derived from product SKUs, as in the example above, let the indexing job update photos without creating duplicates.
This replacement contract applies to every supporting store; with add(), behavior for repeated IDs depends on the store.
Each upsert call accepts a list of documents paired with their embeddings.
An empty list returns without writing anything.
Writes within a single call are not guaranteed to be transactional: if one fails, the call throws an exception, but some documents may already have been written.
After resolving the cause, retry the entire list with the same IDs and input.
EmbeddedDocument rejects empty vectors and non-finite values (NaN or infinity) at construction.
When the store knows its index dimension, it checks every vector in the list before writing, so a mismatch prevents any writes.
These checks cannot detect incompatible embedding spaces.
Spring AI gets the expected vector size from configuration or by reading the existing index, table, or collection. The embedding model is consulted for dimensions only when creating an index without an explicit size.
If Spring AI cannot read the index's expected vector size and none is configured, stores other than Redis still send the vectors to the database, which applies its own size checks. Spring AI tries to read the expected size again on the next call.
Redis can save a vector of the wrong size without reporting an error, but that document will not appear in vector search results.
To prevent this, RedisVectorStore throws before writing when it cannot determine the expected size.
Configure dimensions to match the Redis index, or make sure the index exists and is readable, then retry.
VectorStore.upsert(List<EmbeddedDocument>) and VectorStoreRetriever.similaritySearch(float[], SearchRequest) are opt-in default methods that throw UnsupportedOperationException until implemented.
Existing store implementations continue to compile.
As of Spring AI 2.1.0-RC1, PGvector, Redis, Elasticsearch, Qdrant, and OpenSearch support both upsert and searching with a query embedding.
For stores extending AbstractObservationVectorStore, implement doUpsert and doSimilaritySearch(float[], SearchRequest), and override supportsUpsert() and supportsQueryEmbeddingSearch() to return true.
The base class checks support before creating observations and records upsert as a separate operation in vector store metrics.
Existing applications can continue using add() to have the store generate embeddings for text documents.
For more details, see the reference documentation on writing your own embeddings and searching with a query embedding. Thanks to Aniket Gupta, whose proposal in #3541 started this work.
Try these APIs and share feedback through GitHub issues.