diff --git a/gradle.properties b/gradle.properties index 70abb120..b4ca498b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ # The version of qdrant to use to download protos -qdrantProtosVersion=v1.15.0 +qdrantProtosVersion=v1.16.0 # The version of qdrant docker image to run integration tests against -qdrantVersion=v1.15.0 +qdrantVersion=v1.16.0 # The version of the client to generate -packageVersion=1.15.0 +packageVersion=1.16.0 diff --git a/src/main/java/io/qdrant/client/ConditionFactory.java b/src/main/java/io/qdrant/client/ConditionFactory.java index 27d77750..d9a04bfa 100644 --- a/src/main/java/io/qdrant/client/ConditionFactory.java +++ b/src/main/java/io/qdrant/client/ConditionFactory.java @@ -1,25 +1,25 @@ package io.qdrant.client; -import io.qdrant.client.grpc.Points.Condition; -import io.qdrant.client.grpc.Points.DatetimeRange; -import io.qdrant.client.grpc.Points.FieldCondition; -import io.qdrant.client.grpc.Points.Filter; -import io.qdrant.client.grpc.Points.GeoBoundingBox; -import io.qdrant.client.grpc.Points.GeoLineString; -import io.qdrant.client.grpc.Points.GeoPoint; -import io.qdrant.client.grpc.Points.GeoPolygon; -import io.qdrant.client.grpc.Points.GeoRadius; -import io.qdrant.client.grpc.Points.HasIdCondition; -import io.qdrant.client.grpc.Points.HasVectorCondition; -import io.qdrant.client.grpc.Points.IsEmptyCondition; -import io.qdrant.client.grpc.Points.IsNullCondition; -import io.qdrant.client.grpc.Points.Match; -import io.qdrant.client.grpc.Points.NestedCondition; -import io.qdrant.client.grpc.Points.PointId; -import io.qdrant.client.grpc.Points.Range; -import io.qdrant.client.grpc.Points.RepeatedIntegers; -import io.qdrant.client.grpc.Points.RepeatedStrings; -import io.qdrant.client.grpc.Points.ValuesCount; +import io.qdrant.client.grpc.Common.Condition; +import io.qdrant.client.grpc.Common.DatetimeRange; +import io.qdrant.client.grpc.Common.FieldCondition; +import io.qdrant.client.grpc.Common.Filter; +import io.qdrant.client.grpc.Common.GeoBoundingBox; +import io.qdrant.client.grpc.Common.GeoLineString; +import io.qdrant.client.grpc.Common.GeoPoint; +import io.qdrant.client.grpc.Common.GeoPolygon; +import io.qdrant.client.grpc.Common.GeoRadius; +import io.qdrant.client.grpc.Common.HasIdCondition; +import io.qdrant.client.grpc.Common.HasVectorCondition; +import io.qdrant.client.grpc.Common.IsEmptyCondition; +import io.qdrant.client.grpc.Common.IsNullCondition; +import io.qdrant.client.grpc.Common.Match; +import io.qdrant.client.grpc.Common.NestedCondition; +import io.qdrant.client.grpc.Common.PointId; +import io.qdrant.client.grpc.Common.Range; +import io.qdrant.client.grpc.Common.RepeatedIntegers; +import io.qdrant.client.grpc.Common.RepeatedStrings; +import io.qdrant.client.grpc.Common.ValuesCount; import java.util.List; /** Convenience methods for constructing {@link Condition} */ @@ -125,6 +125,23 @@ public static Condition matchPhrase(String field, String phrase) { .build(); } + /** + * Match records where the given field matches any word in the text. + * + * @param field The name of the field + * @param textAny The text to match + * @return a new instance of {@link Condition} + */ + public static Condition matchTextAny(String field, String textAny) { + return Condition.newBuilder() + .setField( + FieldCondition.newBuilder() + .setKey(field) + .setMatch(Match.newBuilder().setTextAny(textAny).build()) + .build()) + .build(); + } + /** * Match records where the given field matches the given boolean value. * diff --git a/src/main/java/io/qdrant/client/ExpressionFactory.java b/src/main/java/io/qdrant/client/ExpressionFactory.java index ffaaa758..604ab2cb 100644 --- a/src/main/java/io/qdrant/client/ExpressionFactory.java +++ b/src/main/java/io/qdrant/client/ExpressionFactory.java @@ -1,6 +1,6 @@ package io.qdrant.client; -import io.qdrant.client.grpc.Points.Condition; +import io.qdrant.client.grpc.Common.Condition; import io.qdrant.client.grpc.Points.DecayParamsExpression; import io.qdrant.client.grpc.Points.DivExpression; import io.qdrant.client.grpc.Points.Expression; diff --git a/src/main/java/io/qdrant/client/PointIdFactory.java b/src/main/java/io/qdrant/client/PointIdFactory.java index 35844562..86a744d9 100644 --- a/src/main/java/io/qdrant/client/PointIdFactory.java +++ b/src/main/java/io/qdrant/client/PointIdFactory.java @@ -1,6 +1,6 @@ package io.qdrant.client; -import io.qdrant.client.grpc.Points.PointId; +import io.qdrant.client.grpc.Common.PointId; import java.util.UUID; /** Convenience methods for constructing {@link PointId} */ diff --git a/src/main/java/io/qdrant/client/QdrantClient.java b/src/main/java/io/qdrant/client/QdrantClient.java index 7ed2de21..717e798f 100644 --- a/src/main/java/io/qdrant/client/QdrantClient.java +++ b/src/main/java/io/qdrant/client/QdrantClient.java @@ -10,6 +10,8 @@ import io.qdrant.client.grpc.Collections.AliasDescription; import io.qdrant.client.grpc.Collections.AliasOperations; import io.qdrant.client.grpc.Collections.ChangeAliases; +import io.qdrant.client.grpc.Collections.CollectionClusterInfoRequest; +import io.qdrant.client.grpc.Collections.CollectionClusterInfoResponse; import io.qdrant.client.grpc.Collections.CollectionDescription; import io.qdrant.client.grpc.Collections.CollectionExistsRequest; import io.qdrant.client.grpc.Collections.CollectionExistsResponse; @@ -35,10 +37,14 @@ import io.qdrant.client.grpc.Collections.RenameAlias; import io.qdrant.client.grpc.Collections.ShardKey; import io.qdrant.client.grpc.Collections.UpdateCollection; +import io.qdrant.client.grpc.Collections.UpdateCollectionClusterSetupRequest; +import io.qdrant.client.grpc.Collections.UpdateCollectionClusterSetupResponse; import io.qdrant.client.grpc.Collections.VectorParams; import io.qdrant.client.grpc.Collections.VectorParamsMap; import io.qdrant.client.grpc.Collections.VectorsConfig; import io.qdrant.client.grpc.CollectionsGrpc; +import io.qdrant.client.grpc.Common.Filter; +import io.qdrant.client.grpc.Common.PointId; import io.qdrant.client.grpc.JsonWithInt.Value; import io.qdrant.client.grpc.Points; import io.qdrant.client.grpc.Points.BatchResult; @@ -55,11 +61,9 @@ import io.qdrant.client.grpc.Points.DiscoverPoints; import io.qdrant.client.grpc.Points.DiscoverResponse; import io.qdrant.client.grpc.Points.FieldType; -import io.qdrant.client.grpc.Points.Filter; import io.qdrant.client.grpc.Points.GetPoints; import io.qdrant.client.grpc.Points.GetResponse; import io.qdrant.client.grpc.Points.PointGroup; -import io.qdrant.client.grpc.Points.PointId; import io.qdrant.client.grpc.Points.PointStruct; import io.qdrant.client.grpc.Points.PointVectors; import io.qdrant.client.grpc.Points.PointsIdsList; @@ -400,6 +404,35 @@ public ListenableFuture getCollectionInfoAsync( future, GetCollectionInfoResponse::getResult, MoreExecutors.directExecutor()); } + /** + * Gets detailed information about a collection's cluster setup. + * + * @param collectionName The name of the collection. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture getCollectionClusterInfoAsync( + String collectionName) { + return getCollectionClusterInfoAsync(collectionName, null); + } + + /** + * Gets detailed information about a collection's cluster setup. + * + * @param collectionName The name of the collection. + * @param timeout The timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture getCollectionClusterInfoAsync( + String collectionName, @Nullable Duration timeout) { + logger.debug("Get collection cluster info for '{}'", collectionName); + CollectionClusterInfoRequest request = + CollectionClusterInfoRequest.newBuilder().setCollectionName(collectionName).build(); + ListenableFuture future = + getCollections(timeout).collectionClusterInfo(request); + addLogFailureCallback(future, "Get collection cluster info"); + return future; + } + /** * Deletes a collection and all its associated data. * @@ -511,6 +544,47 @@ public ListenableFuture updateCollectionAsync( MoreExecutors.directExecutor()); } + /** + * Update cluster setup for a collection + * + * @param updateCollectionClusterSetup The update parameters. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture updateCollectionClusterSetupAsync( + UpdateCollectionClusterSetupRequest updateCollectionClusterSetup) { + return updateCollectionClusterSetupAsync(updateCollectionClusterSetup, null); + } + + /** + * Update cluster setup for a collection + * + * @param updateCollectionClusterSetup The update parameters. + * @param timeout The timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture updateCollectionClusterSetupAsync( + UpdateCollectionClusterSetupRequest updateCollectionClusterSetup, + @Nullable Duration timeout) { + String collectionName = updateCollectionClusterSetup.getCollectionName(); + Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty"); + logger.debug("Update collection cluster setup'{}'", collectionName); + + ListenableFuture future = + getCollections(timeout).updateCollectionClusterSetup(updateCollectionClusterSetup); + addLogFailureCallback(future, "Update collection"); + return Futures.transform( + future, + response -> { + if (!response.getResult()) { + logger.error("Cluster setup of collection '{}' could not be updated", collectionName); + throw new QdrantException( + "Cluster setup of collection '" + collectionName + "' could not be updated"); + } + return response; + }, + MoreExecutors.directExecutor()); + } + /** * Check if a collection exists * @@ -1212,6 +1286,16 @@ public ListenableFuture updateVectorsAsync( return updateVectorsAsync(requestBuilder.build(), timeout); } + /** + * Update named vectors for point. + * + * @param request The update point vectors request + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture updateVectorsAsync(UpdatePointVectors request) { + return updateVectorsAsync(request, null); + } + /** * Update named vectors for point. * diff --git a/src/main/java/io/qdrant/client/QueryFactory.java b/src/main/java/io/qdrant/client/QueryFactory.java index 22b4358c..40a283c0 100644 --- a/src/main/java/io/qdrant/client/QueryFactory.java +++ b/src/main/java/io/qdrant/client/QueryFactory.java @@ -3,6 +3,7 @@ import static io.qdrant.client.VectorInputFactory.multiVectorInput; import static io.qdrant.client.VectorInputFactory.vectorInput; +import io.qdrant.client.grpc.Common.PointId; import io.qdrant.client.grpc.Points.ContextInput; import io.qdrant.client.grpc.Points.DiscoverInput; import io.qdrant.client.grpc.Points.Document; @@ -13,9 +14,9 @@ import io.qdrant.client.grpc.Points.Mmr; import io.qdrant.client.grpc.Points.NearestInputWithMmr; import io.qdrant.client.grpc.Points.OrderBy; -import io.qdrant.client.grpc.Points.PointId; import io.qdrant.client.grpc.Points.Query; import io.qdrant.client.grpc.Points.RecommendInput; +import io.qdrant.client.grpc.Points.Rrf; import io.qdrant.client.grpc.Points.Sample; import io.qdrant.client.grpc.Points.VectorInput; import java.util.List; @@ -65,6 +66,16 @@ public static Query fusion(Fusion fusion) { return Query.newBuilder().setFusion(fusion).build(); } + /** + * Creates a {@link Query} for reciprocal rank fusion (RRF). + * + * @param rrf An instance of {@link Rrf} + * @return a new instance of {@link Query} + */ + public static Query rrf(Rrf rrf) { + return Query.newBuilder().setRrf(rrf).build(); + } + /** * Creates a {@link Query} to order points by a payload field. * diff --git a/src/main/java/io/qdrant/client/TargetVectorFactory.java b/src/main/java/io/qdrant/client/TargetVectorFactory.java index a7c50680..04cc83cd 100644 --- a/src/main/java/io/qdrant/client/TargetVectorFactory.java +++ b/src/main/java/io/qdrant/client/TargetVectorFactory.java @@ -1,6 +1,6 @@ package io.qdrant.client; -import io.qdrant.client.grpc.Points.PointId; +import io.qdrant.client.grpc.Common.PointId; import io.qdrant.client.grpc.Points.TargetVector; import io.qdrant.client.grpc.Points.Vector; import io.qdrant.client.grpc.Points.VectorExample; diff --git a/src/main/java/io/qdrant/client/VectorFactory.java b/src/main/java/io/qdrant/client/VectorFactory.java index dc5607f0..dfde007a 100644 --- a/src/main/java/io/qdrant/client/VectorFactory.java +++ b/src/main/java/io/qdrant/client/VectorFactory.java @@ -1,10 +1,12 @@ package io.qdrant.client; import com.google.common.primitives.Floats; +import io.qdrant.client.grpc.Points.DenseVector; import io.qdrant.client.grpc.Points.Document; import io.qdrant.client.grpc.Points.Image; import io.qdrant.client.grpc.Points.InferenceObject; -import io.qdrant.client.grpc.Points.SparseIndices; +import io.qdrant.client.grpc.Points.MultiDenseVector; +import io.qdrant.client.grpc.Points.SparseVector; import io.qdrant.client.grpc.Points.Vector; import java.util.ArrayList; import java.util.List; @@ -21,7 +23,9 @@ private VectorFactory() {} * @return A new instance of {@link Vector} */ public static Vector vector(List values) { - return Vector.newBuilder().addAllData(values).build(); + return Vector.newBuilder() + .setDense(DenseVector.newBuilder().addAllData(values).build()) + .build(); } /** @@ -31,7 +35,9 @@ public static Vector vector(List values) { * @return A new instance of {@link Vector} */ public static Vector vector(float... values) { - return Vector.newBuilder().addAllData(Floats.asList(values)).build(); + return Vector.newBuilder() + .setDense(DenseVector.newBuilder().addAllData(Floats.asList(values)).build()) + .build(); } /** @@ -43,8 +49,7 @@ public static Vector vector(float... values) { */ public static Vector vector(List vector, List indices) { return Vector.newBuilder() - .addAllData(vector) - .setIndices(SparseIndices.newBuilder().addAllData(indices).build()) + .setSparse(SparseVector.newBuilder().addAllValues(vector).addAllIndices(indices).build()) .build(); } @@ -85,10 +90,13 @@ public static Vector vector(InferenceObject object) { * @return A new instance of {@link Vector} */ public static Vector multiVector(List> vectors) { - int vectorSize = vectors.size(); - List flatVector = vectors.stream().flatMap(List::stream).collect(Collectors.toList()); - - return Vector.newBuilder().addAllData(flatVector).setVectorsCount(vectorSize).build(); + List denseVectors = + vectors.stream() + .map(v -> DenseVector.newBuilder().addAllData(v).build()) + .collect(Collectors.toList()); + return Vector.newBuilder() + .setMultiDense(MultiDenseVector.newBuilder().addAllVectors(denseVectors).build()) + .build(); } /** @@ -98,15 +106,12 @@ public static Vector multiVector(List> vectors) { * @return A new instance of {@link Vector} */ public static Vector multiVector(float[][] vectors) { - int vectorSize = vectors.length; - - List flatVector = new ArrayList<>(); + List denseVectors = new ArrayList<>(); for (float[] vector : vectors) { - for (float value : vector) { - flatVector.add(value); - } + denseVectors.add(DenseVector.newBuilder().addAllData(Floats.asList(vector)).build()); } - - return Vector.newBuilder().addAllData(flatVector).setVectorsCount(vectorSize).build(); + return Vector.newBuilder() + .setMultiDense(MultiDenseVector.newBuilder().addAllVectors(denseVectors).build()) + .build(); } } diff --git a/src/main/java/io/qdrant/client/VectorInputFactory.java b/src/main/java/io/qdrant/client/VectorInputFactory.java index 30d20ce6..e41dafd7 100644 --- a/src/main/java/io/qdrant/client/VectorInputFactory.java +++ b/src/main/java/io/qdrant/client/VectorInputFactory.java @@ -3,12 +3,12 @@ import static io.qdrant.client.PointIdFactory.id; import com.google.common.primitives.Floats; +import io.qdrant.client.grpc.Common.PointId; import io.qdrant.client.grpc.Points.DenseVector; import io.qdrant.client.grpc.Points.Document; import io.qdrant.client.grpc.Points.Image; import io.qdrant.client.grpc.Points.InferenceObject; import io.qdrant.client.grpc.Points.MultiDenseVector; -import io.qdrant.client.grpc.Points.PointId; import io.qdrant.client.grpc.Points.SparseVector; import io.qdrant.client.grpc.Points.VectorInput; import java.util.ArrayList; diff --git a/src/main/java/io/qdrant/client/VectorOutputHelper.java b/src/main/java/io/qdrant/client/VectorOutputHelper.java new file mode 100644 index 00000000..87531de2 --- /dev/null +++ b/src/main/java/io/qdrant/client/VectorOutputHelper.java @@ -0,0 +1,103 @@ +package io.qdrant.client; + +import io.qdrant.client.grpc.Points.DenseVector; +import io.qdrant.client.grpc.Points.MultiDenseVector; +import io.qdrant.client.grpc.Points.SparseIndices; +import io.qdrant.client.grpc.Points.SparseVector; +import io.qdrant.client.grpc.Points.VectorOutput; +import java.util.ArrayList; +import java.util.List; + +/** Helper methods for extracting vector data from {@link VectorOutput}. */ +public final class VectorOutputHelper { + private VectorOutputHelper() {} + + /** + * Returns the DenseVector from the VectorOutput. + * + * @param vectorOutput the VectorOutput to extract from + * @return the DenseVector if available, null otherwise + */ + public static DenseVector getDenseVector(VectorOutput vectorOutput) { + if (vectorOutput == null) { + return null; + } + + List data = vectorOutput.getDataList(); + if (!data.isEmpty()) { + return DenseVector.newBuilder().addAllData(data).build(); + } + + if (vectorOutput.hasDense()) { + return vectorOutput.getDense(); + } + + return null; + } + + /** + * Returns the SparseVector from the VectorOutput. + * + * @param vectorOutput the VectorOutput to extract from + * @return the SparseVector if available, null otherwise + */ + public static SparseVector getSparseVector(VectorOutput vectorOutput) { + if (vectorOutput == null) { + return null; + } + + List data = vectorOutput.getDataList(); + if (!data.isEmpty()) { + if (vectorOutput.hasIndices()) { + SparseIndices indices = vectorOutput.getIndices(); + return SparseVector.newBuilder() + .addAllValues(data) + .addAllIndices(indices.getDataList()) + .build(); + } + } + + if (vectorOutput.hasSparse()) { + return vectorOutput.getSparse(); + } + + return null; + } + + /** + * Returns the MultiDenseVector from the VectorOutput. + * + * @param vectorOutput the VectorOutput to extract from + * @return the MultiDenseVector if available, null otherwise + */ + public static MultiDenseVector getMultiVector(VectorOutput vectorOutput) { + if (vectorOutput == null) { + return null; + } + + List data = vectorOutput.getDataList(); + if (!data.isEmpty()) { + int vectorsCount = vectorOutput.getVectorsCount(); + if (vectorsCount > 0) { + int vectorSize = data.size() / vectorsCount; + List vectors = new ArrayList<>(vectorsCount); + + for (int i = 0; i < vectorsCount; i++) { + int start = i * vectorSize; + int end = start + vectorSize; + List vectorData = data.subList(start, end); + + vectors.add(DenseVector.newBuilder().addAllData(vectorData).build()); + } + + return MultiDenseVector.newBuilder().addAllVectors(vectors).build(); + } + } + + if (vectorOutput.hasMultiDense()) { + return vectorOutput.getMultiDense(); + } + + return null; + } +} diff --git a/src/test/java/io/qdrant/client/ApiKeyTest.java b/src/test/java/io/qdrant/client/ApiKeyTest.java index 48eccf9e..3174fbb7 100644 --- a/src/test/java/io/qdrant/client/ApiKeyTest.java +++ b/src/test/java/io/qdrant/client/ApiKeyTest.java @@ -9,8 +9,8 @@ import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.StatusRuntimeException; -import io.qdrant.client.grpc.QdrantOuterClass.HealthCheckReply; -import io.qdrant.client.grpc.QdrantOuterClass.HealthCheckRequest; +import io.qdrant.client.grpc.Collections.ListCollectionsRequest; +import io.qdrant.client.grpc.Collections.ListCollectionsResponse; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; @@ -44,15 +44,14 @@ public void teardown() throws InterruptedException { @Test public void client_with_api_key_can_connect() throws ExecutionException, InterruptedException { - HealthCheckReply healthCheckReply; + ListCollectionsResponse listCollectionsResponse; try (QdrantGrpcClient grpcClient = QdrantGrpcClient.newBuilder(channel).withApiKey("password!").build()) { - healthCheckReply = - grpcClient.qdrant().healthCheck(HealthCheckRequest.getDefaultInstance()).get(); + listCollectionsResponse = + grpcClient.collections().list(ListCollectionsRequest.getDefaultInstance()).get(); } - assertNotNull(healthCheckReply.getTitle()); - assertNotNull(healthCheckReply.getVersion()); + assertNotNull(listCollectionsResponse.getCollectionsList()); } @Test @@ -61,7 +60,8 @@ public void client_without_api_key_cannot_connect() { ExecutionException executionException = assertThrows( ExecutionException.class, - () -> grpcClient.qdrant().healthCheck(HealthCheckRequest.getDefaultInstance()).get()); + () -> + grpcClient.collections().list(ListCollectionsRequest.getDefaultInstance()).get()); Throwable cause = executionException.getCause(); assertEquals(StatusRuntimeException.class, cause.getClass()); StatusRuntimeException statusRuntimeException = (StatusRuntimeException) cause; diff --git a/src/test/java/io/qdrant/client/PointsTest.java b/src/test/java/io/qdrant/client/PointsTest.java index efe460de..3828854a 100644 --- a/src/test/java/io/qdrant/client/PointsTest.java +++ b/src/test/java/io/qdrant/client/PointsTest.java @@ -25,10 +25,10 @@ import io.qdrant.client.grpc.Collections.PayloadSchemaType; import io.qdrant.client.grpc.Collections.VectorParams; import io.qdrant.client.grpc.Collections.VectorsConfig; +import io.qdrant.client.grpc.Common.Filter; import io.qdrant.client.grpc.Points; import io.qdrant.client.grpc.Points.BatchResult; import io.qdrant.client.grpc.Points.DiscoverPoints; -import io.qdrant.client.grpc.Points.Filter; import io.qdrant.client.grpc.Points.Fusion; import io.qdrant.client.grpc.Points.PointGroup; import io.qdrant.client.grpc.Points.PointStruct;