Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
QDRANT_SERVERLESS_URL: ${{ secrets.QDRANT_SERVERLESS_URL }}
QDRANT_SERVERLESS_API_KEY: ${{ secrets.QDRANT_SERVERLESS_API_KEY }}

steps:
- name: Checkout
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ Since the API reference is published at <https://qdrant.github.io/java-client>,

5. Submit your pull request and get those approvals.

### Qdrant Serverless

[`QdrantServerlessClient`](https://github.com/qdrant/java-client/blob/master/src/main/java/io/qdrant/client/QdrantServerlessClient.java) uses a copy of the [serverless collections proto](https://github.com/qdrant/qdrant-cloud-public-api/blob/main/proto/qdrant/serverless/collections.proto) in `src/main/proto`. To update it:

```bash
./gradlew syncServerlessProto
```

Its tests run against a live serverless space when `QDRANT_SERVERLESS_URL` and `QDRANT_SERVERLESS_API_KEY` are set, and are skipped otherwise.

### Releasing a New Version

Once the new Qdrant version is live:
Expand Down
38 changes: 38 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,44 @@ tasks.register('downloadProtos') {
}
}

tasks.register('syncServerlessProto') {
def protoFile = file('src/main/proto/serverless_collections.proto')
outputs.file(protoFile)
outputs.upToDateWhen { false }

doLast {
def url = 'https://raw.githubusercontent.com/qdrant/qdrant-cloud-public-api/main/proto/qdrant/serverless/collections.proto'
String source
try (def httpClient = HttpClients.createDefault()) {
def response = httpClient.execute(new HttpGet(url))
if (response.getStatusLine().getStatusCode() != 200) {
throw new GradleException("failed to download ${url}: ${response.getStatusLine()}")
}
source = response.getEntity().getContent().getText('UTF-8')
}

// buf.validate is server-side only and would need an extra proto dependency. Only plain
// single-option lists are stripped. Anything else is left in place and fails the check.
def javaPackage = 'option java_package = "io.qdrant.client.grpc.serverless";'
source = source
.replace('import "buf/validate/validate.proto";\n', '')
.replaceAll(/\s*\[\(buf\.validate\.[^\[\]"',]*\]/, '')
.replace('package qdrant.serverless;\n', "package qdrant.serverless;\n\n${javaPackage}\n")
if (source.contains('buf.validate') || source.contains('buf/validate') || !source.contains(javaPackage)) {
throw new GradleException('unexpected serverless proto, update syncServerlessProto')
}

def header = '''\
// Source: https://github.com/qdrant/qdrant-cloud-public-api/blob/main/proto/qdrant/serverless/collections.proto
// Renamed because the protobuf registry already has the regular qdrant "collections.proto".
// buf.validate options are stripped: they are server-side only.
// Regenerate with: ./gradlew syncServerlessProto
'''
protoFile.parentFile.mkdirs()
protoFile.text = header + '\n' + source
}
}

processResources {
dependsOn downloadProtos
}
Expand Down
58 changes: 55 additions & 3 deletions src/main/java/io/qdrant/client/QdrantClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2857,8 +2857,31 @@ public ListenableFuture<Long> countAsync(
requestBuilder.setFilter(filter);
}

return countAsync(requestBuilder.build(), timeout);
}

/**
* Count the points in a collection.
*
* @param request The count request.
* @return a new instance of {@link ListenableFuture}
*/
public ListenableFuture<Long> countAsync(CountPoints request) {
return countAsync(request, null);
}

/**
* Count the points in a collection.
*
* @param request The count request.
* @param timeout The timeout for the call.
* @return a new instance of {@link ListenableFuture}
*/
public ListenableFuture<Long> countAsync(CountPoints request, @Nullable Duration timeout) {
String collectionName = request.getCollectionName();
Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty");
logger.debug("Count on '{}'", collectionName);
ListenableFuture<CountResponse> future = getPoints(timeout).count(requestBuilder.build());
ListenableFuture<CountResponse> future = getPoints(timeout).count(request);
addLogFailureCallback(future, "Count");
return Futures.transform(
future, response -> response.getResult().getCount(), MoreExecutors.directExecutor());
Expand Down Expand Up @@ -2943,9 +2966,34 @@ public ListenableFuture<List<BatchResult>> queryBatchAsync(
requestBuilder.setReadConsistency(readConsistency);
}

return queryBatchAsync(requestBuilder.build(), timeout);
}

/**
* Universally query points in batch. Covers all capabilities of search, recommend, discover,
* filters. Also enables hybrid and multi-stage queries.
*
* @param request The batch query request.
* @return a new instance of {@link ListenableFuture}
*/
public ListenableFuture<List<BatchResult>> queryBatchAsync(QueryBatchPoints request) {
return queryBatchAsync(request, null);
}

/**
* Universally query points in batch. Covers all capabilities of search, recommend, discover,
* filters. Also enables hybrid and multi-stage queries.
*
* @param request The batch query request.
* @param timeout The timeout for the call.
* @return a new instance of {@link ListenableFuture}
*/
public ListenableFuture<List<BatchResult>> queryBatchAsync(
QueryBatchPoints request, @Nullable Duration timeout) {
String collectionName = request.getCollectionName();
Preconditions.checkArgument(!collectionName.isEmpty(), "Collection name must not be empty");
logger.debug("Query batch on '{}'", collectionName);
ListenableFuture<QueryBatchResponse> future =
getPoints(timeout).queryBatch(requestBuilder.build());
ListenableFuture<QueryBatchResponse> future = getPoints(timeout).queryBatch(request);
addLogFailureCallback(future, "Query batch");
return Futures.transform(
future, QueryBatchResponse::getResultList, MoreExecutors.directExecutor());
Expand Down Expand Up @@ -3261,6 +3309,10 @@ public void close() {
}

private <V> void addLogFailureCallback(ListenableFuture<V> future, String message) {
addLogFailureCallback(logger, future, message);
}

static <V> void addLogFailureCallback(Logger logger, ListenableFuture<V> future, String message) {
Futures.addCallback(
future,
new FutureCallback<V>() {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/qdrant/client/QdrantGrpcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import io.qdrant.client.grpc.PointsGrpc.PointsFutureStub;
import io.qdrant.client.grpc.QdrantGrpc.QdrantFutureStub;
import io.qdrant.client.grpc.SnapshotsGrpc.SnapshotsFutureStub;
import io.qdrant.client.grpc.serverless.CollectionsServiceGrpc;
import io.qdrant.client.grpc.serverless.CollectionsServiceGrpc.CollectionsServiceFutureStub;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -183,6 +185,18 @@ public SnapshotsFutureStub snapshots() {
timeout != null ? Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS) : null);
}

/**
* Gets the client for Qdrant Serverless collections
*
* @return a new instance of {@link CollectionsServiceFutureStub}
*/
public CollectionsServiceFutureStub serverlessCollections() {
return CollectionsServiceGrpc.newFutureStub(interceptedChannel)
.withCallCredentials(callCredentials)
.withDeadline(
timeout != null ? Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS) : null);
}

@Override
public void close() {
if (shutdownChannelOnClose && !channel.isShutdown() && !channel.isTerminated()) {
Expand Down
Loading
Loading