diff --git a/README.md b/README.md index 2329365..e577abd 100644 --- a/README.md +++ b/README.md @@ -34,20 +34,20 @@ To install the library, add the following lines to your build config file. io.qdrant client - 1.10.0 + 1.11.0 ``` #### SBT ```sbt -libraryDependencies += "io.qdrant" % "client" % "1.10.0" +libraryDependencies += "io.qdrant" % "client" % "1.11.0" ``` #### Gradle ```gradle -implementation 'io.qdrant:client:1.10.0' +implementation 'io.qdrant:client:1.11.0' ``` > [!NOTE] diff --git a/gradle.properties b/gradle.properties index 2abb78d..01f86b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,8 @@ # The version of qdrant to use to download protos -qdrantProtosVersion=v1.10.0 +qdrantProtosVersion=v1.11.0 # The version of qdrant docker image to run integration tests against -qdrantVersion=v1.10.0 +qdrantVersion=v1.11.0 # The version of the client to generate -packageVersion=1.10.0 - -## Extension of the default memory config for the spotless formatter plugin -org.gradle.jvmargs= -Xmx2000m "-XX:MaxMetaspaceSize=1000m" +packageVersion=1.11.0 diff --git a/src/main/java/io/qdrant/client/QdrantClient.java b/src/main/java/io/qdrant/client/QdrantClient.java index d03a8f8..e525b0c 100644 --- a/src/main/java/io/qdrant/client/QdrantClient.java +++ b/src/main/java/io/qdrant/client/QdrantClient.java @@ -67,6 +67,8 @@ import io.qdrant.client.grpc.Points.PointsUpdateOperation; import io.qdrant.client.grpc.Points.QueryBatchPoints; import io.qdrant.client.grpc.Points.QueryBatchResponse; +import io.qdrant.client.grpc.Points.QueryGroupsResponse; +import io.qdrant.client.grpc.Points.QueryPointGroups; import io.qdrant.client.grpc.Points.QueryPoints; import io.qdrant.client.grpc.Points.QueryResponse; import io.qdrant.client.grpc.Points.ReadConsistency; @@ -2771,6 +2773,37 @@ public ListenableFuture> queryBatchAsync( future, QueryBatchResponse::getResultList, MoreExecutors.directExecutor()); } + /** + * Universally query points. Covers all capabilities of search, recommend, discover, filters. + * Grouped by a payload field. + * + * @param request the query request + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture> queryGroupsAsync(QueryPointGroups request) { + return queryGroupsAsync(request, null); + } + + /** + * Universally query points. Covers all capabilities of search, recommend, discover, filters. + * Grouped by a payload field. + * + * @param request the query request + * @param timeout the timeout for the call. + * @return a new instance of {@link ListenableFuture} + */ + public ListenableFuture> queryGroupsAsync( + QueryPointGroups request, @Nullable Duration timeout) { + Preconditions.checkArgument( + !request.getCollectionName().isEmpty(), "Collection name must not be empty"); + + logger.debug("Query groups on '{}'", request.getCollectionName()); + ListenableFuture future = getPoints(timeout).queryGroups(request); + addLogFailureCallback(future, "Query groups"); + return Futures.transform( + future, response -> response.getResult().getGroupsList(), MoreExecutors.directExecutor()); + } + // region Snapshot Management /** diff --git a/src/main/java/io/qdrant/client/QueryFactory.java b/src/main/java/io/qdrant/client/QueryFactory.java index 275ab6a..a229942 100644 --- a/src/main/java/io/qdrant/client/QueryFactory.java +++ b/src/main/java/io/qdrant/client/QueryFactory.java @@ -10,6 +10,7 @@ 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.Sample; import io.qdrant.client.grpc.Points.VectorInput; import java.util.List; import java.util.UUID; @@ -172,5 +173,15 @@ public static Query nearestMultiVector(List> vectors) { return Query.newBuilder().setNearest(multiVectorInput(vectors)).build(); } + /** + * Creates a {@link Query} for sampling. + * + * @param sample An instance of {@link Sample} + * @return A new instance of {@link Query} + */ + public static Query sample(Sample sample) { + return Query.newBuilder().setSample(sample).build(); + } + // endregion } diff --git a/src/main/java/io/qdrant/client/ValueFactory.java b/src/main/java/io/qdrant/client/ValueFactory.java index a4dc88c..de16f89 100644 --- a/src/main/java/io/qdrant/client/ValueFactory.java +++ b/src/main/java/io/qdrant/client/ValueFactory.java @@ -2,8 +2,10 @@ import io.qdrant.client.grpc.JsonWithInt.ListValue; import io.qdrant.client.grpc.JsonWithInt.NullValue; +import io.qdrant.client.grpc.JsonWithInt.Struct; import io.qdrant.client.grpc.JsonWithInt.Value; import java.util.List; +import java.util.Map; /** Convenience methods for constructing {@link Value} */ public final class ValueFactory { @@ -69,4 +71,26 @@ public static Value list(List values) { .setListValue(ListValue.newBuilder().addAllValues(values).build()) .build(); } + + /** + * Creates a value from a list of values. Same as {@link #list(List)} + * + * @param values The list of values + * @return a new instance of {@link io.qdrant.client.grpc.JsonWithInt.Value} + */ + public static Value value(List values) { + return list(values); + } + + /** + * Creates a value from a map of nested values + * + * @param values The map of values + * @return a new instance of {@link io.qdrant.client.grpc.JsonWithInt.Value} + */ + public static Value value(Map values) { + return Value.newBuilder() + .setStructValue(Struct.newBuilder().putAllFields(values).build()) + .build(); + } } diff --git a/src/test/java/io/qdrant/client/PointsTest.java b/src/test/java/io/qdrant/client/PointsTest.java index 4422bd8..9549a24 100644 --- a/src/test/java/io/qdrant/client/PointsTest.java +++ b/src/test/java/io/qdrant/client/PointsTest.java @@ -6,6 +6,7 @@ import static io.qdrant.client.QueryFactory.fusion; import static io.qdrant.client.QueryFactory.nearest; import static io.qdrant.client.QueryFactory.orderBy; +import static io.qdrant.client.QueryFactory.sample; import static io.qdrant.client.TargetVectorFactory.targetVector; import static io.qdrant.client.ValueFactory.value; import static io.qdrant.client.VectorFactory.vector; @@ -38,10 +39,12 @@ import io.qdrant.client.grpc.Points.PointsUpdateOperation.ClearPayload; import io.qdrant.client.grpc.Points.PointsUpdateOperation.UpdateVectors; import io.qdrant.client.grpc.Points.PrefetchQuery; +import io.qdrant.client.grpc.Points.QueryPointGroups; import io.qdrant.client.grpc.Points.QueryPoints; import io.qdrant.client.grpc.Points.RecommendPointGroups; import io.qdrant.client.grpc.Points.RecommendPoints; import io.qdrant.client.grpc.Points.RetrievedPoint; +import io.qdrant.client.grpc.Points.Sample; import io.qdrant.client.grpc.Points.ScoredPoint; import io.qdrant.client.grpc.Points.ScrollPoints; import io.qdrant.client.grpc.Points.ScrollResponse; @@ -50,6 +53,7 @@ import io.qdrant.client.grpc.Points.UpdateResult; import io.qdrant.client.grpc.Points.UpdateStatus; import io.qdrant.client.grpc.Points.Vectors; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -596,7 +600,7 @@ public void batchPointUpdate() throws ExecutionException, InterruptedException { createAndSeedCollection(testName); List operations = - List.of( + Arrays.asList( PointsUpdateOperation.newBuilder() .setClearPayload( ClearPayload.newBuilder() @@ -757,6 +761,58 @@ public void queryWithPrefetchAndFusion() throws ExecutionException, InterruptedE assertEquals(2, points.size()); } + @Test + public void queryWithSampling() throws ExecutionException, InterruptedException { + createAndSeedCollection(testName); + + List points = + client + .queryAsync( + QueryPoints.newBuilder() + .setCollectionName(testName) + .setQuery(sample(Sample.Random)) + .setLimit(1) + .build()) + .get(); + + assertEquals(1, points.size()); + } + + @Test + public void queryGroups() throws ExecutionException, InterruptedException { + createAndSeedCollection(testName); + + client + .upsertAsync( + testName, + ImmutableList.of( + PointStruct.newBuilder() + .setId(id(10)) + .setVectors(VectorsFactory.vectors(30f, 31f)) + .putAllPayload(ImmutableMap.of("foo", value("hello"))) + .build())) + .get(); + // 3 points in total, 2 with "foo" = "hello" and 1 with "foo" = "goodbye" + + List groups = + client + .queryGroupsAsync( + QueryPointGroups.newBuilder() + .setCollectionName(testName) + .setQuery(nearest(ImmutableList.of(10.4f, 11.4f))) + .setGroupBy("foo") + .setGroupSize(2) + .setLimit(10) + .build()) + .get(); + + assertEquals(2, groups.size()); + // A group with 2 hits because of 2 points with "foo" = "hello" + assertEquals(1, groups.stream().filter(g -> g.getHitsCount() == 2).count()); + // A group with 1 hit because of 1 point with "foo" = "goodbye" + assertEquals(1, groups.stream().filter(g -> g.getHitsCount() == 1).count()); + } + private void createAndSeedCollection(String collectionName) throws ExecutionException, InterruptedException { CreateCollection request =