diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3a6ab6d1a4..e2b32f544f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -123,6 +123,7 @@ jobs: set -e (cd examples && mm.py ./src/main/java/io/dapr/examples/crypto/README.md) (cd examples && mm.py ./src/main/java/io/dapr/examples/binarystore/README.md) + (cd examples && mm.py ./src/main/java/io/dapr/examples/vector/README.md) (cd examples && mm.py ./src/main/java/io/dapr/examples/workflows/README.md) (cd spring-boot-examples/pubsub && mm.py README.md) (cd spring-boot-examples/workflows/patterns && mm.py README.md) diff --git a/examples/components/vector/inmemory.yaml b/examples/components/vector/inmemory.yaml new file mode 100644 index 0000000000..9eb32cf56b --- /dev/null +++ b/examples/components/vector/inmemory.yaml @@ -0,0 +1,7 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: vectorstore +spec: + type: vector.in-memory + version: v1 diff --git a/examples/src/main/java/io/dapr/examples/vector/README.md b/examples/src/main/java/io/dapr/examples/vector/README.md new file mode 100644 index 0000000000..3b6e4aa968 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/vector/README.md @@ -0,0 +1,149 @@ +## Dapr Vector API Example (alpha) + +This example shows how to create a vector collection, store vectors, query them by similarity, and delete them with the Dapr vector API. The vector API is an alpha API in Dapr 1.19. Its names and behavior can change in a later release. + +### Using the Vector API + +The Java SDK has these methods on `DaprPreviewClient`: +* `client.createVectorCollection(...)`, `client.getVectorCollection(...)`, `client.listVectorCollections(...)` and `client.deleteVectorCollection(...)` manage collections. +* `client.upsertVectors(...)` stores records by ID, and `client.getVectors(...)` and `client.deleteVectors(...)` read and delete them. +* `client.queryVectors(...)` finds the records closest to a vector or to a stored record, optionally filtered on metadata. +* `client.batchQueryVectors(...)` runs several queries in one call. Each query succeeds or fails on its own. + +## Pre-requisites + +* [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) and Dapr runtime 1.19 or later. +* Java JDK 17 (or greater): + * [Microsoft JDK 17](https://docs.microsoft.com/en-us/java/openjdk/download#openjdk-17) + * [Oracle JDK 17](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK17) + * [OpenJDK 17](https://jdk.java.net/17/) +* [Apache Maven](https://maven.apache.org/install.html) version 3.x. + +### Checking out the code + +Clone this repository: + +```sh +git clone https://github.com/dapr/java-sdk.git +cd java-sdk +``` + +Then build the Maven project: + +```sh +# make sure you are in the `java-sdk` directory. +mvn install +``` + +Then get into the examples directory: + +```sh +cd examples +``` + +### Initialize Dapr + +Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized. + +### The vector store component + +The example uses the in-memory vector store component in `./components/vector/inmemory.yaml`: + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: vectorstore +spec: + type: vector.in-memory + version: v1 +``` + +The in-memory store keeps vectors only in the sidecar's memory. Use a different vector store component, such as `vector.meilisearch`, to keep them. + +### Running the Example + +`VectorExample.java` creates a `books` collection with 3 dimensions and the cosine metric, stores three books, and then queries them: + +```java +try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) { + client.createVectorCollection(new CreateVectorCollectionRequestAlpha1("vectorstore", "books", 3) + .setMetric(DistanceMetric.COSINE)).block(); + + List books = List.of( + new VectorRecord("dune", new float[] {0.9f, 0.1f, 0.0f}).setMetadata(Map.of("genre", "scifi")), + ...); + client.upsertVectors(new UpsertVectorsRequestAlpha1("vectorstore", "books", books) + .setOptions(IndexingOptions.waitForCompletion(Duration.ofSeconds(5), IndexingWaitTimeoutAction.FAIL_REQUEST))) + .block(); + + QueryVectorsResponseAlpha1 closest = client.queryVectors(new QueryVectorsRequestAlpha1("vectorstore", "books", + VectorQuery.byVector(new float[] {0.85f, 0.15f, 0.05f}).setTopK(2))).block(); + + QueryVectorsResponseAlpha1 fantasy = client.queryVectors(new QueryVectorsRequestAlpha1("vectorstore", "books", + VectorQuery.byVector(new float[] {0.85f, 0.15f, 0.05f}) + .setFilter(Map.of("genre", Map.of("$eq", "fantasy"))))).block(); +} +``` + +The upsert waits until the store has indexed the records, so the queries that follow see them. Without indexing options, a write returns as soon as the store accepts it. + +The example also runs a batch of two queries, one of which asks for a record that does not exist. That query fails with `NOT_FOUND` on its own, and the other query still returns its matches. + +Use the following command to run this example: + + + +```bash +dapr run --resources-path ./components/vector --app-id vector-app --dapr-http-port 3500 --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.vector.VectorExample +``` + + + +### Sample Output + +``` +=== Dapr Vector Example === +Created collection: books +Stored books: 3, COMPLETED, failed: 0 +Collection books: 3 dimensions, COSINE +Closest two books: [dune, foundation] +Closest fantasy book: [hobbit] +Batch query 0: [hobbit] +Batch query 1 failed: NOT_FOUND +Read records: [dune] +Deleted record dune: COMPLETED +Deleted collection: books +SUCCESS: The vector example finished. +``` + +### Cleanup + +To stop the app, run (or press CTRL+C): + + + +```bash +dapr stop --app-id vector-app +``` + + diff --git a/examples/src/main/java/io/dapr/examples/vector/VectorExample.java b/examples/src/main/java/io/dapr/examples/vector/VectorExample.java new file mode 100644 index 0000000000..5a1994e807 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/vector/VectorExample.java @@ -0,0 +1,132 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.vector; + +import io.dapr.client.DaprClientBuilder; +import io.dapr.client.DaprPreviewClient; +import io.dapr.client.domain.BatchQueryVectorsRequestAlpha1; +import io.dapr.client.domain.BatchQueryVectorsResultAlpha1; +import io.dapr.client.domain.CreateVectorCollectionRequestAlpha1; +import io.dapr.client.domain.DeleteVectorsRequestAlpha1; +import io.dapr.client.domain.DistanceMetric; +import io.dapr.client.domain.GetVectorCollectionResponseAlpha1; +import io.dapr.client.domain.GetVectorsRequestAlpha1; +import io.dapr.client.domain.IndexAck; +import io.dapr.client.domain.IndexingOptions; +import io.dapr.client.domain.IndexingWaitTimeoutAction; +import io.dapr.client.domain.QueryVectorsRequestAlpha1; +import io.dapr.client.domain.QueryVectorsResponseAlpha1; +import io.dapr.client.domain.UpsertVectorsRequestAlpha1; +import io.dapr.client.domain.UpsertVectorsResponseAlpha1; +import io.dapr.client.domain.VectorMatch; +import io.dapr.client.domain.VectorQuery; +import io.dapr.client.domain.VectorRecord; +import io.dapr.config.Properties; +import io.dapr.config.Property; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Stores, reads, queries, and deletes vectors with the Dapr vector API (alpha). + * + *

