diff --git a/README.md b/README.md index 2c8757c..51c562b 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ Java client library with handy utility methods and overloads for interfacing wit ## 📥 Installation -> Not yet published. - > [!IMPORTANT] > Requires Java 8 or above. @@ -36,20 +34,20 @@ To install the library, add the following lines to your build config file. io.qdrant client - 1.7-SNAPSHOT + 1.7.0 ``` #### Scala SBT ```sbt -libraryDependencies += "io.qdrant" % "client" % "1.7-SNAPSHOT" +libraryDependencies += "io.qdrant" % "client" % "1.7.0" ``` #### Gradle ```gradle -implementation 'io.qdrant:client:1.7-SNAPSHOT' +implementation 'io.qdrant:client:1.7.0' ``` ## 📖 Documentation diff --git a/build.gradle b/build.gradle index 722d6c0..189e243 100644 --- a/build.gradle +++ b/build.gradle @@ -228,4 +228,4 @@ publishing { repositories { mavenLocal() } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 44e0858..a609339 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ qdrantProtosVersion=v1.7.0 qdrantVersion=v1.7.0 # The version of the client to generate -packageVersion=1.7-SNAPSHOT \ No newline at end of file +packageVersion=1.7.0 \ No newline at end of file diff --git a/src/main/java/io/qdrant/client/QdrantClient.java b/src/main/java/io/qdrant/client/QdrantClient.java index 680f29e..a8dcae4 100644 --- a/src/main/java/io/qdrant/client/QdrantClient.java +++ b/src/main/java/io/qdrant/client/QdrantClient.java @@ -10,6 +10,7 @@ import io.qdrant.client.grpc.CollectionsGrpc; import io.qdrant.client.grpc.PointsGrpc; import io.qdrant.client.grpc.SnapshotsGrpc; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,8 +29,16 @@ import static io.qdrant.client.grpc.Collections.CollectionOperationResponse; import static io.qdrant.client.grpc.Collections.CreateAlias; import static io.qdrant.client.grpc.Collections.CreateCollection; +import static io.qdrant.client.grpc.Collections.CreateShardKeyRequest; +import static io.qdrant.client.grpc.Collections.CreateShardKeyResponse; import static io.qdrant.client.grpc.Collections.DeleteAlias; import static io.qdrant.client.grpc.Collections.DeleteCollection; +import static io.qdrant.client.grpc.Collections.DeleteShardKeyRequest; +import static io.qdrant.client.grpc.Collections.DeleteShardKeyResponse; +import static io.qdrant.client.grpc.Points.DiscoverBatchPoints; +import static io.qdrant.client.grpc.Points.DiscoverBatchResponse; +import static io.qdrant.client.grpc.Points.DiscoverPoints; +import static io.qdrant.client.grpc.Points.DiscoverResponse; import static io.qdrant.client.grpc.Collections.GetCollectionInfoRequest; import static io.qdrant.client.grpc.Collections.GetCollectionInfoResponse; import static io.qdrant.client.grpc.Collections.ListAliasesRequest; @@ -40,6 +49,7 @@ import static io.qdrant.client.grpc.Collections.PayloadIndexParams; import static io.qdrant.client.grpc.Collections.PayloadSchemaType; import static io.qdrant.client.grpc.Collections.RenameAlias; +import static io.qdrant.client.grpc.Collections.ShardKey; import static io.qdrant.client.grpc.Collections.UpdateCollection; import static io.qdrant.client.grpc.Collections.VectorParams; import static io.qdrant.client.grpc.Collections.VectorParamsMap; @@ -665,6 +675,78 @@ public ListenableFuture> listAliasesAsync(@Nullable Durat //endregion + //region ShardKey Management + + /** + * Creates a shard key for a collection. + * + * @param createShardKey The request object for the operation. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture createShardKeyAsync(CreateShardKeyRequest createShardKey) { + return createShardKeyAsync(createShardKey, null); + } + + /** + * Creates a shard key for a collection. + * + * @param createShardKey The request object for the operation. + * @param timeout The timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture createShardKeyAsync(CreateShardKeyRequest createShardKey, @Nullable Duration timeout) { + String collectionName = createShardKey.getCollectionName(); + Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty"); + ShardKey shardKey = createShardKey.getRequest().getShardKey(); + logger.debug("Create shard key '{}' for '{}'", shardKey, collectionName); + + ListenableFuture future = getCollections(timeout).createShardKey(createShardKey); + addLogFailureCallback(future, "Create shard key"); + return Futures.transform(future, response -> { + if (!response.getResult()) { + logger.error("Shard key could not be created for '{}'", collectionName); + throw new QdrantException("Shard key " + shardKey + " could not be created for " + collectionName); + } + return response; + }, MoreExecutors.directExecutor()); + } + + /** + * Deletes a shard key for a collection. + * + * @param deleteShardKey The request object for the operation. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture deleteShardKeyAsync(DeleteShardKeyRequest deleteShardKey) { + return deleteShardKeyAsync(deleteShardKey, null); + } + + /** + * Deletes a shard key for a collection. + * + * @param deleteShardKey The request object for the operation. + * @param timeout The timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture deleteShardKeyAsync(DeleteShardKeyRequest deleteShardKey, @Nullable Duration timeout) { + String collectionName = deleteShardKey.getCollectionName(); + Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty"); + ShardKey shardKey = deleteShardKey.getRequest().getShardKey(); + logger.debug("Delete shard key '{}' for '{}'", shardKey, collectionName); + + ListenableFuture future = getCollections(timeout).deleteShardKey(deleteShardKey); + addLogFailureCallback(future, "Delete shard key"); + return Futures.transform(future, response -> { + if (!response.getResult()) { + logger.error("Shard key '{}' could not be deleted for '{}'", shardKey, collectionName); + throw new QdrantException("Shard key " + shardKey + " could not be created for " + collectionName); + } + return response; + }, MoreExecutors.directExecutor()); + } + + //endregion + //region Point Management /** @@ -2153,6 +2235,88 @@ public ListenableFuture> recommendGroupsAsync(RecommendPointGro MoreExecutors.directExecutor()); } + /** + * Use the context and a target to find the most similar points to the target. + * Constraints by the context. + * + * @param request The discover points request + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture> discoverAsync(DiscoverPoints request) { + return discoverAsync(request, null); + } + + /** + * Use the context and a target to find the most similar points to the target. + * Constraints by the context. + * + * @param request The discover points request + * @param timeout The timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture> discoverAsync(DiscoverPoints request, @Nullable Duration timeout) { + String collectionName = request.getCollectionName(); + Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty"); + logger.debug("Discover on '{}'", collectionName); + ListenableFuture future = getPoints(timeout).discover(request); + addLogFailureCallback(future, "Discover"); + return Futures.transform( + future, + response -> response.getResultList(), + MoreExecutors.directExecutor()); + } + + /** + * Use the context and a target to find the most similar points to the target in + * a batch. + * Constrained by the context. + * + * @param collectionName The name of the collection + * @param discoverSearches The list for discover point searches + * @param readConsistency Options for specifying read consistency guarantees + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture> discoverBatchAsync( + String collectionName, + List discoverSearches, + @Nullable ReadConsistency readConsistency) { + return discoverBatchAsync(collectionName, discoverSearches, readConsistency, null); + } + + /** + * Use the context and a target to find the most similar points to the target in + * a batch. + * Constrained by the context. + * + * @param collectionName The name of the collection + * @param discoverSearches The list for discover point searches + * @param readConsistency Options for specifying read consistency guarantees + * @param timeout The timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture> discoverBatchAsync( + String collectionName, + List discoverSearches, + @Nullable ReadConsistency readConsistency, + @Nullable Duration timeout) { + Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty"); + + DiscoverBatchPoints.Builder requestBuilder = DiscoverBatchPoints.newBuilder() + .setCollectionName(collectionName) + .addAllDiscoverPoints(discoverSearches); + + if (readConsistency != null) { + requestBuilder.setReadConsistency(readConsistency); + } + logger.debug("Discover batch on '{}'", collectionName); + ListenableFuture future = getPoints(timeout).discoverBatch(requestBuilder.build()); + addLogFailureCallback(future, "Discover batch"); + return Futures.transform( + future, + response -> response.getResultList(), + MoreExecutors.directExecutor()); + } + /** * Count the points in a collection. The count is exact * diff --git a/src/main/java/io/qdrant/client/ShardKeyFactory.java b/src/main/java/io/qdrant/client/ShardKeyFactory.java new file mode 100644 index 0000000..262a4ed --- /dev/null +++ b/src/main/java/io/qdrant/client/ShardKeyFactory.java @@ -0,0 +1,31 @@ +package io.qdrant.client; + +import io.qdrant.client.grpc.Collections.ShardKey; + +/** + * Convenience methods for constructing {@link ShardKey} + */ +public final class ShardKeyFactory { + private ShardKeyFactory() { + } + + /** + * Creates a {@link ShardKey} based on a keyword. + * + * @param keyword The keyword to create the shard key from + * @return The {@link ShardKey} object + */ + public static ShardKey shardKey(String keyword) { + return ShardKey.newBuilder().setKeyword(keyword).build(); + } + + /** + * Creates a {@link ShardKey} based on a number. + * + * @param number The number to create the shard key from + * @return The {@link ShardKey} object + */ + public static ShardKey shardKey(long number) { + return ShardKey.newBuilder().setNumber(number).build(); + } +} diff --git a/src/main/java/io/qdrant/client/ShardKeySelectorFactory.java b/src/main/java/io/qdrant/client/ShardKeySelectorFactory.java new file mode 100644 index 0000000..a171f11 --- /dev/null +++ b/src/main/java/io/qdrant/client/ShardKeySelectorFactory.java @@ -0,0 +1,54 @@ +package io.qdrant.client; + +import io.qdrant.client.grpc.Collections.ShardKey; +import io.qdrant.client.grpc.Points.ShardKeySelector; + +import static io.qdrant.client.ShardKeyFactory.shardKey; + +import java.util.Arrays; + +/** + * Convenience methods for constructing {@link ShardKeySelector} + */ +public class ShardKeySelectorFactory { + private ShardKeySelectorFactory() { + } + + /** + * Creates a {@link ShardKeySelector} with the given shard keys. + * + * @param shardKeys The shard keys to include in the selector. + * @return The created {@link ShardKeySelector} object. + */ + public static ShardKeySelector shardKeySelector(ShardKey... shardKeys) { + return ShardKeySelector.newBuilder().addAllShardKeys(Arrays.asList(shardKeys)).build(); + } + + /** + * Creates a {@link ShardKeySelector} with the given shard key keywords. + * + * @param keywords The shard key keywords to include in the selector. + * @return The created {@link ShardKeySelector} object. + */ + public static ShardKeySelector shardKeySelector(String... keywords) { + ShardKeySelector.Builder builder = ShardKeySelector.newBuilder(); + for (String keyword : keywords) { + builder.addShardKeys(shardKey(keyword)); + } + return builder.build(); + } + + /** + * Creates a {@link ShardKeySelector} with the given shard key numbers. + * + * @param numbers The shard key numbers to include in the selector. + * @return The created {@link ShardKeySelector} object. + */ + public static ShardKeySelector shardKeySelector(long... numbers) { + ShardKeySelector.Builder builder = ShardKeySelector.newBuilder(); + for (long number : numbers) { + builder.addShardKeys(shardKey(number)); + } + return builder.build(); + } +} diff --git a/src/main/java/io/qdrant/client/TargetVectorFactory.java b/src/main/java/io/qdrant/client/TargetVectorFactory.java new file mode 100644 index 0000000..f62c32b --- /dev/null +++ b/src/main/java/io/qdrant/client/TargetVectorFactory.java @@ -0,0 +1,32 @@ +package io.qdrant.client; + +import io.qdrant.client.grpc.Points.PointId; +import io.qdrant.client.grpc.Points.TargetVector; +import io.qdrant.client.grpc.Points.Vector; +import io.qdrant.client.grpc.Points.VectorExample; + +/** + * Convenience methods for constructing {@link TargetVector} + */ +public class TargetVectorFactory { + private TargetVectorFactory() { + } + + /** + * Creates a TargetVector from a point ID + * @param id The point ID to use + * @return A new instance of {@link TargetVector} + */ + public static TargetVector targetVector(PointId id) { + return TargetVector.newBuilder().setSingle(VectorExample.newBuilder().setId(id)).build(); + } + + /** + * Creates a TargetVector from a Vector + * @param vector The Vector value to use + * @return A new instance of {@link TargetVector} + */ + public static TargetVector targetVector(Vector vector) { + return TargetVector.newBuilder().setSingle(VectorExample.newBuilder().setVector(vector)).build(); + } +} diff --git a/src/main/java/io/qdrant/client/VectorFactory.java b/src/main/java/io/qdrant/client/VectorFactory.java new file mode 100644 index 0000000..c82ce12 --- /dev/null +++ b/src/main/java/io/qdrant/client/VectorFactory.java @@ -0,0 +1,52 @@ +package io.qdrant.client; + +import com.google.common.primitives.Floats; + +import java.util.List; + +import static io.qdrant.client.grpc.Points.SparseIndices; +import static io.qdrant.client.grpc.Points.Vector; + +/** + * Convenience methods for constructing {@link Vector} + */ +public final class VectorFactory { + private VectorFactory() { + } + + /** + * Creates a vector from a list of floats + * + * @param values A map of vector names to values + * @return A new instance of {@link Vector} + */ + public static Vector vector(List values) { + return Vector.newBuilder().addAllData(values).build(); + } + + /** + * Creates a vector from a list of floats + * + * @param values A list of values + * @return A new instance of {@link Vector} + */ + public static Vector vector(float... values) { + return Vector.newBuilder() + .addAllData(Floats.asList(values)) + .build(); + } + + /** + * Creates a sparse vector from a list of floats and integers as indices + * + * @param vector The list of floats representing the vector. + * @param indices The list of integers representing the indices. + * @return A new instance of {@link Vector} + */ + public static Vector vector(List vector, List indices) { + return Vector.newBuilder() + .addAllData(vector) + .setIndices(SparseIndices.newBuilder().addAllData(indices).build()) + .build(); + } +} diff --git a/src/main/java/io/qdrant/client/VectorsFactory.java b/src/main/java/io/qdrant/client/VectorsFactory.java index 0e2c348..6fe9864 100644 --- a/src/main/java/io/qdrant/client/VectorsFactory.java +++ b/src/main/java/io/qdrant/client/VectorsFactory.java @@ -1,13 +1,12 @@ package io.qdrant.client; import com.google.common.collect.Maps; -import com.google.common.primitives.Floats; import java.util.List; import java.util.Map; +import static io.qdrant.client.VectorFactory.vector; import static io.qdrant.client.grpc.Points.NamedVectors; -import static io.qdrant.client.grpc.Points.Vector; import static io.qdrant.client.grpc.Points.Vectors; /** @@ -25,9 +24,7 @@ private VectorsFactory() { public static Vectors namedVectors(Map> values) { return Vectors.newBuilder() .setVectors(NamedVectors.newBuilder() - .putAllVectors(Maps.transformValues(values, v -> Vector.newBuilder() - .addAllData(v) - .build())) + .putAllVectors(Maps.transformValues(values, v -> vector(v))) ) .build(); } @@ -37,11 +34,9 @@ public static Vectors namedVectors(Map> values) { * @param values A list of values * @return a new instance of {@link Vectors} */ - public static Vectors vector(List values) { + public static Vectors vectors(List values) { return Vectors.newBuilder() - .setVector(Vector.newBuilder() - .addAllData(values) - .build()) + .setVector(vector(values)) .build(); } @@ -50,11 +45,9 @@ public static Vectors vector(List values) { * @param values A list of values * @return a new instance of {@link Vectors} */ - public static Vectors vector(float... values) { + public static Vectors vectors(float... values) { return Vectors.newBuilder() - .setVector(Vector.newBuilder() - .addAllData(Floats.asList(values)) - .build()) + .setVector(vector(values)) .build(); } } diff --git a/src/test/java/io/qdrant/client/PointsTest.java b/src/test/java/io/qdrant/client/PointsTest.java index 82c30c5..75cb540 100644 --- a/src/test/java/io/qdrant/client/PointsTest.java +++ b/src/test/java/io/qdrant/client/PointsTest.java @@ -13,6 +13,7 @@ import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; import io.qdrant.client.container.QdrantContainer; +import io.qdrant.client.grpc.Points.DiscoverPoints; import java.util.List; import java.util.concurrent.ExecutionException; @@ -45,8 +46,9 @@ import static io.qdrant.client.ConditionFactory.hasId; import static io.qdrant.client.ConditionFactory.matchKeyword; import static io.qdrant.client.PointIdFactory.id; +import static io.qdrant.client.TargetVectorFactory.targetVector; import static io.qdrant.client.ValueFactory.value; -import static io.qdrant.client.VectorsFactory.vector; +import static io.qdrant.client.VectorFactory.vector; @Testcontainers class PointsTest { @@ -307,7 +309,7 @@ public void searchGroups() throws ExecutionException, InterruptedException { ImmutableList.of( PointStruct.newBuilder() .setId(id(10)) - .setVectors(VectorsFactory.vector(30f, 31f)) + .setVectors(VectorsFactory.vectors(30f, 31f)) .putAllPayload(ImmutableMap.of("foo", value("hello"))) .build() ) @@ -404,7 +406,7 @@ public void recommendGroups() throws ExecutionException, InterruptedException { ImmutableList.of( PointStruct.newBuilder() .setId(id(10)) - .setVectors(VectorsFactory.vector(30f, 31f)) + .setVectors(VectorsFactory.vectors(30f, 31f)) .putAllPayload(ImmutableMap.of("foo", value("hello"))) .build() ) @@ -423,6 +425,48 @@ public void recommendGroups() throws ExecutionException, InterruptedException { assertEquals(2, groups.get(0).getHitsCount()); } + @Test + public void discover() throws ExecutionException, InterruptedException { + createAndSeedCollection(testName); + + List points = client.discoverAsync(DiscoverPoints.newBuilder() + .setCollectionName(testName) + .setTarget(targetVector(vector(ImmutableList.of(10.4f, 11.4f)))) + .setLimit(1) + .build()).get(); + + assertEquals(1, points.size()); + assertEquals(id(9), points.get(0).getId()); + } + + @Test + public void discoverBatch() throws ExecutionException, InterruptedException { + createAndSeedCollection(testName); + + List batchResults = client.discoverBatchAsync( + testName, + ImmutableList.of( + DiscoverPoints.newBuilder() + .setCollectionName(testName) + .setTarget(targetVector(vector(ImmutableList.of(10.4f, 11.4f)))) + .setLimit(1) + .build(), + DiscoverPoints.newBuilder() + .setCollectionName(testName) + .setTarget(targetVector(vector(ImmutableList.of(3.5f, 4.5f)))) + .setLimit(1) + .build()), + null).get(); + + assertEquals(2, batchResults.size()); + BatchResult result = batchResults.get(0); + assertEquals(1, result.getResultCount()); + assertEquals(id(9), result.getResult(0).getId()); + result = batchResults.get(1); + assertEquals(1, result.getResultCount()); + assertEquals(id(8), result.getResult(0).getId()); + } + @Test public void count() throws ExecutionException, InterruptedException { createAndSeedCollection(testName); @@ -512,7 +556,7 @@ private void createAndSeedCollection(String collectionName) throws ExecutionExce UpdateResult result = client.upsertAsync(collectionName, ImmutableList.of( PointStruct.newBuilder() .setId(id(8)) - .setVectors(VectorsFactory.vector(ImmutableList.of(3.5f, 4.5f))) + .setVectors(VectorsFactory.vectors(ImmutableList.of(3.5f, 4.5f))) .putAllPayload(ImmutableMap.of( "foo", value("hello"), "bar", value(1) @@ -520,7 +564,7 @@ private void createAndSeedCollection(String collectionName) throws ExecutionExce .build(), PointStruct.newBuilder() .setId(id(9)) - .setVectors(VectorsFactory.vector(ImmutableList.of(10.5f, 11.5f))) + .setVectors(VectorsFactory.vectors(ImmutableList.of(10.5f, 11.5f))) .putAllPayload(ImmutableMap.of( "foo", value("goodbye"), "bar", value(2)