Skip to content

Add VectorStore.upsert for pre-computed embeddings - #7002

Open
sobychacko wants to merge 3 commits into
spring-projects:mainfrom
sobychacko:explore/vector-store-upsert
Open

sobychacko wants to merge 3 commits into
spring-projects:mainfrom
sobychacko:explore/vector-store-upsert

Conversation

@sobychacko

Copy link
Copy Markdown
Contributor

Fixes #6989

User supplied embeddings write path: an EmbeddedDocument record pairing a Document with a caller-supplied vector, and a default VectorStore.upsert that throws so stores opt in. Wired into AbstractObservationVectorStore (UPSERT observation + doUpsert), with replace-by-id implementations for pgvector, Redis, Elasticsearch, and Qdrant. Adds a shared AbstractVectorStoreUpsertTests TCK, the DocumentMetadata.CONTENT_REF key for non-text content, and reference docs.

See also #3540 and #3541

@tzolov tzolov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sobychacko,
I was wondering if we need to put some guards in cases where CONTENT_REF is empty or set on non upsert() execution path? Are those supported invariants? if not maybe a waring message would be useful.

Also Seems like Redis drops the CONTENT_REF? It returns only metadata declared in metadataFields at build time.

/**
* VectorStore upsert operation.
*/
UPSERT("upsert"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the observability/index.adoc's (low cardinality keys and Labels tables) for the new Operation.UPSERT and the emitted db.operation.name=upsert.

if (errResponse.isPresent()) {
String message = MessageFormat.format("Could not upsert document: {0}", errResponse.get());
logger.error(message);
throw new RuntimeException(message);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other vector stores seem to throw IllegalStateException here. We can be consistent throwing the same even though RuntimeException is still good.


this.qdrantClient.upsertAsync(this.collectionName, points).get();
}
catch (InterruptedException | ExecutionException e) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call Thread.currentThread().interrupt() when handling InterruptedException?

@sobychacko

Copy link
Copy Markdown
Contributor Author

@tzolov Thanks for the review. I will address them. See below.

Guards / invariants: you're right, I never documented the contract.

CONTENT_REF on add() looks like valid usage to me: embed a summary or a chunk and keep a pointer to the full article, close to what #63 asks for, so a warning would hit legitimate use.

On a blank value: CONTENT_REF is an ordinary metadata entry, and today any metadata value can be blank, on both add and upsert. Checking this one would make it the only metadata key we validate.

Would documenting the invariants plus a TCK case that round-trips a CONTENT_REF row be enough? There's no behavioral test for it today. Happy to add guards if you'd rather.

Redis: The read only asks for the fields declared in metadataFields, so undeclared keys get stored but dropped on the way back. add does the same, and Redis is the only one of the four stores here that behaves this way.

content_ref is a known key, so declaring it covers it:

.metadataFields(MetadataField.tag(DocumentMetadata.CONTENT_REF.value()))

I'll put that in the docs and the Redis IT so it runs in CI, plus a warning when a document carries undeclared metadata so it stops being silent.

@ilayaperumalg Thanks for the review. I will address them.

Fixes spring-projects#6989

User supplied embeddings write path: an EmbeddedDocument record pairing a
Document with a caller-supplied vector, and a default
VectorStore.upsert that throws so stores opt in. Wired into
AbstractObservationVectorStore (UPSERT observation + doUpsert), with
replace-by-id implementations for pgvector, Redis, Elasticsearch, and
Qdrant. Adds a shared AbstractVectorStoreUpsertTests TCK, the
DocumentMetadata.CONTENT_REF key for non-text content, and reference
docs.

See also spring-projects#3540 and spring-projects#3541

Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
- List upsert in the observability tables
- Document the CONTENT_REF rules and add a TCK case that round-trips
  a reference row
- Declare content_ref in the Redis test and warn on undeclared
  metadata, which Redis drops on read
- Throw IllegalStateException from the Redis and Qdrant upsert paths
- Restore the interrupt flag when a Qdrant upsert is interrupted

Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
- Reject media documents, as add does, since every store requires
  text on the row
- Write Redis metadata before the reserved fields so it cannot
  overwrite the vector
- Check vector dimensions only when the expected width is known
- Use ids the stores accept in the docs examples
- Document what the caller owns when supplying vectors

Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
@sobychacko
sobychacko force-pushed the explore/vector-store-upsert branch from 1a0aa34 to 5a74983 Compare September 18, 2026 01:49

@tzolov tzolov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks Soby

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request vector store

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add VectorStore.upsert for pre-computed embeddings

3 participants