Run with the in-memory vector store component in components/vector. + */ +public class VectorExample { + + private static final String STORE_NAME = "vectorstore"; + private static final String COLLECTION = "books"; + + /** + * The main method of the example. + * + * @param args Command line arguments (unused). + * @throws Exception if the client fails to close. + */ + public static void main(String[] args) throws Exception { + Map, String> overrides = Map.of( + Properties.HTTP_PORT, "3500", + Properties.GRPC_PORT, "50001" + ); + + try (DaprPreviewClient client = new DaprClientBuilder().withPropertyOverrides(overrides).buildPreviewClient()) { + System.out.println("=== Dapr Vector Example ==="); + + client.createVectorCollection(new CreateVectorCollectionRequestAlpha1(STORE_NAME, COLLECTION, 3) + .setMetric(DistanceMetric.COSINE)).block(); + System.out.println("Created collection: " + COLLECTION); + + // The vectors are made up; an app would get them from an embedding model. + List books = List.of( + new VectorRecord("dune", new float[] {0.9f, 0.1f, 0.0f}).setMetadata(Map.of("genre", "scifi")), + new VectorRecord("foundation", new float[] {0.8f, 0.2f, 0.1f}).setMetadata(Map.of("genre", "scifi")), + new VectorRecord("hobbit", new float[] {0.1f, 0.9f, 0.2f}).setMetadata(Map.of("genre", "fantasy"))); + UpsertVectorsResponseAlpha1 upsert = client.upsertVectors( + new UpsertVectorsRequestAlpha1(STORE_NAME, COLLECTION, books) + .setOptions(IndexingOptions.waitForCompletion( + Duration.ofSeconds(5), IndexingWaitTimeoutAction.FAIL_REQUEST))).block(); + System.out.println("Stored books: " + books.size() + ", " + upsert.getAck() + + ", failed: " + upsert.getFailedItems().size()); + + GetVectorCollectionResponseAlpha1 collection = client.getVectorCollection(STORE_NAME, COLLECTION).block(); + System.out.println("Collection " + collection.getCollection() + ": " + collection.getDimensions() + + " dimensions, " + collection.getMetric()); + + float[] scifiVector = {0.85f, 0.15f, 0.05f}; + QueryVectorsResponseAlpha1 closest = client.queryVectors(new QueryVectorsRequestAlpha1( + STORE_NAME, COLLECTION, VectorQuery.byVector(scifiVector).setTopK(2))).block(); + System.out.println("Closest two books: " + ids(closest)); + + QueryVectorsResponseAlpha1 fantasy = client.queryVectors(new QueryVectorsRequestAlpha1( + STORE_NAME, COLLECTION, VectorQuery.byVector(scifiVector) + .setFilter(Map.of("genre", Map.of("$eq", "fantasy"))))).block(); + System.out.println("Closest fantasy book: " + ids(fantasy)); + + // Each query in a batch succeeds or fails on its own. + List batch = client.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE_NAME, COLLECTION, List.of( + VectorQuery.byId("hobbit").setTopK(1), + VectorQuery.byId("missing")))).block(); + for (int i = 0; i < batch.size(); i++) { + BatchQueryVectorsResultAlpha1 result = batch.get(i); + if (result.getError() != null) { + System.out.println("Batch query " + i + " failed: " + result.getError().getErrorCode()); + } else { + System.out.println("Batch query " + i + ": " + ids(result.getResponse())); + } + } + + List found = client.getVectors( + new GetVectorsRequestAlpha1(STORE_NAME, COLLECTION, List.of("dune", "missing"))).block(); + System.out.println("Read records: " + found.stream().map(VectorRecord::getId).collect(Collectors.toList())); + + IndexAck deleted = client.deleteVectors( + new DeleteVectorsRequestAlpha1(STORE_NAME, COLLECTION, List.of("dune"))).block(); + System.out.println("Deleted record dune: " + deleted); + + client.deleteVectorCollection(STORE_NAME, COLLECTION).block(); + System.out.println("Deleted collection: " + COLLECTION); + System.out.println("SUCCESS: The vector example finished."); + } + } + + private static List ids(QueryVectorsResponseAlpha1 response) { + return response.getMatches().stream() + .map(VectorMatch::getRecord) + .map(VectorRecord::getId) + .collect(Collectors.toList()); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/vector/DaprPreviewClientVectorIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/vector/DaprPreviewClientVectorIT.java new file mode 100644 index 0000000000..d5afd2eabd --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/vector/DaprPreviewClientVectorIT.java @@ -0,0 +1,219 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.testcontainers.vector; + +import io.dapr.client.DaprClientBuilder; +import io.dapr.client.DaprPreviewClient; +import io.dapr.client.domain.BatchQueryVectorsRequestAlpha1; +import io.dapr.client.domain.BatchQueryVectorsResultAlpha1; +import io.dapr.client.domain.CreateVectorCollectionRequestAlpha1; +import io.dapr.client.domain.DeleteVectorsRequestAlpha1; +import io.dapr.client.domain.DistanceMetric; +import io.dapr.client.domain.GetVectorCollectionResponseAlpha1; +import io.dapr.client.domain.GetVectorsRequestAlpha1; +import io.dapr.client.domain.IndexAck; +import io.dapr.client.domain.IndexingOptions; +import io.dapr.client.domain.IndexingWaitTimeoutAction; +import io.dapr.client.domain.QueryVectorsRequestAlpha1; +import io.dapr.client.domain.QueryVectorsResponseAlpha1; +import io.dapr.client.domain.UpsertVectorsRequestAlpha1; +import io.dapr.client.domain.UpsertVectorsResponseAlpha1; +import io.dapr.client.domain.VectorMatch; +import io.dapr.client.domain.VectorQuery; +import io.dapr.client.domain.VectorRecord; +import io.dapr.config.Properties; +import io.dapr.exceptions.DaprException; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for the Dapr vector Alpha1 API. + */ +@Testcontainers +@Tag("testcontainers") +public class DaprPreviewClientVectorIT { + + private static final String STORE_NAME = "vectorstore"; + + private static final IndexingOptions WAIT = IndexingOptions.waitForCompletion( + Duration.ofSeconds(5), IndexingWaitTimeoutAction.FAIL_REQUEST); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("vector-test-app") + .withComponent(new Component(STORE_NAME, "vector.in-memory", "v1", Collections.emptyList())); + + private static DaprPreviewClient daprPreviewClient; + + @BeforeAll + static void setUp() { + daprPreviewClient = new DaprClientBuilder() + .withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint()) + .withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint()) + .buildPreviewClient(); + } + + @AfterAll + static void tearDown() throws Exception { + if (daprPreviewClient != null) { + daprPreviewClient.close(); + } + } + + @Test + public void shouldCreateGetListAndDeleteCollection() { + String collection = createCollection(DistanceMetric.EUCLIDEAN); + + GetVectorCollectionResponseAlpha1 created = daprPreviewClient.getVectorCollection(STORE_NAME, collection).block(); + assertEquals(collection, created.getCollection()); + assertEquals(3, created.getDimensions()); + assertEquals(DistanceMetric.EUCLIDEAN, created.getMetric()); + assertTrue(daprPreviewClient.listVectorCollections(STORE_NAME).block().contains(collection)); + + daprPreviewClient.deleteVectorCollection(STORE_NAME, collection).block(); + + assertFalse(daprPreviewClient.listVectorCollections(STORE_NAME).block().contains(collection)); + DaprException error = assertThrows(DaprException.class, + () -> daprPreviewClient.getVectorCollection(STORE_NAME, collection).block()); + assertEquals("NOT_FOUND", error.getErrorCode()); + } + + @Test + public void shouldStoreAndReadRecordsWithPayloadAndMetadata() { + String collection = createCollection(DistanceMetric.COSINE); + byte[] payload = "Arrakis".getBytes(StandardCharsets.UTF_8); + VectorRecord dune = new VectorRecord("dune", new float[] {0.9f, 0.1f, 0.0f}) + .setPayload(payload) + .setMetadata(Map.of("genre", "scifi", "year", 1965, "tags", List.of("classic", true))); + + UpsertVectorsResponseAlpha1 upsert = daprPreviewClient.upsertVectors( + new UpsertVectorsRequestAlpha1(STORE_NAME, collection, List.of(dune)).setOptions(WAIT)).block(); + assertEquals(IndexAck.COMPLETED, upsert.getAck()); + assertTrue(upsert.getFailedItems().isEmpty()); + + List records = daprPreviewClient.getVectors( + new GetVectorsRequestAlpha1(STORE_NAME, collection, List.of("dune", "missing")).setIncludeValues(true)) + .block(); + + assertEquals(1, records.size()); + VectorRecord read = records.get(0); + assertEquals("dune", read.getId()); + assertArrayEquals(new float[] {0.9f, 0.1f, 0.0f}, read.getValues()); + assertArrayEquals(payload, read.getPayload()); + assertEquals("scifi", read.getMetadata().get("genre")); + assertEquals(1965.0, read.getMetadata().get("year")); + assertEquals(List.of("classic", true), read.getMetadata().get("tags")); + + IndexAck deleted = daprPreviewClient.deleteVectors( + new DeleteVectorsRequestAlpha1(STORE_NAME, collection, List.of("dune")).setOptions(WAIT)).block(); + assertEquals(IndexAck.COMPLETED, deleted); + assertTrue(daprPreviewClient.getVectors(new GetVectorsRequestAlpha1(STORE_NAME, collection, List.of("dune"))) + .block().isEmpty()); + } + + @Test + public void shouldQueryByVectorByIdAndWithFilter() { + String collection = createCollectionWithBooks(); + float[] scifiVector = {0.85f, 0.15f, 0.05f}; + + QueryVectorsResponseAlpha1 closest = daprPreviewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE_NAME, collection, VectorQuery.byVector(scifiVector).setTopK(2))).block(); + assertEquals(List.of("dune", "foundation"), ids(closest)); + assertEquals(DistanceMetric.COSINE, closest.getMetric()); + assertNull(closest.getMatches().get(0).getRecord().getPayload()); + + QueryVectorsResponseAlpha1 fantasy = daprPreviewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE_NAME, collection, VectorQuery.byVector(scifiVector) + .setFilter(Map.of("genre", Map.of("$eq", "fantasy"))))).block(); + assertEquals(List.of("hobbit"), ids(fantasy)); + + QueryVectorsResponseAlpha1 likeFoundation = daprPreviewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE_NAME, collection, VectorQuery.byId("foundation").setTopK(2))).block(); + assertEquals(List.of("foundation", "dune"), ids(likeFoundation)); + } + + @Test + public void shouldReportEachBatchQueryOnItsOwn() { + String collection = createCollectionWithBooks(); + + List results = daprPreviewClient.batchQueryVectors( + new BatchQueryVectorsRequestAlpha1(STORE_NAME, collection, List.of( + VectorQuery.byVector(new float[] {0.1f, 0.9f, 0.2f}).setTopK(1), + VectorQuery.byId("missing")))).block(); + + assertEquals(2, results.size()); + assertNull(results.get(0).getError()); + assertEquals(List.of("hobbit"), ids(results.get(0).getResponse())); + assertNull(results.get(1).getResponse()); + assertEquals("NOT_FOUND", results.get(1).getError().getErrorCode()); + } + + @Test + public void shouldFailOnMissingCollection() { + String collection = "missing-" + UUID.randomUUID(); + + DaprException error = assertThrows(DaprException.class, () -> daprPreviewClient.queryVectors( + new QueryVectorsRequestAlpha1(STORE_NAME, collection, VectorQuery.byVector(new float[] {1f, 0f, 0f}))) + .block()); + + assertEquals("NOT_FOUND", error.getErrorCode()); + } + + private static String createCollection(DistanceMetric metric) { + String collection = "books-" + UUID.randomUUID(); + daprPreviewClient.createVectorCollection( + new CreateVectorCollectionRequestAlpha1(STORE_NAME, collection, 3).setMetric(metric)).block(); + return collection; + } + + private static String createCollectionWithBooks() { + String collection = createCollection(DistanceMetric.COSINE); + List books = List.of( + new VectorRecord("dune", new float[] {0.9f, 0.1f, 0.0f}).setMetadata(Map.of("genre", "scifi")), + new VectorRecord("foundation", new float[] {0.8f, 0.2f, 0.1f}).setMetadata(Map.of("genre", "scifi")), + new VectorRecord("hobbit", new float[] {0.1f, 0.9f, 0.2f}).setMetadata(Map.of("genre", "fantasy"))); + daprPreviewClient.upsertVectors( + new UpsertVectorsRequestAlpha1(STORE_NAME, collection, books).setOptions(WAIT)).block(); + return collection; + } + + private static List ids(QueryVectorsResponseAlpha1 response) { + return response.getMatches().stream() + .map(VectorMatch::getRecord) + .map(VectorRecord::getId) + .collect(Collectors.toList()); + } +} diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index e1fb357638..2a60cd72f8 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -20,6 +20,8 @@ import io.dapr.client.domain.ActorMetadata; import io.dapr.client.domain.AppConnectionPropertiesHealthMetadata; import io.dapr.client.domain.AppConnectionPropertiesMetadata; +import io.dapr.client.domain.BatchQueryVectorsRequestAlpha1; +import io.dapr.client.domain.BatchQueryVectorsResultAlpha1; import io.dapr.client.domain.BulkPublishEntry; import io.dapr.client.domain.BulkPublishRequest; import io.dapr.client.domain.BulkPublishResponse; @@ -32,10 +34,12 @@ import io.dapr.client.domain.ConversationRequestAlpha2; import io.dapr.client.domain.ConversationResponse; import io.dapr.client.domain.ConversationResponseAlpha2; +import io.dapr.client.domain.CreateVectorCollectionRequestAlpha1; import io.dapr.client.domain.DaprMetadata; import io.dapr.client.domain.DecryptRequestAlpha1; import io.dapr.client.domain.DeleteJobRequest; import io.dapr.client.domain.DeleteStateRequest; +import io.dapr.client.domain.DeleteVectorsRequestAlpha1; import io.dapr.client.domain.DropFailurePolicy; import io.dapr.client.domain.EncryptRequestAlpha1; import io.dapr.client.domain.ExecuteStateTransactionRequest; @@ -47,8 +51,11 @@ import io.dapr.client.domain.GetJobResponse; import io.dapr.client.domain.GetSecretRequest; import io.dapr.client.domain.GetStateRequest; +import io.dapr.client.domain.GetVectorCollectionResponseAlpha1; +import io.dapr.client.domain.GetVectorsRequestAlpha1; import io.dapr.client.domain.HttpEndpointMetadata; import io.dapr.client.domain.HttpExtension; +import io.dapr.client.domain.IndexAck; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.client.domain.JobSchedule; @@ -57,6 +64,8 @@ import io.dapr.client.domain.QueryStateItem; import io.dapr.client.domain.QueryStateRequest; import io.dapr.client.domain.QueryStateResponse; +import io.dapr.client.domain.QueryVectorsRequestAlpha1; +import io.dapr.client.domain.QueryVectorsResponseAlpha1; import io.dapr.client.domain.RuleMetadata; import io.dapr.client.domain.SaveStateRequest; import io.dapr.client.domain.ScheduleJobRequest; @@ -71,6 +80,9 @@ import io.dapr.client.domain.UnlockResponseStatus; import io.dapr.client.domain.UnsubscribeConfigurationRequest; import io.dapr.client.domain.UnsubscribeConfigurationResponse; +import io.dapr.client.domain.UpsertVectorsRequestAlpha1; +import io.dapr.client.domain.UpsertVectorsResponseAlpha1; +import io.dapr.client.domain.VectorRecord; import io.dapr.client.resiliency.ResiliencyOptions; import io.dapr.exceptions.DaprException; import io.dapr.internal.exceptions.DaprHttpException; @@ -178,6 +190,8 @@ public class DaprClientImpl extends AbstractDaprClient { private final DaprConversationClient conversationClient; + private final DaprVectorClient vectorClient; + /** * Default access level constructor, in order to create an instance of this class use io.dapr.client.DaprClientBuilder * @@ -276,6 +290,7 @@ private DaprClientImpl( this.grpcInterceptors = new DaprClientGrpcInterceptors(daprApiToken, timeoutPolicy); this.grpcCalls = new DaprGrpcCalls(asyncStub, this.grpcInterceptors, retryPolicy); this.conversationClient = new DaprConversationClient(this.grpcCalls, objectSerializer); + this.vectorClient = new DaprVectorClient(this.grpcCalls); this.logger = LoggerFactory.getLogger(DaprClientImpl.class); } @@ -2296,4 +2311,101 @@ private static List splitBinaryFileChunk(byte[] chunk) { } return parts; } + + /** + * {@inheritDoc} + */ + @Override + public Mono createVectorCollection(CreateVectorCollectionRequestAlpha1 request) { + return vectorClient.createVectorCollection(request); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono getVectorCollection(String storeName, String collection) { + return vectorClient.getVectorCollection(storeName, collection, null); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono getVectorCollection( + String storeName, String collection, Map metadata) { + return vectorClient.getVectorCollection(storeName, collection, metadata); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> listVectorCollections(String storeName) { + return vectorClient.listVectorCollections(storeName, null); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> listVectorCollections(String storeName, Map metadata) { + return vectorClient.listVectorCollections(storeName, metadata); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono deleteVectorCollection(String storeName, String collection) { + return vectorClient.deleteVectorCollection(storeName, collection, null); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono deleteVectorCollection(String storeName, String collection, Map metadata) { + return vectorClient.deleteVectorCollection(storeName, collection, metadata); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono upsertVectors(UpsertVectorsRequestAlpha1 request) { + return vectorClient.upsertVectors(request); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> getVectors(GetVectorsRequestAlpha1 request) { + return vectorClient.getVectors(request); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono deleteVectors(DeleteVectorsRequestAlpha1 request) { + return vectorClient.deleteVectors(request); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono queryVectors(QueryVectorsRequestAlpha1 request) { + return vectorClient.queryVectors(request); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> batchQueryVectors(BatchQueryVectorsRequestAlpha1 request) { + return vectorClient.batchQueryVectors(request); + } } diff --git a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java index 9078fc80c8..4fefb65786 100644 --- a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java @@ -13,6 +13,8 @@ package io.dapr.client; +import io.dapr.client.domain.BatchQueryVectorsRequestAlpha1; +import io.dapr.client.domain.BatchQueryVectorsResultAlpha1; import io.dapr.client.domain.BulkPublishEntry; import io.dapr.client.domain.BulkPublishRequest; import io.dapr.client.domain.BulkPublishResponse; @@ -21,14 +23,24 @@ import io.dapr.client.domain.ConversationRequestAlpha2; import io.dapr.client.domain.ConversationResponse; import io.dapr.client.domain.ConversationResponseAlpha2; +import io.dapr.client.domain.CreateVectorCollectionRequestAlpha1; import io.dapr.client.domain.DecryptRequestAlpha1; +import io.dapr.client.domain.DeleteVectorsRequestAlpha1; import io.dapr.client.domain.EncryptRequestAlpha1; +import io.dapr.client.domain.GetVectorCollectionResponseAlpha1; +import io.dapr.client.domain.GetVectorsRequestAlpha1; +import io.dapr.client.domain.IndexAck; import io.dapr.client.domain.LockRequest; import io.dapr.client.domain.QueryStateRequest; import io.dapr.client.domain.QueryStateResponse; +import io.dapr.client.domain.QueryVectorsRequestAlpha1; +import io.dapr.client.domain.QueryVectorsResponseAlpha1; import io.dapr.client.domain.SetBinaryFileRequestAlpha1; import io.dapr.client.domain.UnlockRequest; import io.dapr.client.domain.UnlockResponseStatus; +import io.dapr.client.domain.UpsertVectorsRequestAlpha1; +import io.dapr.client.domain.UpsertVectorsResponseAlpha1; +import io.dapr.client.domain.VectorRecord; import io.dapr.client.domain.query.Query; import io.dapr.utils.TypeRef; import reactor.core.publisher.Flux; @@ -512,4 +524,157 @@ Flux subscribeToTopic( * @throws IllegalArgumentException if storeName, fileName, or content is missing. */ Mono setBinaryFile(String storeName, String fileName, byte[] content, boolean overwrite); + + /** + * Creates a collection in a vector store component. + * + *

If the collection exists, the Mono fails with an error from the component. + * This is an alpha API. Its name and behavior can change in a later release. + * + * @param request The request with the store name, collection name, dimensions, metric and metadata. + * @return An empty Mono that completes when the sidecar creates the collection. + * @throws IllegalArgumentException if the request, store name or collection name is missing, or the + * dimensions are not positive. + */ + Mono createVectorCollection(CreateVectorCollectionRequestAlpha1 request); + + /** + * Gets a collection of a vector store component. + * + *

If the collection does not exist, the Mono fails with the NOT_FOUND error code. + * This is an alpha API. Its name and behavior can change in a later release. + * + * @param storeName Name of the vector store component. + * @param collection Name of the collection. + * @return A Mono with the collection's dimensions, metric, approximate record count and properties. + * @throws IllegalArgumentException if storeName or collection is null or blank. + */ + Mono getVectorCollection(String storeName, String collection); + + /** + * Gets a collection of a vector store component, passing metadata to the component. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param storeName Name of the vector store component. + * @param collection Name of the collection. + * @param metadata Metadata passed to the component. Can be null. + * @return A Mono with the collection's dimensions, metric, approximate record count and properties. + * @throws IllegalArgumentException if storeName or collection is null or blank. + */ + Mono getVectorCollection( + String storeName, String collection, Map metadata); + + /** + * Lists the collections of a vector store component. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param storeName Name of the vector store component. + * @return A Mono with the collection names. + * @throws IllegalArgumentException if storeName is null or blank. + */ + Mono> listVectorCollections(String storeName); + + /** + * Lists the collections of a vector store component, passing metadata to the component. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param storeName Name of the vector store component. + * @param metadata Metadata passed to the component. Can be null. + * @return A Mono with the collection names. + * @throws IllegalArgumentException if storeName is null or blank. + */ + Mono> listVectorCollections(String storeName, Map metadata); + + /** + * Deletes a collection and all its records from a vector store component. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param storeName Name of the vector store component. + * @param collection Name of the collection. + * @return An empty Mono that completes when the sidecar deletes the collection. + * @throws IllegalArgumentException if storeName or collection is null or blank. + */ + Mono deleteVectorCollection(String storeName, String collection); + + /** + * Deletes a collection and all its records from a vector store component, passing metadata to the component. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param storeName Name of the vector store component. + * @param collection Name of the collection. + * @param metadata Metadata passed to the component. Can be null. + * @return An empty Mono that completes when the sidecar deletes the collection. + * @throws IllegalArgumentException if storeName or collection is null or blank. + */ + Mono deleteVectorCollection(String storeName, String collection, Map metadata); + + /** + * Inserts or replaces records in a vector collection, by record ID. + * + *

Without indexing options, the call returns once the store accepts the write, and the records may not be + * searchable yet. Records the store rejects are listed in the response; the call itself still succeeds. + * This is an alpha API. Its name and behavior can change in a later release. + * + * @param request The request with the store name, collection name, records, metadata and indexing options. + * @return A Mono with the acknowledgement and the records that failed. + * @throws IllegalArgumentException if the request, store name, collection name or records are missing, a record + * has no ID or vector or has metadata of an unsupported type, or the indexing options are not valid. + */ + Mono upsertVectors(UpsertVectorsRequestAlpha1 request); + + /** + * Reads records from a vector collection by ID. + * + *

The records come back in request order. IDs that do not exist are left out. + * This is an alpha API. Its name and behavior can change in a later release. + * + * @param request The request with the store name, collection name, IDs, include-values flag and metadata. + * @return A Mono with the records that exist. + * @throws IllegalArgumentException if the request, store name, collection name or IDs are missing, or an ID is + * blank. + */ + Mono> getVectors(GetVectorsRequestAlpha1 request); + + /** + * Deletes records from a vector collection by ID. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param request The request with the store name, collection name, IDs, metadata and indexing options. + * @return A Mono with the acknowledgement. + * @throws IllegalArgumentException if the request, store name, collection name or IDs are missing, an ID is + * blank, or the indexing options are not valid. + */ + Mono deleteVectors(DeleteVectorsRequestAlpha1 request); + + /** + * Finds the records in a vector collection that are closest to a vector or to a stored record. + * + *

This is an alpha API. Its name and behavior can change in a later release. + * + * @param request The request with the store name, collection name, query and metadata. + * @return A Mono with the matches, in the order the store returned them, and the metric that scored them. + * @throws IllegalArgumentException if the request, store name, collection name or query is missing, or the query + * is not valid. + */ + Mono queryVectors(QueryVectorsRequestAlpha1 request); + + /** + * Runs several queries against one vector collection in one call. + * + *

Each query succeeds or fails on its own: the results come back in query order, each with its matches or + * its error. The Mono fails only if the whole call fails. + * This is an alpha API. Its name and behavior can change in a later release. + * + * @param request The request with the store name, collection name, queries and metadata. + * @return A Mono with one result per query, in query order. + * @throws IllegalArgumentException if the request, store name, collection name or queries are missing, or a + * query is not valid. + */ + Mono> batchQueryVectors(BatchQueryVectorsRequestAlpha1 request); } diff --git a/sdk/src/main/java/io/dapr/client/DaprVectorClient.java b/sdk/src/main/java/io/dapr/client/DaprVectorClient.java new file mode 100644 index 0000000000..7bd129aafa --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/DaprVectorClient.java @@ -0,0 +1,599 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import com.google.rpc.Status; +import io.dapr.client.domain.BatchQueryVectorsRequestAlpha1; +import io.dapr.client.domain.BatchQueryVectorsResultAlpha1; +import io.dapr.client.domain.CreateVectorCollectionRequestAlpha1; +import io.dapr.client.domain.DeleteVectorsRequestAlpha1; +import io.dapr.client.domain.DistanceMetric; +import io.dapr.client.domain.FailedItem; +import io.dapr.client.domain.GetVectorCollectionResponseAlpha1; +import io.dapr.client.domain.GetVectorsRequestAlpha1; +import io.dapr.client.domain.IndexAck; +import io.dapr.client.domain.IndexingOptions; +import io.dapr.client.domain.IndexingWaitTimeoutAction; +import io.dapr.client.domain.QueryVectorsRequestAlpha1; +import io.dapr.client.domain.QueryVectorsResponseAlpha1; +import io.dapr.client.domain.UpsertVectorsRequestAlpha1; +import io.dapr.client.domain.UpsertVectorsResponseAlpha1; +import io.dapr.client.domain.VectorMatch; +import io.dapr.client.domain.VectorQuery; +import io.dapr.client.domain.VectorRecord; +import io.dapr.exceptions.DaprErrorDetails; +import io.dapr.exceptions.DaprException; +import io.dapr.v1.DaprSearchProtos; +import io.dapr.v1.DaprVectorProtos; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Implements the vector building block (alpha) of {@link DaprPreviewClient} over gRPC. + */ +final class DaprVectorClient { + + private final DaprGrpcCalls grpcCalls; + + DaprVectorClient(DaprGrpcCalls grpcCalls) { + this.grpcCalls = grpcCalls; + } + + Mono createVectorCollection(CreateVectorCollectionRequestAlpha1 request) { + try { + requireRequest(request); + validateCollection(request.getStoreName(), request.getCollection()); + if (request.getDimensions() <= 0) { + throw new IllegalArgumentException("Dimensions must be greater than zero."); + } + + DaprVectorProtos.CreateCollectionRequestAlpha1.Builder builder = + DaprVectorProtos.CreateCollectionRequestAlpha1.newBuilder() + .setStoreName(request.getStoreName()) + .setCollection(request.getCollection()) + .setDimensions(request.getDimensions()) + .setMetric(toProto(request.getMetric())); + putMetadata(builder::putAllMetadata, request.getMetadata()); + DaprVectorProtos.CreateCollectionRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).createCollectionAlpha1(protoRequest, it) + ) + ).then(); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono getVectorCollection( + String storeName, String collection, Map metadata) { + try { + validateCollection(storeName, collection); + + DaprVectorProtos.GetCollectionRequestAlpha1.Builder builder = + DaprVectorProtos.GetCollectionRequestAlpha1.newBuilder() + .setStoreName(storeName) + .setCollection(collection); + putMetadata(builder::putAllMetadata, metadata); + DaprVectorProtos.GetCollectionRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).getCollectionAlpha1(protoRequest, it) + ) + ).map(response -> new GetVectorCollectionResponseAlpha1( + response.getCollection(), + response.getRecordCount(), + response.getPropertiesMap(), + response.getDimensions(), + fromProto(response.getMetric()))); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono> listVectorCollections(String storeName, Map metadata) { + try { + validateStoreName(storeName); + + DaprVectorProtos.ListCollectionsRequestAlpha1.Builder builder = + DaprVectorProtos.ListCollectionsRequestAlpha1.newBuilder().setStoreName(storeName); + putMetadata(builder::putAllMetadata, metadata); + DaprVectorProtos.ListCollectionsRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).listCollectionsAlpha1(protoRequest, it) + ) + ).map(response -> List.copyOf(response.getCollectionsList())); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono deleteVectorCollection(String storeName, String collection, Map metadata) { + try { + validateCollection(storeName, collection); + + DaprVectorProtos.DeleteCollectionRequestAlpha1.Builder builder = + DaprVectorProtos.DeleteCollectionRequestAlpha1.newBuilder() + .setStoreName(storeName) + .setCollection(collection); + putMetadata(builder::putAllMetadata, metadata); + DaprVectorProtos.DeleteCollectionRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).deleteCollectionAlpha1(protoRequest, it) + ) + ).then(); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono upsertVectors(UpsertVectorsRequestAlpha1 request) { + try { + requireRequest(request); + validateCollection(request.getStoreName(), request.getCollection()); + if (request.getRecords() == null) { + throw new IllegalArgumentException("Records cannot be null."); + } + + DaprVectorProtos.UpsertVectorsRequestAlpha1.Builder builder = + DaprVectorProtos.UpsertVectorsRequestAlpha1.newBuilder() + .setStoreName(request.getStoreName()) + .setCollection(request.getCollection()); + for (VectorRecord record : request.getRecords()) { + builder.addRecords(toProto(record)); + } + putMetadata(builder::putAllMetadata, request.getMetadata()); + if (request.getOptions() != null) { + builder.setOptions(toProto(request.getOptions())); + } + DaprVectorProtos.UpsertVectorsRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).upsertVectorsAlpha1(protoRequest, it) + ) + ).map(response -> new UpsertVectorsResponseAlpha1( + fromProto(response.getAck()), fromProto(response.getFailedItemsList()))); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono> getVectors(GetVectorsRequestAlpha1 request) { + try { + requireRequest(request); + validateCollection(request.getStoreName(), request.getCollection()); + validateIds(request.getIds()); + + DaprVectorProtos.GetVectorsRequestAlpha1.Builder builder = + DaprVectorProtos.GetVectorsRequestAlpha1.newBuilder() + .setStoreName(request.getStoreName()) + .setCollection(request.getCollection()) + .addAllIds(request.getIds()); + if (request.getIncludeValues() != null) { + builder.setIncludeValues(request.getIncludeValues()); + } + putMetadata(builder::putAllMetadata, request.getMetadata()); + DaprVectorProtos.GetVectorsRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).getVectorsAlpha1(protoRequest, it) + ) + ).map(response -> { + List records = new ArrayList<>(); + for (DaprVectorProtos.VectorRecord record : response.getRecordsList()) { + records.add(fromProto(record)); + } + return List.copyOf(records); + }); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono deleteVectors(DeleteVectorsRequestAlpha1 request) { + try { + requireRequest(request); + validateCollection(request.getStoreName(), request.getCollection()); + validateIds(request.getIds()); + + DaprVectorProtos.DeleteVectorsRequestAlpha1.Builder builder = + DaprVectorProtos.DeleteVectorsRequestAlpha1.newBuilder() + .setStoreName(request.getStoreName()) + .setCollection(request.getCollection()) + .addAllIds(request.getIds()); + putMetadata(builder::putAllMetadata, request.getMetadata()); + if (request.getOptions() != null) { + builder.setOptions(toProto(request.getOptions())); + } + DaprVectorProtos.DeleteVectorsRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).deleteVectorsAlpha1(protoRequest, it) + ) + ).map(response -> fromProto(response.getAck())); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono queryVectors(QueryVectorsRequestAlpha1 request) { + try { + requireRequest(request); + validateCollection(request.getStoreName(), request.getCollection()); + + DaprVectorProtos.QueryVectorsRequestAlpha1.Builder builder = + DaprVectorProtos.QueryVectorsRequestAlpha1.newBuilder() + .setStoreName(request.getStoreName()) + .setCollection(request.getCollection()) + .setQuery(toProto(request.getQuery())); + putMetadata(builder::putAllMetadata, request.getMetadata()); + DaprVectorProtos.QueryVectorsRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).queryVectorsAlpha1(protoRequest, it) + ) + ).map(DaprVectorClient::fromProto); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + Mono> batchQueryVectors(BatchQueryVectorsRequestAlpha1 request) { + try { + requireRequest(request); + validateCollection(request.getStoreName(), request.getCollection()); + if (request.getQueries() == null) { + throw new IllegalArgumentException("Queries cannot be null."); + } + + DaprVectorProtos.BatchQueryVectorsRequestAlpha1.Builder builder = + DaprVectorProtos.BatchQueryVectorsRequestAlpha1.newBuilder() + .setStoreName(request.getStoreName()) + .setCollection(request.getCollection()); + for (VectorQuery query : request.getQueries()) { + builder.addQueries(toProto(query)); + } + putMetadata(builder::putAllMetadata, request.getMetadata()); + DaprVectorProtos.BatchQueryVectorsRequestAlpha1 protoRequest = builder.build(); + + return Mono.deferContextual( + context -> grpcCalls.createMono( + it -> grpcCalls.intercept(context).batchQueryVectorsAlpha1(protoRequest, it) + ) + ).map(response -> fromProto(response, protoRequest.getQueriesCount())); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + private static void requireRequest(Object request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null."); + } + } + + private static void validateStoreName(String storeName) { + if (isBlank(storeName)) { + throw new IllegalArgumentException("Store name cannot be null or empty."); + } + } + + private static void validateCollection(String storeName, String collection) { + validateStoreName(storeName); + if (isBlank(collection)) { + throw new IllegalArgumentException("Collection name cannot be null or empty."); + } + } + + private static void validateIds(List ids) { + if (ids == null) { + throw new IllegalArgumentException("IDs cannot be null."); + } + for (String id : ids) { + if (isBlank(id)) { + throw new IllegalArgumentException("IDs cannot contain a null or empty ID."); + } + } + } + + private static boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + private static void putMetadata(Consumer> target, Map metadata) { + if (metadata != null) { + target.accept(metadata); + } + } + + private static DaprVectorProtos.VectorRecord toProto(VectorRecord record) { + if (record == null) { + throw new IllegalArgumentException("Records cannot contain a null record."); + } + if (isBlank(record.getId())) { + throw new IllegalArgumentException("Record ID cannot be null or empty."); + } + if (record.getValues() == null) { + throw new IllegalArgumentException("Record values cannot be null."); + } + + DaprVectorProtos.VectorRecord.Builder builder = DaprVectorProtos.VectorRecord.newBuilder() + .setId(record.getId()); + addValues(builder, record.getValues()); + if (record.getPayload() != null) { + builder.setPayload(ByteString.copyFrom(record.getPayload())); + } + if (record.getMetadata() != null) { + builder.setMetadata(toStruct(record.getMetadata())); + } + return builder.build(); + } + + private static DaprVectorProtos.VectorQuery toProto(VectorQuery query) { + if (query == null) { + throw new IllegalArgumentException("Query cannot be null."); + } + if ((query.getVector() == null) == (query.getById() == null)) { + throw new IllegalArgumentException("A query needs exactly one of a vector or a record ID."); + } + if (query.getTopK() != null && query.getTopK() < 0) { + throw new IllegalArgumentException("Top K cannot be negative."); + } + + DaprVectorProtos.VectorQuery.Builder builder = DaprVectorProtos.VectorQuery.newBuilder(); + if (query.getVector() != null) { + if (query.getVector().length == 0) { + throw new IllegalArgumentException("The query vector cannot be empty."); + } + DaprVectorProtos.VectorRecord.Builder vector = DaprVectorProtos.VectorRecord.newBuilder(); + addValues(vector, query.getVector()); + builder.setVector(vector); + } else { + if (isBlank(query.getById())) { + throw new IllegalArgumentException("The query record ID cannot be empty."); + } + builder.setById(query.getById()); + } + if (query.getTopK() != null) { + builder.setTopK(query.getTopK()); + } + if (query.getFilter() != null) { + builder.setFilter(toStruct(query.getFilter())); + } + if (query.getIncludeValues() != null) { + builder.setIncludeValues(query.getIncludeValues()); + } + if (query.getIncludePayload() != null) { + builder.setIncludePayload(query.getIncludePayload()); + } + builder.setMetric(toProto(query.getMetric())); + if (query.getScoreThreshold() != null) { + builder.setScoreThreshold(query.getScoreThreshold()); + } + return builder.build(); + } + + private static DaprSearchProtos.IndexingOptionsAlpha1 toProto(IndexingOptions options) { + if (!options.isWaitForCompletion()) { + return DaprSearchProtos.IndexingOptionsAlpha1.newBuilder() + .setMode(DaprSearchProtos.IndexingMode.INDEXING_MODE_RETURN_ON_ACCEPTANCE) + .build(); + } + + Duration waitTimeout = options.getWaitTimeout(); + if (waitTimeout == null || waitTimeout.isZero() || waitTimeout.isNegative()) { + throw new IllegalArgumentException("Waiting for completion needs a positive wait timeout."); + } + if (options.getOnWaitTimeout() == null) { + throw new IllegalArgumentException("Waiting for completion needs an action for when the wait times out."); + } + return DaprSearchProtos.IndexingOptionsAlpha1.newBuilder() + .setMode(DaprSearchProtos.IndexingMode.INDEXING_MODE_WAIT_FOR_COMPLETION) + .setWaitTimeout(com.google.protobuf.Duration.newBuilder() + .setSeconds(waitTimeout.getSeconds()) + .setNanos(waitTimeout.getNano())) + .setOnWaitTimeout(toProto(options.getOnWaitTimeout())) + .build(); + } + + private static DaprSearchProtos.IndexingWaitTimeoutAction toProto(IndexingWaitTimeoutAction action) { + switch (action) { + case CONTINUE_ASYNC: + return DaprSearchProtos.IndexingWaitTimeoutAction.INDEXING_WAIT_TIMEOUT_ACTION_CONTINUE_ASYNC; + case FAIL_REQUEST: + return DaprSearchProtos.IndexingWaitTimeoutAction.INDEXING_WAIT_TIMEOUT_ACTION_FAIL_REQUEST; + default: + throw new IllegalArgumentException("Unknown wait timeout action: " + action); + } + } + + private static DaprVectorProtos.DistanceMetric toProto(DistanceMetric metric) { + if (metric == null) { + return DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_UNSPECIFIED; + } + switch (metric) { + case COSINE: + return DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_COSINE; + case DOT_PRODUCT: + return DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_DOT_PRODUCT; + case EUCLIDEAN: + return DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_EUCLIDEAN; + default: + return DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_UNSPECIFIED; + } + } + + private static DistanceMetric fromProto(DaprVectorProtos.DistanceMetric metric) { + switch (metric) { + case DISTANCE_METRIC_COSINE: + return DistanceMetric.COSINE; + case DISTANCE_METRIC_DOT_PRODUCT: + return DistanceMetric.DOT_PRODUCT; + case DISTANCE_METRIC_EUCLIDEAN: + return DistanceMetric.EUCLIDEAN; + default: + throw invalidResponse("Vector store returned an invalid distance metric: " + metric + "."); + } + } + + private static IndexAck fromProto(DaprSearchProtos.IndexAck ack) { + switch (ack) { + case INDEX_ACK_QUEUED: + return IndexAck.QUEUED; + case INDEX_ACK_COMPLETED: + return IndexAck.COMPLETED; + default: + throw invalidResponse("Vector store returned an invalid write acknowledgement: " + ack + "."); + } + } + + private static List fromProto(List failedItems) { + List items = new ArrayList<>(); + for (DaprSearchProtos.FailedItem failedItem : failedItems) { + items.add(new FailedItem(failedItem.getId(), toDaprException(failedItem.getError()))); + } + return items; + } + + private static VectorRecord fromProto(DaprVectorProtos.VectorRecord record) { + float[] values = new float[record.getValuesCount()]; + for (int i = 0; i < values.length; i++) { + values[i] = record.getValues(i); + } + VectorRecord vectorRecord = new VectorRecord(record.getId(), values); + if (record.hasPayload()) { + vectorRecord.setPayload(record.getPayload().toByteArray()); + } + if (record.hasMetadata()) { + vectorRecord.setMetadata(ProtobufValueHelper.fromProtobufStruct(record.getMetadata())); + } + return vectorRecord; + } + + private static QueryVectorsResponseAlpha1 fromProto(DaprVectorProtos.QueryVectorsResponseAlpha1 response) { + List matches = new ArrayList<>(); + for (DaprVectorProtos.VectorMatch match : response.getMatchesList()) { + if (!match.hasRecord()) { + throw invalidResponse("Vector store returned a match without a record."); + } + matches.add(new VectorMatch(fromProto(match.getRecord()), match.getScore())); + } + return new QueryVectorsResponseAlpha1(matches, fromProto(response.getMetric())); + } + + private static List fromProto( + DaprVectorProtos.BatchQueryVectorsResponseAlpha1 response, int queryCount) { + // Results are matched to queries by position, so a reply of the wrong shape is rejected. + if (response.getResultsCount() != queryCount) { + throw invalidResponse( + "Vector store returned " + response.getResultsCount() + " results for " + queryCount + " queries."); + } + + List results = new ArrayList<>(); + for (DaprVectorProtos.BatchQueryResultAlpha1 result : response.getResultsList()) { + switch (result.getResultCase()) { + case RESPONSE: + results.add(BatchQueryVectorsResultAlpha1.ofResponse(fromProto(result.getResponse()))); + break; + case ERROR: + results.add(BatchQueryVectorsResultAlpha1.ofError(toDaprException(result.getError()))); + break; + default: + throw invalidResponse("Vector store returned a batch result with neither a response nor an error."); + } + } + return List.copyOf(results); + } + + private static DaprException toDaprException(Status status) { + io.grpc.Status.Code code = io.grpc.Status.fromCodeValue(status.getCode()).getCode(); + if (code == io.grpc.Status.Code.OK) { + throw invalidResponse("Vector store returned an error with an OK status code."); + } + return new DaprException(code.toString(), status.getMessage(), new DaprErrorDetails(status), status.toByteArray()); + } + + private static DaprException invalidResponse(String message) { + return new DaprException(io.grpc.Status.Code.INTERNAL.toString(), message, (Throwable) null); + } + + private static void addValues(DaprVectorProtos.VectorRecord.Builder builder, float[] values) { + for (float value : values) { + builder.addValues(value); + } + } + + // Unlike ProtobufValueHelper.toProtobufValue, this rejects unsupported types instead of sending their toString(), + // which a filter would silently fail to match. + private static Struct toStruct(Map map) { + Struct.Builder builder = Struct.newBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (!(entry.getKey() instanceof String)) { + throw new IllegalArgumentException("Filter and metadata keys must be strings."); + } + builder.putFields((String) entry.getKey(), toValue(entry.getValue())); + } + return builder.build(); + } + + private static Value toValue(Object value) { + Value.Builder builder = Value.newBuilder(); + if (value == null) { + return builder.setNullValue(NullValue.NULL_VALUE).build(); + } + if (value instanceof Boolean) { + return builder.setBoolValue((Boolean) value).build(); + } + if (value instanceof Number) { + return builder.setNumberValue(((Number) value).doubleValue()).build(); + } + if (value instanceof String) { + return builder.setStringValue((String) value).build(); + } + if (value instanceof Collection) { + ListValue.Builder list = ListValue.newBuilder(); + for (Object item : (Collection) value) { + list.addValues(toValue(item)); + } + return builder.setListValue(list).build(); + } + if (value instanceof Map) { + return builder.setStructValue(toStruct((Map) value)).build(); + } + throw new IllegalArgumentException("Filter and metadata values must be null, a Boolean, a Number, a String, " + + "a Collection or a Map, not " + value.getClass().getName() + "."); + } +} diff --git a/sdk/src/main/java/io/dapr/client/ProtobufValueHelper.java b/sdk/src/main/java/io/dapr/client/ProtobufValueHelper.java index 409967f8e8..d8441796be 100644 --- a/sdk/src/main/java/io/dapr/client/ProtobufValueHelper.java +++ b/sdk/src/main/java/io/dapr/client/ProtobufValueHelper.java @@ -20,11 +20,13 @@ import io.dapr.serializer.DaprObjectSerializer; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** - * Helper class to convert Java objects to Google Protobuf Value types. + * Helper class to convert between Java objects and Google Protobuf Value types. */ public class ProtobufValueHelper { @@ -73,4 +75,45 @@ public static Value toProtobufValue(Object obj) throws IOException { // Fallback: convert to string return Value.newBuilder().setStringValue(obj.toString()).build(); } + + /** + * Converts a Google Protobuf Value to a Java object: null, Boolean, Double, String, List or Map. + * + * @param value the Protobuf Value to convert + * @return the corresponding Java object + */ + static Object fromProtobufValue(Value value) { + switch (value.getKindCase()) { + case BOOL_VALUE: + return value.getBoolValue(); + case NUMBER_VALUE: + return value.getNumberValue(); + case STRING_VALUE: + return value.getStringValue(); + case LIST_VALUE: + List list = new ArrayList<>(); + for (Value item : value.getListValue().getValuesList()) { + list.add(fromProtobufValue(item)); + } + return list; + case STRUCT_VALUE: + return fromProtobufStruct(value.getStructValue()); + default: + return null; + } + } + + /** + * Converts a Google Protobuf Struct to a Java map. + * + * @param struct the Protobuf Struct to convert + * @return a map with the same fields, converted by {@link #fromProtobufValue(Value)} + */ + static Map fromProtobufStruct(Struct struct) { + Map map = new LinkedHashMap<>(); + for (Map.Entry field : struct.getFieldsMap().entrySet()) { + map.put(field.getKey(), fromProtobufValue(field.getValue())); + } + return map; + } } diff --git a/sdk/src/main/java/io/dapr/client/domain/BatchQueryVectorsRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/BatchQueryVectorsRequestAlpha1.java new file mode 100644 index 0000000000..535c9f7901 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/BatchQueryVectorsRequestAlpha1.java @@ -0,0 +1,90 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.List; +import java.util.Map; + +/** + * Request to run several queries against one vector collection in one call. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class BatchQueryVectorsRequestAlpha1 { + + private final String storeName; + private final String collection; + private final List queries; + private Map metadata; + + /** + * Constructor for BatchQueryVectorsRequestAlpha1. + * + * @param storeName Name of the vector store component. Required. + * @param collection Name of the collection. Required. + * @param queries The queries. Required. + */ + public BatchQueryVectorsRequestAlpha1(String storeName, String collection, List queries) { + this.storeName = storeName; + this.collection = collection; + this.queries = queries; + } + + /** + * Gets the vector store component name. + * + * @return the store name + */ + public String getStoreName() { + return storeName; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the queries. + * + * @return the queries + */ + public List getQueries() { + return queries; + } + + /** + * Gets the metadata passed to the vector store component for every query. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets metadata passed to the vector store component for every query. + * + * @param metadata the metadata + * @return this request for method chaining + */ + public BatchQueryVectorsRequestAlpha1 setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/BatchQueryVectorsResultAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/BatchQueryVectorsResultAlpha1.java new file mode 100644 index 0000000000..ee0de8b0a4 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/BatchQueryVectorsResultAlpha1.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import io.dapr.exceptions.DaprException; + +/** + * Outcome of one query in a batch: its matches, or the error that query failed with. + * + *

Each query in a batch succeeds or fails on its own. + * This is an alpha API. Its name and behavior can change in a later release. + */ +public class BatchQueryVectorsResultAlpha1 { + + private final QueryVectorsResponseAlpha1 response; + private final DaprException error; + + private BatchQueryVectorsResultAlpha1(QueryVectorsResponseAlpha1 response, DaprException error) { + this.response = response; + this.error = error; + } + + /** + * Creates the result of a query that succeeded. + * + * @param response the matches of the query + * @return the result + */ + public static BatchQueryVectorsResultAlpha1 ofResponse(QueryVectorsResponseAlpha1 response) { + return new BatchQueryVectorsResultAlpha1(response, null); + } + + /** + * Creates the result of a query that failed. + * + * @param error why the query failed + * @return the result + */ + public static BatchQueryVectorsResultAlpha1 ofError(DaprException error) { + return new BatchQueryVectorsResultAlpha1(null, error); + } + + /** + * Gets the matches of the query. + * + * @return the response, or null when the query failed + */ + public QueryVectorsResponseAlpha1 getResponse() { + return response; + } + + /** + * Gets why the query failed, with the gRPC status code, message and error details from the store. + * + * @return the error, or null when the query succeeded + */ + public DaprException getError() { + return error; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/CreateVectorCollectionRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/CreateVectorCollectionRequestAlpha1.java new file mode 100644 index 0000000000..c4c6a2dfe1 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/CreateVectorCollectionRequestAlpha1.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.Map; + +/** + * Request to create a collection in a vector store component. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class CreateVectorCollectionRequestAlpha1 { + + private final String storeName; + private final String collection; + private final int dimensions; + private DistanceMetric metric = DistanceMetric.UNSPECIFIED; + private Map metadata; + + /** + * Constructor for CreateVectorCollectionRequestAlpha1. + * + * @param storeName Name of the vector store component. Required. + * @param collection Name of the collection to create. Required. + * @param dimensions Length of every vector in the collection. Must be positive. + */ + public CreateVectorCollectionRequestAlpha1(String storeName, String collection, int dimensions) { + this.storeName = storeName; + this.collection = collection; + this.dimensions = dimensions; + } + + /** + * Gets the vector store component name. + * + * @return the store name + */ + public String getStoreName() { + return storeName; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the length of every vector in the collection. + * + * @return the dimensions + */ + public int getDimensions() { + return dimensions; + } + + /** + * Gets the distance metric of the collection. + * + * @return the metric + */ + public DistanceMetric getMetric() { + return metric; + } + + /** + * Sets the distance metric of the collection. The default, {@link DistanceMetric#UNSPECIFIED}, uses the + * component's default metric. A metric the component cannot provide fails the call with INVALID_ARGUMENT. + * + * @param metric the metric + * @return this request for method chaining + */ + public CreateVectorCollectionRequestAlpha1 setMetric(DistanceMetric metric) { + this.metric = metric; + return this; + } + + /** + * Gets the metadata passed to the vector store component. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets metadata passed to the vector store component. + * + * @param metadata the metadata + * @return this request for method chaining + */ + public CreateVectorCollectionRequestAlpha1 setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/DeleteVectorsRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/DeleteVectorsRequestAlpha1.java new file mode 100644 index 0000000000..b7619a8d65 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/DeleteVectorsRequestAlpha1.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.List; +import java.util.Map; + +/** + * Request to delete records from a vector collection by ID. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class DeleteVectorsRequestAlpha1 { + + private final String storeName; + private final String collection; + private final List ids; + private Map metadata; + private IndexingOptions options; + + /** + * Constructor for DeleteVectorsRequestAlpha1. + * + * @param storeName Name of the vector store component. Required. + * @param collection Name of the collection. Required. + * @param ids IDs of the records to delete. Required. + */ + public DeleteVectorsRequestAlpha1(String storeName, String collection, List ids) { + this.storeName = storeName; + this.collection = collection; + this.ids = ids; + } + + /** + * Gets the vector store component name. + * + * @return the store name + */ + public String getStoreName() { + return storeName; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the IDs of the records to delete. + * + * @return the record IDs + */ + public List getIds() { + return ids; + } + + /** + * Gets the metadata passed to the vector store component. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets metadata passed to the vector store component. + * + * @param metadata the metadata + * @return this request for method chaining + */ + public DeleteVectorsRequestAlpha1 setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + /** + * Gets when the write returns. + * + * @return the indexing options, or null to return once the store accepts the write + */ + public IndexingOptions getOptions() { + return options; + } + + /** + * Sets when the write returns. Without options, it returns once the store accepts the write. + * + * @param options the indexing options + * @return this request for method chaining + */ + public DeleteVectorsRequestAlpha1 setOptions(IndexingOptions options) { + this.options = options; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/DistanceMetric.java b/sdk/src/main/java/io/dapr/client/domain/DistanceMetric.java new file mode 100644 index 0000000000..d16fa1beca --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/DistanceMetric.java @@ -0,0 +1,42 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +/** + * Distance metric of a vector collection or a vector query. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public enum DistanceMetric { + + /** + * When creating a collection, the component's default metric. In a query, the collection's metric. + */ + UNSPECIFIED, + + /** + * Cosine similarity. Higher scores are closer matches. + */ + COSINE, + + /** + * Dot-product similarity. Higher scores are closer matches. + */ + DOT_PRODUCT, + + /** + * Euclidean distance. Lower scores are closer matches. + */ + EUCLIDEAN +} diff --git a/sdk/src/main/java/io/dapr/client/domain/FailedItem.java b/sdk/src/main/java/io/dapr/client/domain/FailedItem.java new file mode 100644 index 0000000000..297bd42a40 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/FailedItem.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import io.dapr.exceptions.DaprException; + +/** + * An item that a write to a vector or search store could not store or delete. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class FailedItem { + + private final String id; + private final DaprException error; + + /** + * Constructor for FailedItem. + * + * @param id ID of the item that failed. + * @param error why the item failed. + */ + public FailedItem(String id, DaprException error) { + this.id = id; + this.error = error; + } + + /** + * Gets the ID of the item that failed. + * + * @return the item ID + */ + public String getId() { + return id; + } + + /** + * Gets why the item failed, with the gRPC status code, message and error details from the store. + * + * @return the error + */ + public DaprException getError() { + return error; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/GetVectorCollectionResponseAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/GetVectorCollectionResponseAlpha1.java new file mode 100644 index 0000000000..92a105da16 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/GetVectorCollectionResponseAlpha1.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.Map; + +/** + * A collection in a vector store component, as returned by the store. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class GetVectorCollectionResponseAlpha1 { + + private final String collection; + private final long recordCount; + private final Map properties; + private final int dimensions; + private final DistanceMetric metric; + + /** + * Constructor for GetVectorCollectionResponseAlpha1. + * + * @param collection Name of the collection. + * @param recordCount Approximate number of records. + * @param properties Component-specific properties of the collection. + * @param dimensions Length of every vector in the collection. + * @param metric Distance metric of the collection. + */ + public GetVectorCollectionResponseAlpha1( + String collection, long recordCount, Map properties, int dimensions, DistanceMetric metric) { + this.collection = collection; + this.recordCount = recordCount; + this.properties = Map.copyOf(properties); + this.dimensions = dimensions; + this.metric = metric; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the approximate number of records. Stores that cannot count records cheaply return 0. + * + * @return the record count + */ + public long getRecordCount() { + return recordCount; + } + + /** + * Gets the component-specific properties of the collection. + * + * @return the properties + */ + public Map getProperties() { + return properties; + } + + /** + * Gets the length of every vector in the collection. + * + * @return the dimensions + */ + public int getDimensions() { + return dimensions; + } + + /** + * Gets the distance metric of the collection. It is never {@link DistanceMetric#UNSPECIFIED}. + * + * @return the metric + */ + public DistanceMetric getMetric() { + return metric; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/GetVectorsRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/GetVectorsRequestAlpha1.java new file mode 100644 index 0000000000..2a762fc560 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/GetVectorsRequestAlpha1.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.List; +import java.util.Map; + +/** + * Request to read records from a vector collection by ID. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class GetVectorsRequestAlpha1 { + + private final String storeName; + private final String collection; + private final List ids; + private Boolean includeValues; + private Map metadata; + + /** + * Constructor for GetVectorsRequestAlpha1. + * + * @param storeName Name of the vector store component. Required. + * @param collection Name of the collection. Required. + * @param ids IDs of the records to read. Required. + */ + public GetVectorsRequestAlpha1(String storeName, String collection, List ids) { + this.storeName = storeName; + this.collection = collection; + this.ids = ids; + } + + /** + * Gets the vector store component name. + * + * @return the store name + */ + public String getStoreName() { + return storeName; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the IDs of the records to read. + * + * @return the record IDs + */ + public List getIds() { + return ids; + } + + /** + * Gets whether the records include their vectors. + * + * @return true to include vectors, or null for the default (false) + */ + public Boolean getIncludeValues() { + return includeValues; + } + + /** + * Sets whether the records include their vectors. The default is false. + * + * @param includeValues true to include vectors + * @return this request for method chaining + */ + public GetVectorsRequestAlpha1 setIncludeValues(Boolean includeValues) { + this.includeValues = includeValues; + return this; + } + + /** + * Gets the metadata passed to the vector store component. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets metadata passed to the vector store component. + * + * @param metadata the metadata + * @return this request for method chaining + */ + public GetVectorsRequestAlpha1 setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/IndexAck.java b/sdk/src/main/java/io/dapr/client/domain/IndexAck.java new file mode 100644 index 0000000000..d73c556346 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/IndexAck.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +/** + * Acknowledgement of a write to a vector or search store. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public enum IndexAck { + + /** + * The store accepted the write for background processing. This does not mean any item is indexed yet. + */ + QUEUED, + + /** + * The store finished the write. The response lists every item that failed. + */ + COMPLETED +} diff --git a/sdk/src/main/java/io/dapr/client/domain/IndexingOptions.java b/sdk/src/main/java/io/dapr/client/domain/IndexingOptions.java new file mode 100644 index 0000000000..382840d021 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/IndexingOptions.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.time.Duration; + +/** + * Controls when a write to a vector or search store returns. + * + *

Without options, a write returns once the store accepts it. + * This is an alpha API. Its name and behavior can change in a later release. + */ +public final class IndexingOptions { + + private final boolean waitForCompletion; + private final Duration waitTimeout; + private final IndexingWaitTimeoutAction onWaitTimeout; + + private IndexingOptions(boolean waitForCompletion, Duration waitTimeout, IndexingWaitTimeoutAction onWaitTimeout) { + this.waitForCompletion = waitForCompletion; + this.waitTimeout = waitTimeout; + this.onWaitTimeout = onWaitTimeout; + } + + /** + * Returns as soon as the store accepts the write. + * + * @return the indexing options + */ + public static IndexingOptions returnOnAcceptance() { + return new IndexingOptions(false, null, null); + } + + /** + * Waits until the store finishes the write, for at most the given time. + * + *

The timeout must be shorter than any deadline on the call. + * + * @param waitTimeout how long to wait. Must be positive. + * @param onWaitTimeout what to do when the wait times out. Required. + * @return the indexing options + */ + public static IndexingOptions waitForCompletion(Duration waitTimeout, IndexingWaitTimeoutAction onWaitTimeout) { + return new IndexingOptions(true, waitTimeout, onWaitTimeout); + } + + /** + * Gets whether the write waits for indexing to finish. + * + * @return true to wait for completion, false to return on acceptance + */ + public boolean isWaitForCompletion() { + return waitForCompletion; + } + + /** + * Gets how long the write waits for indexing to finish. + * + * @return the wait timeout, or null when the write returns on acceptance + */ + public Duration getWaitTimeout() { + return waitTimeout; + } + + /** + * Gets what the write does when the wait times out. + * + * @return the action, or null when the write returns on acceptance + */ + public IndexingWaitTimeoutAction getOnWaitTimeout() { + return onWaitTimeout; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/IndexingWaitTimeoutAction.java b/sdk/src/main/java/io/dapr/client/domain/IndexingWaitTimeoutAction.java new file mode 100644 index 0000000000..e22a6afacf --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/IndexingWaitTimeoutAction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +/** + * What a write does when waiting for indexing to finish takes longer than the wait timeout. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public enum IndexingWaitTimeoutAction { + + /** + * Return {@link IndexAck#QUEUED} and let the store finish the write in the background. + * Not valid for stores that cannot queue writes. + */ + CONTINUE_ASYNC, + + /** + * Fail the call with DEADLINE_EXCEEDED. The store may still finish the write. + */ + FAIL_REQUEST +} diff --git a/sdk/src/main/java/io/dapr/client/domain/QueryVectorsRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/QueryVectorsRequestAlpha1.java new file mode 100644 index 0000000000..825ae51c39 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/QueryVectorsRequestAlpha1.java @@ -0,0 +1,89 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.Map; + +/** + * Request to find the records in a vector collection that are closest to a query. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class QueryVectorsRequestAlpha1 { + + private final String storeName; + private final String collection; + private final VectorQuery query; + private Map metadata; + + /** + * Constructor for QueryVectorsRequestAlpha1. + * + * @param storeName Name of the vector store component. Required. + * @param collection Name of the collection. Required. + * @param query The query. Required. + */ + public QueryVectorsRequestAlpha1(String storeName, String collection, VectorQuery query) { + this.storeName = storeName; + this.collection = collection; + this.query = query; + } + + /** + * Gets the vector store component name. + * + * @return the store name + */ + public String getStoreName() { + return storeName; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the query. + * + * @return the query + */ + public VectorQuery getQuery() { + return query; + } + + /** + * Gets the metadata passed to the vector store component. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets metadata passed to the vector store component. + * + * @param metadata the metadata + * @return this request for method chaining + */ + public QueryVectorsRequestAlpha1 setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/QueryVectorsResponseAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/QueryVectorsResponseAlpha1.java new file mode 100644 index 0000000000..2739fe0863 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/QueryVectorsResponseAlpha1.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.List; + +/** + * Matches of a vector query, in the order the vector store returned them. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class QueryVectorsResponseAlpha1 { + + private final List matches; + private final DistanceMetric metric; + + /** + * Constructor for QueryVectorsResponseAlpha1. + * + * @param matches The matching records with their scores. + * @param metric The metric that produced the scores. + */ + public QueryVectorsResponseAlpha1(List matches, DistanceMetric metric) { + this.matches = List.copyOf(matches); + this.metric = metric; + } + + /** + * Gets the matching records with their scores. + * + * @return the matches + */ + public List getMatches() { + return matches; + } + + /** + * Gets the metric that produced the scores. It is never {@link DistanceMetric#UNSPECIFIED}. + * + * @return the metric + */ + public DistanceMetric getMetric() { + return metric; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/UpsertVectorsRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/UpsertVectorsRequestAlpha1.java new file mode 100644 index 0000000000..1933f5a86b --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/UpsertVectorsRequestAlpha1.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.List; +import java.util.Map; + +/** + * Request to insert or replace records in a vector collection, by record ID. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class UpsertVectorsRequestAlpha1 { + + private final String storeName; + private final String collection; + private final List records; + private Map metadata; + private IndexingOptions options; + + /** + * Constructor for UpsertVectorsRequestAlpha1. + * + * @param storeName Name of the vector store component. Required. + * @param collection Name of the collection. Required. + * @param records Records to store. Required. Each needs an ID and a vector. + */ + public UpsertVectorsRequestAlpha1(String storeName, String collection, List records) { + this.storeName = storeName; + this.collection = collection; + this.records = records; + } + + /** + * Gets the vector store component name. + * + * @return the store name + */ + public String getStoreName() { + return storeName; + } + + /** + * Gets the collection name. + * + * @return the collection name + */ + public String getCollection() { + return collection; + } + + /** + * Gets the records to store. + * + * @return the records + */ + public List getRecords() { + return records; + } + + /** + * Gets the metadata passed to the vector store component. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets metadata passed to the vector store component. + * + * @param metadata the metadata + * @return this request for method chaining + */ + public UpsertVectorsRequestAlpha1 setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + /** + * Gets when the write returns. + * + * @return the indexing options, or null to return once the store accepts the write + */ + public IndexingOptions getOptions() { + return options; + } + + /** + * Sets when the write returns. Without options, it returns once the store accepts the write. + * + * @param options the indexing options + * @return this request for method chaining + */ + public UpsertVectorsRequestAlpha1 setOptions(IndexingOptions options) { + this.options = options; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/UpsertVectorsResponseAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/UpsertVectorsResponseAlpha1.java new file mode 100644 index 0000000000..3643ae594e --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/UpsertVectorsResponseAlpha1.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.List; + +/** + * Result of storing records in a vector collection. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class UpsertVectorsResponseAlpha1 { + + private final IndexAck ack; + private final List failedItems; + + /** + * Constructor for UpsertVectorsResponseAlpha1. + * + * @param ack How far the store got with the write. + * @param failedItems Records the store could not write. + */ + public UpsertVectorsResponseAlpha1(IndexAck ack, List failedItems) { + this.ack = ack; + this.failedItems = List.copyOf(failedItems); + } + + /** + * Gets how far the store got with the write. + * + * @return the acknowledgement + */ + public IndexAck getAck() { + return ack; + } + + /** + * Gets the records the store could not write. After {@link IndexAck#QUEUED}, this lists only the + * failures known when the store accepted the write. + * + * @return the failed records + */ + public List getFailedItems() { + return failedItems; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/VectorMatch.java b/sdk/src/main/java/io/dapr/client/domain/VectorMatch.java new file mode 100644 index 0000000000..3ac00cdc84 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/VectorMatch.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +/** + * A record that matched a vector query, with its score. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class VectorMatch { + + private final VectorRecord record; + private final double score; + + /** + * Constructor for VectorMatch. + * + * @param record the matching record. + * @param score the score under the query's metric. + */ + public VectorMatch(VectorRecord record, double score) { + this.record = record; + this.score = score; + } + + /** + * Gets the matching record. + * + * @return the record + */ + public VectorRecord getRecord() { + return record; + } + + /** + * Gets the score. Higher is closer for cosine and dot product; lower is closer for Euclidean distance. + * + * @return the score + */ + public double getScore() { + return score; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/VectorQuery.java b/sdk/src/main/java/io/dapr/client/domain/VectorQuery.java new file mode 100644 index 0000000000..cde53814e3 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/VectorQuery.java @@ -0,0 +1,201 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.Map; + +/** + * A similarity query against a vector collection, by vector or by the ID of a stored record. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class VectorQuery { + + private final float[] vector; + private final String byId; + private Integer topK; + private Map filter; + private Boolean includeValues; + private Boolean includePayload; + private DistanceMetric metric = DistanceMetric.UNSPECIFIED; + private Double scoreThreshold; + + private VectorQuery(float[] vector, String byId) { + this.vector = vector; + this.byId = byId; + } + + /** + * Creates a query for the records closest to a vector. + * + * @param vector the query vector. Must not be empty. + * @return the query + */ + public static VectorQuery byVector(float[] vector) { + return new VectorQuery(vector, null); + } + + /** + * Creates a query for the records closest to a stored record. + * + * @param id ID of the stored record. Must not be blank. + * @return the query + */ + public static VectorQuery byId(String id) { + return new VectorQuery(null, id); + } + + /** + * Gets the query vector. + * + * @return the vector, or null for a query by ID + */ + public float[] getVector() { + return vector; + } + + /** + * Gets the ID of the stored record to query by. + * + * @return the record ID, or null for a query by vector + */ + public String getById() { + return byId; + } + + /** + * Gets the maximum number of matches. + * + * @return the limit, or null for the component's default + */ + public Integer getTopK() { + return topK; + } + + /** + * Sets the maximum number of matches. 0 or null uses the component's default. + * + * @param topK the limit. Must not be negative. + * @return this query for method chaining + */ + public VectorQuery setTopK(Integer topK) { + this.topK = topK; + return this; + } + + /** + * Gets the filter on record metadata. + * + * @return the filter, or null when the query has none + */ + public Map getFilter() { + return filter; + } + + /** + * Sets a filter on record metadata, written in the Dapr filter format. + * + *

Values can be null, a Boolean, a Number, a String, a Collection or a Map with string keys. Other types + * are rejected with an {@link IllegalArgumentException} when the query is sent. + * + * @param filter the filter + * @return this query for method chaining + */ + public VectorQuery setFilter(Map filter) { + this.filter = filter; + return this; + } + + /** + * Gets whether matches include their vectors. + * + * @return true to include vectors, or null for the default (false) + */ + public Boolean getIncludeValues() { + return includeValues; + } + + /** + * Sets whether matches include their vectors. The default is false. + * + * @param includeValues true to include vectors + * @return this query for method chaining + */ + public VectorQuery setIncludeValues(Boolean includeValues) { + this.includeValues = includeValues; + return this; + } + + /** + * Gets whether matches include their payloads. + * + * @return true to include payloads, or null for the default (false) + */ + public Boolean getIncludePayload() { + return includePayload; + } + + /** + * Sets whether matches include their payloads. The default is false. + * + * @param includePayload true to include payloads + * @return this query for method chaining + */ + public VectorQuery setIncludePayload(Boolean includePayload) { + this.includePayload = includePayload; + return this; + } + + /** + * Gets the metric that scores the matches. + * + * @return the metric + */ + public DistanceMetric getMetric() { + return metric; + } + + /** + * Sets the metric that scores the matches. The default, {@link DistanceMetric#UNSPECIFIED}, uses the + * collection's metric. + * + * @param metric the metric + * @return this query for method chaining + */ + public VectorQuery setMetric(DistanceMetric metric) { + this.metric = metric; + return this; + } + + /** + * Gets the score cutoff. + * + * @return the cutoff, or null when the query has none + */ + public Double getScoreThreshold() { + return scoreThreshold; + } + + /** + * Sets a score cutoff. Matches scoring at least this value are kept for cosine and dot product, and matches + * scoring at most this value are kept for Euclidean distance. + * + * @param scoreThreshold the cutoff + * @return this query for method chaining + */ + public VectorQuery setScoreThreshold(Double scoreThreshold) { + this.scoreThreshold = scoreThreshold; + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/VectorRecord.java b/sdk/src/main/java/io/dapr/client/domain/VectorRecord.java new file mode 100644 index 0000000000..0a7d11dffa --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/VectorRecord.java @@ -0,0 +1,103 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import java.util.Map; + +/** + * A record in a vector collection: an ID, a dense vector, and optional payload and metadata. + * + *

This is an alpha API. Its name and behavior can change in a later release. + */ +public class VectorRecord { + + private final String id; + private final float[] values; + private byte[] payload; + private Map metadata; + + /** + * Constructor for VectorRecord. + * + * @param id ID of the record. Required. + * @param values dense vector. Required. Its length must equal the collection's dimensions. + */ + public VectorRecord(String id, float[] values) { + this.id = id; + this.values = values; + } + + /** + * Gets the ID of the record. + * + * @return the record ID + */ + public String getId() { + return id; + } + + /** + * Gets the dense vector. + * + * @return the vector, empty when a read did not ask for values + */ + public float[] getValues() { + return values; + } + + /** + * Gets the opaque payload stored with the record. + * + * @return the payload, or null when there is none or a read did not ask for it + */ + public byte[] getPayload() { + return payload; + } + + /** + * Sets an opaque payload to store with the record. The store returns it unchanged and cannot filter on it. + * + * @param payload the payload + * @return this record for method chaining + */ + public VectorRecord setPayload(byte[] payload) { + this.payload = payload; + return this; + } + + /** + * Gets the metadata of the record. + * + *

Numbers come back as {@link Double}, because a protobuf Struct keeps every number as a double. + * + * @return the metadata, or null when there is none + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets structured attributes that queries can filter on. + * + *

Values can be null, a Boolean, a Number, a String, a Collection or a Map with string keys. Other types + * are rejected with an {@link IllegalArgumentException} when the record is sent. + * + * @param metadata the metadata + * @return this record for method chaining + */ + public VectorRecord setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } +} diff --git a/sdk/src/test/java/io/dapr/client/DaprPreviewClientVectorTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientVectorTest.java new file mode 100644 index 0000000000..9bd579d0cb --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientVectorTest.java @@ -0,0 +1,776 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import com.google.rpc.Code; +import com.google.rpc.ErrorInfo; +import com.google.rpc.Status; +import io.dapr.client.domain.BatchQueryVectorsRequestAlpha1; +import io.dapr.client.domain.BatchQueryVectorsResultAlpha1; +import io.dapr.client.domain.CreateVectorCollectionRequestAlpha1; +import io.dapr.client.domain.DeleteVectorsRequestAlpha1; +import io.dapr.client.domain.DistanceMetric; +import io.dapr.client.domain.GetVectorCollectionResponseAlpha1; +import io.dapr.client.domain.GetVectorsRequestAlpha1; +import io.dapr.client.domain.IndexAck; +import io.dapr.client.domain.IndexingOptions; +import io.dapr.client.domain.IndexingWaitTimeoutAction; +import io.dapr.client.domain.QueryVectorsRequestAlpha1; +import io.dapr.client.domain.QueryVectorsResponseAlpha1; +import io.dapr.client.domain.UpsertVectorsRequestAlpha1; +import io.dapr.client.domain.UpsertVectorsResponseAlpha1; +import io.dapr.client.domain.VectorQuery; +import io.dapr.client.domain.VectorRecord; +import io.dapr.exceptions.DaprErrorDetails; +import io.dapr.exceptions.DaprException; +import io.dapr.serializer.DefaultObjectSerializer; +import io.dapr.utils.TypeRef; +import io.dapr.v1.DaprGrpc; +import io.dapr.v1.DaprSearchProtos; +import io.dapr.v1.DaprVectorProtos; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static io.dapr.client.DaprClientGrpcTest.newStatusRuntimeException; +import static io.dapr.utils.TestUtils.assertThrowsDaprException; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DaprPreviewClientVectorTest { + + private static final String STORE = "vectorstore"; + + private static final String COLLECTION = "books"; + + private static final Duration TIMEOUT = Duration.ofSeconds(5); + + private GrpcChannelFacade channel; + private DaprGrpc.DaprStub daprStub; + private DaprPreviewClient previewClient; + + @BeforeEach + public void setup() throws IOException { + channel = mock(GrpcChannelFacade.class); + daprStub = mock(DaprGrpc.DaprStub.class); + when(daprStub.withInterceptors(any())).thenReturn(daprStub); + previewClient = new DaprClientImpl( + channel, daprStub, mock(DaprHttp.class), new DefaultObjectSerializer(), new DefaultObjectSerializer()); + doNothing().when(channel).close(); + } + + @AfterEach + public void tearDown() throws Exception { + previewClient.close(); + } + + @SuppressWarnings("unchecked") + private static Answer respond(T response) { + return invocation -> { + StreamObserver observer = (StreamObserver) invocation.getArguments()[1]; + observer.onNext(response); + observer.onCompleted(); + return null; + }; + } + + private static void assertRejected(Supplier> call) { + assertThrows(IllegalArgumentException.class, () -> call.get().block(TIMEOUT)); + } + + @Test + @DisplayName("createVectorCollection should send the names, dimensions, metric and metadata") + public void createVectorCollectionTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.CreateCollectionRequestAlpha1.class); + doAnswer(respond(Empty.getDefaultInstance())).when(daprStub).createCollectionAlpha1(captor.capture(), any()); + + previewClient.createVectorCollection(new CreateVectorCollectionRequestAlpha1(STORE, COLLECTION, 3) + .setMetric(DistanceMetric.DOT_PRODUCT) + .setMetadata(Map.of("shards", "2"))).block(); + + DaprVectorProtos.CreateCollectionRequestAlpha1 request = captor.getValue(); + assertEquals(STORE, request.getStoreName()); + assertEquals(COLLECTION, request.getCollection()); + assertEquals(3, request.getDimensions()); + assertEquals(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_DOT_PRODUCT, request.getMetric()); + assertEquals(Map.of("shards", "2"), request.getMetadataMap()); + } + + @Test + @DisplayName("createVectorCollection should default to the component's metric and no metadata") + public void createVectorCollectionDefaultsTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.CreateCollectionRequestAlpha1.class); + doAnswer(respond(Empty.getDefaultInstance())).when(daprStub).createCollectionAlpha1(captor.capture(), any()); + + previewClient.createVectorCollection(new CreateVectorCollectionRequestAlpha1(STORE, COLLECTION, 8)).block(); + + assertEquals(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_UNSPECIFIED, captor.getValue().getMetric()); + assertTrue(captor.getValue().getMetadataMap().isEmpty()); + } + + @Test + @DisplayName("createVectorCollection should reject a missing request, blank names and non-positive dimensions") + public void createVectorCollectionInvalidArgumentsTest() { + assertRejected(() -> previewClient.createVectorCollection(null)); + assertRejected(() -> previewClient.createVectorCollection( + new CreateVectorCollectionRequestAlpha1(" ", COLLECTION, 3))); + assertRejected(() -> previewClient.createVectorCollection( + new CreateVectorCollectionRequestAlpha1(STORE, null, 3))); + assertRejected(() -> previewClient.createVectorCollection( + new CreateVectorCollectionRequestAlpha1(STORE, COLLECTION, 0))); + assertRejected(() -> previewClient.createVectorCollection( + new CreateVectorCollectionRequestAlpha1(STORE, COLLECTION, -1))); + verify(daprStub, never()).createCollectionAlpha1(any(), any()); + } + + @Test + @DisplayName("getVectorCollection should send the metadata and map the collection") + public void getVectorCollectionTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.GetCollectionRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.GetCollectionResponseAlpha1.newBuilder() + .setCollection(COLLECTION) + .setRecordCount(42) + .putProperties("engine", "hnsw") + .setDimensions(3) + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_EUCLIDEAN) + .build())).when(daprStub).getCollectionAlpha1(captor.capture(), any()); + + GetVectorCollectionResponseAlpha1 collection = + previewClient.getVectorCollection(STORE, COLLECTION, Map.of("consistency", "strong")).block(); + + assertEquals(STORE, captor.getValue().getStoreName()); + assertEquals(COLLECTION, captor.getValue().getCollection()); + assertEquals(Map.of("consistency", "strong"), captor.getValue().getMetadataMap()); + assertEquals(COLLECTION, collection.getCollection()); + assertEquals(42, collection.getRecordCount()); + assertEquals(Map.of("engine", "hnsw"), collection.getProperties()); + assertEquals(3, collection.getDimensions()); + assertEquals(DistanceMetric.EUCLIDEAN, collection.getMetric()); + } + + @Test + @DisplayName("getVectorCollection without metadata should send none") + public void getVectorCollectionWithoutMetadataTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.GetCollectionRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.GetCollectionResponseAlpha1.newBuilder() + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_COSINE) + .build())).when(daprStub).getCollectionAlpha1(captor.capture(), any()); + + GetVectorCollectionResponseAlpha1 collection = previewClient.getVectorCollection(STORE, COLLECTION).block(); + + assertTrue(captor.getValue().getMetadataMap().isEmpty()); + assertEquals(DistanceMetric.COSINE, collection.getMetric()); + } + + @Test + @DisplayName("every vector call should surface the sidecar's error code") + public void sidecarErrorTest() { + Answer notFound = invocation -> { + StreamObserver observer = (StreamObserver) invocation.getArguments()[1]; + observer.onError(newStatusRuntimeException("NOT_FOUND", "collection books not found")); + return null; + }; + doAnswer(notFound).when(daprStub).createCollectionAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).getCollectionAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).listCollectionsAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).deleteCollectionAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).upsertVectorsAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).getVectorsAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).deleteVectorsAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).queryVectorsAlpha1(any(), any()); + doAnswer(notFound).when(daprStub).batchQueryVectorsAlpha1(any(), any()); + List records = List.of(new VectorRecord("a", new float[] {1f})); + List>> calls = List.of( + () -> previewClient.createVectorCollection(new CreateVectorCollectionRequestAlpha1(STORE, COLLECTION, 3)), + () -> previewClient.getVectorCollection(STORE, COLLECTION), + () -> previewClient.listVectorCollections(STORE), + () -> previewClient.deleteVectorCollection(STORE, COLLECTION), + () -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records)), + () -> previewClient.getVectors(new GetVectorsRequestAlpha1(STORE, COLLECTION, List.of("a"))), + () -> previewClient.deleteVectors(new DeleteVectorsRequestAlpha1(STORE, COLLECTION, List.of("a"))), + () -> previewClient.queryVectors(new QueryVectorsRequestAlpha1(STORE, COLLECTION, VectorQuery.byId("a"))), + () -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE, COLLECTION, List.of(VectorQuery.byId("a"))))); + + for (Supplier> call : calls) { + DaprException exception = assertThrows(DaprException.class, () -> call.get().block(TIMEOUT)); + assertEquals("NOT_FOUND", exception.getErrorCode()); + } + } + + @Test + @DisplayName("responses with an unspecified or unknown distance metric should fail") + public void invalidMetricResponseTest() { + doAnswer(respond(DaprVectorProtos.GetCollectionResponseAlpha1.getDefaultInstance())) + .when(daprStub).getCollectionAlpha1(any(), any()); + doAnswer(respond(DaprVectorProtos.QueryVectorsResponseAlpha1.newBuilder().setMetricValue(42).build())) + .when(daprStub).queryVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.getVectorCollection(STORE, COLLECTION).block(TIMEOUT)); + assertThrowsDaprException("INTERNAL", () -> previewClient.queryVectors( + new QueryVectorsRequestAlpha1(STORE, COLLECTION, VectorQuery.byId("a"))).block(TIMEOUT)); + } + + @Test + @DisplayName("write responses with an unspecified or unknown acknowledgement should fail") + public void invalidAckResponseTest() { + doAnswer(respond(DaprVectorProtos.UpsertVectorsResponseAlpha1.getDefaultInstance())) + .when(daprStub).upsertVectorsAlpha1(any(), any()); + doAnswer(respond(DaprVectorProtos.DeleteVectorsResponseAlpha1.newBuilder().setAckValue(9).build())) + .when(daprStub).deleteVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1( + STORE, COLLECTION, List.of(new VectorRecord("a", new float[] {1f})))).block(TIMEOUT)); + assertThrowsDaprException("INTERNAL", () -> previewClient.deleteVectors( + new DeleteVectorsRequestAlpha1(STORE, COLLECTION, List.of("a"))).block(TIMEOUT)); + } + + @Test + @DisplayName("listVectorCollections should send the metadata and return the names") + public void listVectorCollectionsTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.ListCollectionsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.ListCollectionsResponseAlpha1.newBuilder() + .addCollections("books") + .addCollections("films") + .build())).when(daprStub).listCollectionsAlpha1(captor.capture(), any()); + + List names = previewClient.listVectorCollections(STORE, Map.of("page", "1")).block(); + + assertEquals(List.of("books", "films"), names); + assertEquals(STORE, captor.getValue().getStoreName()); + assertEquals(Map.of("page", "1"), captor.getValue().getMetadataMap()); + } + + @Test + @DisplayName("listVectorCollections without metadata should send none") + public void listVectorCollectionsWithoutMetadataTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.ListCollectionsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.ListCollectionsResponseAlpha1.getDefaultInstance())) + .when(daprStub).listCollectionsAlpha1(captor.capture(), any()); + + assertEquals(List.of(), previewClient.listVectorCollections(STORE).block()); + assertTrue(captor.getValue().getMetadataMap().isEmpty()); + } + + @Test + @DisplayName("deleteVectorCollection should send the names and metadata") + public void deleteVectorCollectionTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.DeleteCollectionRequestAlpha1.class); + doAnswer(respond(Empty.getDefaultInstance())).when(daprStub).deleteCollectionAlpha1(captor.capture(), any()); + + previewClient.deleteVectorCollection(STORE, COLLECTION, Map.of("force", "true")).block(); + previewClient.deleteVectorCollection(STORE, COLLECTION).block(); + + List requests = captor.getAllValues(); + assertEquals(STORE, requests.get(0).getStoreName()); + assertEquals(COLLECTION, requests.get(0).getCollection()); + assertEquals(Map.of("force", "true"), requests.get(0).getMetadataMap()); + assertTrue(requests.get(1).getMetadataMap().isEmpty()); + } + + @Test + @DisplayName("collection calls should reject blank store and collection names") + public void collectionCallsInvalidNamesTest() { + assertRejected(() -> previewClient.getVectorCollection(null, COLLECTION)); + assertRejected(() -> previewClient.getVectorCollection(STORE, "")); + assertRejected(() -> previewClient.listVectorCollections(" ")); + assertRejected(() -> previewClient.listVectorCollections(null, Map.of())); + assertRejected(() -> previewClient.deleteVectorCollection("", COLLECTION)); + assertRejected(() -> previewClient.deleteVectorCollection(STORE, " ", Map.of())); + verify(daprStub, never()).getCollectionAlpha1(any(), any()); + verify(daprStub, never()).listCollectionsAlpha1(any(), any()); + verify(daprStub, never()).deleteCollectionAlpha1(any(), any()); + } + + @Test + @DisplayName("upsertVectors should send the records with their payload, metadata and indexing options") + public void upsertVectorsTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.UpsertVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.UpsertVectorsResponseAlpha1.newBuilder() + .setAck(DaprSearchProtos.IndexAck.INDEX_ACK_COMPLETED) + .build())).when(daprStub).upsertVectorsAlpha1(captor.capture(), any()); + + Map metadata = new LinkedHashMap<>(); + metadata.put("genre", "fantasy"); + metadata.put("year", 1954); + metadata.put("tags", List.of("classic", true)); + metadata.put("missing", null); + List records = List.of( + new VectorRecord("lotr", new float[] {0.1f, 0.2f, 0.3f}) + .setPayload("payload".getBytes()) + .setMetadata(metadata), + new VectorRecord("dune", new float[] {0.4f, 0.5f, 0.6f})); + + UpsertVectorsResponseAlpha1 response = previewClient.upsertVectors( + new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records) + .setMetadata(Map.of("ttl", "60")) + .setOptions(IndexingOptions.waitForCompletion( + Duration.ofMillis(1500), IndexingWaitTimeoutAction.FAIL_REQUEST))).block(); + + DaprVectorProtos.UpsertVectorsRequestAlpha1 request = captor.getValue(); + assertEquals(STORE, request.getStoreName()); + assertEquals(COLLECTION, request.getCollection()); + assertEquals(Map.of("ttl", "60"), request.getMetadataMap()); + assertEquals(2, request.getRecordsCount()); + + DaprVectorProtos.VectorRecord first = request.getRecords(0); + assertEquals("lotr", first.getId()); + assertEquals(List.of(0.1f, 0.2f, 0.3f), first.getValuesList()); + assertEquals(ByteString.copyFromUtf8("payload"), first.getPayload()); + Struct struct = first.getMetadata(); + assertEquals("fantasy", struct.getFieldsOrThrow("genre").getStringValue()); + assertEquals(1954, struct.getFieldsOrThrow("year").getNumberValue()); + assertEquals(2, struct.getFieldsOrThrow("tags").getListValue().getValuesCount()); + assertEquals(NullValue.NULL_VALUE, struct.getFieldsOrThrow("missing").getNullValue()); + + DaprVectorProtos.VectorRecord second = request.getRecords(1); + assertFalse(second.hasPayload()); + assertFalse(second.hasMetadata()); + + DaprSearchProtos.IndexingOptionsAlpha1 options = request.getOptions(); + assertEquals(DaprSearchProtos.IndexingMode.INDEXING_MODE_WAIT_FOR_COMPLETION, options.getMode()); + assertEquals(1, options.getWaitTimeout().getSeconds()); + assertEquals(500_000_000, options.getWaitTimeout().getNanos()); + assertEquals(DaprSearchProtos.IndexingWaitTimeoutAction.INDEXING_WAIT_TIMEOUT_ACTION_FAIL_REQUEST, + options.getOnWaitTimeout()); + + assertEquals(IndexAck.COMPLETED, response.getAck()); + assertTrue(response.getFailedItems().isEmpty()); + } + + @Test + @DisplayName("upsertVectors should send return-on-acceptance options and none when they are not set") + public void upsertVectorsIndexingModesTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.UpsertVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.UpsertVectorsResponseAlpha1.newBuilder() + .setAck(DaprSearchProtos.IndexAck.INDEX_ACK_QUEUED) + .build())).when(daprStub).upsertVectorsAlpha1(captor.capture(), any()); + List records = List.of(new VectorRecord("a", new float[] {1f})); + + previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records) + .setOptions(IndexingOptions.returnOnAcceptance())).block(); + previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records)).block(); + + assertEquals(DaprSearchProtos.IndexingMode.INDEXING_MODE_RETURN_ON_ACCEPTANCE, + captor.getAllValues().get(0).getOptions().getMode()); + assertFalse(captor.getAllValues().get(0).getOptions().hasWaitTimeout()); + assertFalse(captor.getAllValues().get(1).hasOptions()); + } + + @Test + @DisplayName("upsertVectors should report records the store rejected as failed items with their error") + public void upsertVectorsFailedItemsTest() { + doAnswer(respond(DaprVectorProtos.UpsertVectorsResponseAlpha1.newBuilder() + .setAck(DaprSearchProtos.IndexAck.INDEX_ACK_COMPLETED) + .addFailedItems(DaprSearchProtos.FailedItem.newBuilder() + .setId("dune") + .setError(Status.newBuilder() + .setCode(Code.INVALID_ARGUMENT_VALUE) + .setMessage("expected 3 dimensions, got 2") + .addDetails(Any.pack(ErrorInfo.newBuilder().setReason("DIMENSION_MISMATCH").build())))) + .build())).when(daprStub).upsertVectorsAlpha1(any(), any()); + + UpsertVectorsResponseAlpha1 response = previewClient.upsertVectors(new UpsertVectorsRequestAlpha1( + STORE, COLLECTION, List.of(new VectorRecord("dune", new float[] {1f, 2f})))).block(); + + assertEquals(1, response.getFailedItems().size()); + assertEquals("dune", response.getFailedItems().get(0).getId()); + DaprException error = response.getFailedItems().get(0).getError(); + assertEquals("INVALID_ARGUMENT", error.getErrorCode()); + assertEquals("INVALID_ARGUMENT: expected 3 dimensions, got 2", error.getMessage()); + assertEquals("DIMENSION_MISMATCH", + error.getErrorDetails().get(DaprErrorDetails.ErrorDetailType.ERROR_INFO, "reason", TypeRef.STRING)); + } + + @Test + @DisplayName("upsertVectors should fail on a failed item with an OK status code") + public void upsertVectorsOkFailedItemTest() { + doAnswer(respond(DaprVectorProtos.UpsertVectorsResponseAlpha1.newBuilder() + .setAck(DaprSearchProtos.IndexAck.INDEX_ACK_COMPLETED) + .addFailedItems(DaprSearchProtos.FailedItem.newBuilder().setId("dune")) + .build())).when(daprStub).upsertVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1( + STORE, COLLECTION, List.of(new VectorRecord("dune", new float[] {1f})))).block(TIMEOUT)); + } + + @Test + @DisplayName("upsertVectors and queryVectors should reject metadata and filter values of unsupported types") + public void unsupportedStructValuesTest() { + Map nonStringKey = new LinkedHashMap<>(); + nonStringKey.put("genre", Map.of(1, "fantasy")); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, List.of( + new VectorRecord("a", new float[] {1f}).setMetadata(Map.of("tags", new String[] {"classic"})))))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, List.of( + new VectorRecord("a", new float[] {1f}).setMetadata(nonStringKey))))); + assertRejected(() -> previewClient.queryVectors(new QueryVectorsRequestAlpha1(STORE, COLLECTION, + VectorQuery.byId("a").setFilter(Map.of("published", Duration.ofDays(1)))))); + verify(daprStub, never()).upsertVectorsAlpha1(any(), any()); + verify(daprStub, never()).queryVectorsAlpha1(any(), any()); + } + + @Test + @DisplayName("upsertVectors should reject missing records, records without an ID or vector, and bad options") + public void upsertVectorsInvalidArgumentsTest() { + List records = List.of(new VectorRecord("a", new float[] {1f})); + assertRejected(() -> previewClient.upsertVectors(null)); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(null, COLLECTION, records))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, null))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1( + STORE, COLLECTION, Collections.singletonList(null)))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1( + STORE, COLLECTION, List.of(new VectorRecord(" ", new float[] {1f}))))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1( + STORE, COLLECTION, List.of(new VectorRecord("a", null))))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records) + .setOptions(IndexingOptions.waitForCompletion(Duration.ZERO, IndexingWaitTimeoutAction.CONTINUE_ASYNC)))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records) + .setOptions(IndexingOptions.waitForCompletion(null, IndexingWaitTimeoutAction.CONTINUE_ASYNC)))); + assertRejected(() -> previewClient.upsertVectors(new UpsertVectorsRequestAlpha1(STORE, COLLECTION, records) + .setOptions(IndexingOptions.waitForCompletion(Duration.ofSeconds(1), null)))); + verify(daprStub, never()).upsertVectorsAlpha1(any(), any()); + } + + @Test + @DisplayName("getVectors should send the IDs and map the records back") + public void getVectorsTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.GetVectorsRequestAlpha1.class); + Struct metadata = Struct.newBuilder() + .putFields("year", Value.newBuilder().setNumberValue(1965).build()) + .putFields("awards", Value.newBuilder().setListValue(ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("Hugo")) + .addValues(Value.newBuilder().setBoolValue(true))).build()) + .build(); + doAnswer(respond(DaprVectorProtos.GetVectorsResponseAlpha1.newBuilder() + .addRecords(DaprVectorProtos.VectorRecord.newBuilder() + .setId("dune") + .addValues(0.4f) + .addValues(0.5f) + .setPayload(ByteString.copyFromUtf8("payload")) + .setMetadata(metadata)) + .addRecords(DaprVectorProtos.VectorRecord.newBuilder().setId("lotr")) + .build())).when(daprStub).getVectorsAlpha1(captor.capture(), any()); + + List records = previewClient.getVectors( + new GetVectorsRequestAlpha1(STORE, COLLECTION, List.of("dune", "lotr", "unknown")) + .setIncludeValues(true) + .setMetadata(Map.of("k", "v"))).block(); + + DaprVectorProtos.GetVectorsRequestAlpha1 request = captor.getValue(); + assertEquals(List.of("dune", "lotr", "unknown"), request.getIdsList()); + assertTrue(request.hasIncludeValues()); + assertTrue(request.getIncludeValues()); + assertEquals(Map.of("k", "v"), request.getMetadataMap()); + + assertEquals(2, records.size()); + VectorRecord dune = records.get(0); + assertEquals("dune", dune.getId()); + assertArrayEquals(new float[] {0.4f, 0.5f}, dune.getValues()); + assertArrayEquals("payload".getBytes(), dune.getPayload()); + assertEquals(1965.0, dune.getMetadata().get("year")); + assertEquals(Arrays.asList("Hugo", true), dune.getMetadata().get("awards")); + + VectorRecord lotr = records.get(1); + assertArrayEquals(new float[0], lotr.getValues()); + assertNull(lotr.getPayload()); + assertNull(lotr.getMetadata()); + } + + @Test + @DisplayName("getVectors should leave include-values unset when it is not set") + public void getVectorsIncludeValuesUnsetTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.GetVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.GetVectorsResponseAlpha1.getDefaultInstance())) + .when(daprStub).getVectorsAlpha1(captor.capture(), any()); + + assertEquals(List.of(), previewClient.getVectors( + new GetVectorsRequestAlpha1(STORE, COLLECTION, List.of("a"))).block()); + + assertFalse(captor.getValue().hasIncludeValues()); + } + + @Test + @DisplayName("getVectors and deleteVectors should reject missing or blank IDs") + public void vectorIdsInvalidArgumentsTest() { + List withBlank = Arrays.asList("a", " "); + List withNull = Arrays.asList("a", null); + assertRejected(() -> previewClient.getVectors(null)); + assertRejected(() -> previewClient.getVectors(new GetVectorsRequestAlpha1(STORE, COLLECTION, null))); + assertRejected(() -> previewClient.getVectors(new GetVectorsRequestAlpha1(STORE, COLLECTION, withBlank))); + assertRejected(() -> previewClient.getVectors(new GetVectorsRequestAlpha1(STORE, "", List.of("a")))); + assertRejected(() -> previewClient.deleteVectors(null)); + assertRejected(() -> previewClient.deleteVectors(new DeleteVectorsRequestAlpha1(STORE, COLLECTION, null))); + assertRejected(() -> previewClient.deleteVectors(new DeleteVectorsRequestAlpha1(STORE, COLLECTION, withNull))); + verify(daprStub, never()).getVectorsAlpha1(any(), any()); + verify(daprStub, never()).deleteVectorsAlpha1(any(), any()); + } + + @Test + @DisplayName("deleteVectors should send the IDs and indexing options and return the acknowledgement") + public void deleteVectorsTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.DeleteVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.DeleteVectorsResponseAlpha1.newBuilder() + .setAck(DaprSearchProtos.IndexAck.INDEX_ACK_QUEUED) + .build())).when(daprStub).deleteVectorsAlpha1(captor.capture(), any()); + + IndexAck ack = previewClient.deleteVectors(new DeleteVectorsRequestAlpha1(STORE, COLLECTION, List.of("dune")) + .setMetadata(Map.of("k", "v")) + .setOptions(IndexingOptions.waitForCompletion( + Duration.ofSeconds(2), IndexingWaitTimeoutAction.CONTINUE_ASYNC))).block(); + + DaprVectorProtos.DeleteVectorsRequestAlpha1 request = captor.getValue(); + assertEquals(List.of("dune"), request.getIdsList()); + assertEquals(Map.of("k", "v"), request.getMetadataMap()); + assertEquals(2, request.getOptions().getWaitTimeout().getSeconds()); + assertEquals(DaprSearchProtos.IndexingWaitTimeoutAction.INDEXING_WAIT_TIMEOUT_ACTION_CONTINUE_ASYNC, + request.getOptions().getOnWaitTimeout()); + assertEquals(IndexAck.QUEUED, ack); + } + + @Test + @DisplayName("queryVectors by vector should send every set option and map the matches") + public void queryVectorsByVectorTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.QueryVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.QueryVectorsResponseAlpha1.newBuilder() + .addMatches(DaprVectorProtos.VectorMatch.newBuilder() + .setRecord(DaprVectorProtos.VectorRecord.newBuilder().setId("lotr").addValues(0.1f)) + .setScore(0.98)) + .addMatches(DaprVectorProtos.VectorMatch.newBuilder() + .setRecord(DaprVectorProtos.VectorRecord.newBuilder().setId("dune")) + .setScore(0.71)) + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_EUCLIDEAN) + .build())).when(daprStub).queryVectorsAlpha1(captor.capture(), any()); + + QueryVectorsResponseAlpha1 response = previewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE, COLLECTION, VectorQuery.byVector(new float[] {0.1f, 0.2f}) + .setTopK(2) + .setFilter(Map.of("genre", Map.of("$in", new LinkedHashSet<>(List.of("fantasy", "scifi"))))) + .setIncludeValues(true) + .setIncludePayload(false) + .setMetric(DistanceMetric.EUCLIDEAN) + .setScoreThreshold(0.5)) + .setMetadata(Map.of("k", "v"))).block(); + + DaprVectorProtos.QueryVectorsRequestAlpha1 request = captor.getValue(); + assertEquals(Map.of("k", "v"), request.getMetadataMap()); + DaprVectorProtos.VectorQuery query = request.getQuery(); + assertEquals(DaprVectorProtos.VectorQuery.KindCase.VECTOR, query.getKindCase()); + assertEquals(List.of(0.1f, 0.2f), query.getVector().getValuesList()); + assertEquals(2, query.getTopK()); + ListValue genres = query.getFilter().getFieldsOrThrow("genre").getStructValue() + .getFieldsOrThrow("$in").getListValue(); + assertEquals(2, genres.getValuesCount()); + assertEquals("fantasy", genres.getValues(0).getStringValue()); + assertEquals("scifi", genres.getValues(1).getStringValue()); + assertTrue(query.getIncludeValues()); + assertTrue(query.hasIncludePayload()); + assertFalse(query.getIncludePayload()); + assertEquals(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_EUCLIDEAN, query.getMetric()); + assertEquals(0.5, query.getScoreThreshold()); + + assertEquals(DistanceMetric.EUCLIDEAN, response.getMetric()); + assertEquals(2, response.getMatches().size()); + assertEquals("lotr", response.getMatches().get(0).getRecord().getId()); + assertArrayEquals(new float[] {0.1f}, response.getMatches().get(0).getRecord().getValues()); + assertEquals(0.98, response.getMatches().get(0).getScore()); + assertEquals("dune", response.getMatches().get(1).getRecord().getId()); + } + + @Test + @DisplayName("queryVectors by ID should send only the ID and leave unset options unset") + public void queryVectorsByIdTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.QueryVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.QueryVectorsResponseAlpha1.newBuilder() + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_DOT_PRODUCT) + .build())).when(daprStub).queryVectorsAlpha1(captor.capture(), any()); + + QueryVectorsResponseAlpha1 response = previewClient.queryVectors( + new QueryVectorsRequestAlpha1(STORE, COLLECTION, VectorQuery.byId("lotr"))).block(); + + DaprVectorProtos.VectorQuery query = captor.getValue().getQuery(); + assertEquals(DaprVectorProtos.VectorQuery.KindCase.BY_ID, query.getKindCase()); + assertEquals("lotr", query.getById()); + assertFalse(query.hasTopK()); + assertFalse(query.hasFilter()); + assertFalse(query.hasIncludeValues()); + assertFalse(query.hasIncludePayload()); + assertFalse(query.hasScoreThreshold()); + assertEquals(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_UNSPECIFIED, query.getMetric()); + assertTrue(response.getMatches().isEmpty()); + assertEquals(DistanceMetric.DOT_PRODUCT, response.getMetric()); + } + + @Test + @DisplayName("queryVectors should fail on a match without a record") + public void queryVectorsMatchWithoutRecordTest() { + doAnswer(respond(DaprVectorProtos.QueryVectorsResponseAlpha1.newBuilder() + .addMatches(DaprVectorProtos.VectorMatch.newBuilder().setScore(0.5)) + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_COSINE) + .build())).when(daprStub).queryVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.queryVectors( + new QueryVectorsRequestAlpha1(STORE, COLLECTION, VectorQuery.byId("a"))).block(TIMEOUT)); + } + + @Test + @DisplayName("queryVectors should reject a missing query, an empty vector, a blank ID and a negative top K") + public void queryVectorsInvalidArgumentsTest() { + assertRejected(() -> previewClient.queryVectors(null)); + assertRejected(() -> previewClient.queryVectors(new QueryVectorsRequestAlpha1(STORE, COLLECTION, null))); + IllegalArgumentException neither = assertThrows(IllegalArgumentException.class, () -> previewClient.queryVectors( + new QueryVectorsRequestAlpha1(STORE, COLLECTION, VectorQuery.byVector(null))).block(TIMEOUT)); + assertEquals("A query needs exactly one of a vector or a record ID.", neither.getMessage()); + assertRejected(() -> previewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE, COLLECTION, VectorQuery.byVector(new float[0])))); + assertRejected(() -> previewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE, COLLECTION, VectorQuery.byId(" ")))); + assertRejected(() -> previewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE, COLLECTION, VectorQuery.byId("a").setTopK(-1)))); + assertRejected(() -> previewClient.queryVectors(new QueryVectorsRequestAlpha1( + STORE, " ", VectorQuery.byId("a")))); + verify(daprStub, never()).queryVectorsAlpha1(any(), any()); + } + + @Test + @DisplayName("batchQueryVectors should return each query's matches or error, in query order") + public void batchQueryVectorsTest() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(DaprVectorProtos.BatchQueryVectorsRequestAlpha1.class); + doAnswer(respond(DaprVectorProtos.BatchQueryVectorsResponseAlpha1.newBuilder() + .addResults(DaprVectorProtos.BatchQueryResultAlpha1.newBuilder() + .setResponse(DaprVectorProtos.QueryVectorsResponseAlpha1.newBuilder() + .addMatches(DaprVectorProtos.VectorMatch.newBuilder() + .setRecord(DaprVectorProtos.VectorRecord.newBuilder().setId("lotr")) + .setScore(0.9)) + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_COSINE))) + .addResults(DaprVectorProtos.BatchQueryResultAlpha1.newBuilder() + .setError(Status.newBuilder().setCode(Code.NOT_FOUND_VALUE).setMessage("record x not found"))) + .build())).when(daprStub).batchQueryVectorsAlpha1(captor.capture(), any()); + + List results = previewClient.batchQueryVectors( + new BatchQueryVectorsRequestAlpha1(STORE, COLLECTION, List.of( + VectorQuery.byVector(new float[] {0.1f}).setMetric(DistanceMetric.COSINE), VectorQuery.byId("x"))) + .setMetadata(Map.of("k", "v"))).block(); + + assertEquals(2, captor.getValue().getQueriesCount()); + assertEquals(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_COSINE, captor.getValue().getQueries(0).getMetric()); + assertEquals(Map.of("k", "v"), captor.getValue().getMetadataMap()); + assertEquals(2, results.size()); + assertNull(results.get(0).getError()); + assertEquals("lotr", results.get(0).getResponse().getMatches().get(0).getRecord().getId()); + assertEquals(DistanceMetric.COSINE, results.get(0).getResponse().getMetric()); + assertNull(results.get(1).getResponse()); + assertEquals("NOT_FOUND", results.get(1).getError().getErrorCode()); + assertEquals("NOT_FOUND: record x not found", results.get(1).getError().getMessage()); + } + + @Test + @DisplayName("batchQueryVectors should fail when the reply does not hold one result per query") + public void batchQueryVectorsResultCountMismatchTest() { + doAnswer(respond(DaprVectorProtos.BatchQueryVectorsResponseAlpha1.newBuilder() + .addResults(DaprVectorProtos.BatchQueryResultAlpha1.newBuilder() + .setResponse(DaprVectorProtos.QueryVectorsResponseAlpha1.newBuilder() + .setMetric(DaprVectorProtos.DistanceMetric.DISTANCE_METRIC_COSINE))) + .build())).when(daprStub).batchQueryVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE, COLLECTION, List.of(VectorQuery.byId("a"), VectorQuery.byId("b")))).block()); + } + + @Test + @DisplayName("batchQueryVectors should fail on a result with neither a response nor an error") + public void batchQueryVectorsEmptyResultTest() { + doAnswer(respond(DaprVectorProtos.BatchQueryVectorsResponseAlpha1.newBuilder() + .addResults(DaprVectorProtos.BatchQueryResultAlpha1.getDefaultInstance()) + .build())).when(daprStub).batchQueryVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE, COLLECTION, List.of(VectorQuery.byId("a")))).block()); + } + + @Test + @DisplayName("batchQueryVectors should fail on an error result with an OK status code") + public void batchQueryVectorsOkErrorTest() { + doAnswer(respond(DaprVectorProtos.BatchQueryVectorsResponseAlpha1.newBuilder() + .addResults(DaprVectorProtos.BatchQueryResultAlpha1.newBuilder() + .setError(Status.newBuilder().setCode(Code.OK_VALUE))) + .build())).when(daprStub).batchQueryVectorsAlpha1(any(), any()); + + assertThrowsDaprException("INTERNAL", () -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE, COLLECTION, List.of(VectorQuery.byId("a")))).block()); + } + + @Test + @DisplayName("batchQueryVectors should reject missing queries and invalid queries") + public void batchQueryVectorsInvalidArgumentsTest() { + List withNull = new ArrayList<>(); + withNull.add(VectorQuery.byId("a")); + withNull.add(null); + assertRejected(() -> previewClient.batchQueryVectors(null)); + assertRejected(() -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1(STORE, COLLECTION, null))); + assertRejected(() -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE, COLLECTION, withNull))); + assertRejected(() -> previewClient.batchQueryVectors(new BatchQueryVectorsRequestAlpha1( + STORE, COLLECTION, List.of(VectorQuery.byVector(new float[0]))))); + verify(daprStub, never()).batchQueryVectorsAlpha1(any(), any()); + } +} diff --git a/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java b/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java index c6bfa5eb28..c2bb247fb9 100644 --- a/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java +++ b/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java @@ -31,10 +31,40 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class ProtobufValueHelperTest { + @Test + public void testFromProtobufValue_Scalars() { + assertNull(ProtobufValueHelper.fromProtobufValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())); + assertNull(ProtobufValueHelper.fromProtobufValue(Value.getDefaultInstance())); + assertEquals(true, ProtobufValueHelper.fromProtobufValue(Value.newBuilder().setBoolValue(true).build())); + assertEquals(42.5, ProtobufValueHelper.fromProtobufValue(Value.newBuilder().setNumberValue(42.5).build())); + assertEquals("text", ProtobufValueHelper.fromProtobufValue(Value.newBuilder().setStringValue("text").build())); + } + + @Test + public void testFromProtobufStruct_RoundTrip() throws IOException { + Map nested = new LinkedHashMap<>(); + nested.put("flag", false); + nested.put("nothing", null); + Map original = new LinkedHashMap<>(); + original.put("name", "dune"); + original.put("year", 1965); + original.put("tags", Arrays.asList("classic", 3, nested)); + original.put("details", nested); + + Struct struct = ProtobufValueHelper.toProtobufValue(original).getStructValue(); + Map result = ProtobufValueHelper.fromProtobufStruct(struct); + + assertEquals("dune", result.get("name")); + assertEquals(1965.0, result.get("year")); + assertEquals(Arrays.asList("classic", 3.0, nested), result.get("tags")); + assertEquals(nested, result.get("details")); + } + @Test public void testToProtobufValue_Null() throws IOException { Value result = ProtobufValueHelper.toProtobufValue(null);