Add a draft of async API tutorial

This commit is contained in:
Kacper Łukawski
2023-08-29 13:04:56 +02:00
parent f10f5e8bef
commit 1874701236
@@ -0,0 +1,362 @@
---
title: Asynchronous API
weight: 18
---
# Using Qdrant asynchronously
Databases are often launched as separate services and are accessed via a network. All the interactions with them are IO-bound and can
be performed asynchronously so as not to waste time actively waiting for a server response. If you use Python, that is achieved by
using `async/await` syntax. That lets the interpreter switch to another task while waiting for a response from the server.
Qdrant exposes two interfaces: HTTP and gRPC. The official SDKs for different languages are based on the autogenerated clients, and
in Python [qdrant-client](https://github.com/qdrant/qdrant-client), you can call all the methods asynchronously. This tutorial presents
how to do it.
## When the usage of async API is justified?
If the application you are writing will never support multiple users at once, for example, it is a script run once daily, then there
is no need to use async API. But if you are writing a web service that multiple users will use simultaneously, you shouldn't be
blocking the threads of the web server as it limits the number of concurrent requests it can handle. In this case, you should use
the async API.
Modern web frameworks like [FastAPI](https://fastapi.tiangolo.com/) and [Quart](https://quart.palletsprojects.com/en/latest/) support
async API out of the box. Asynchronous code cannot be incorporated into an existing synchronous codebase, so it's better to choose
the proper tool from the start.
## How to use async API?
As usual, calling any method of Qdrant requires establishing a connection to the server. We need to create an instance of `QdrantClient`
that will act as a gateway. If you want to do it locally, please make sure Qdrant server is running. If it's not, then you can launch it
in a Docker container:
```bash
docker run -p "6333:6333" -p "6334:6334" qdrant/qdrant:v1.4.0
```
Alternatively, you can also use a [Qdrant Cloud](https://cloud.qdrant.io/) cluster. In this case, you need to pass the API key to the
client constructor. No matter which way you choose, the `QdrantClient` instance has to be created:
```python
from qdrant_client import QdrantClient
client = QdrantClient("localhost")
# If you use Qdrant Cloud, then pass the API key to the constructor:
# client = QdrantClient("https://your-cluster-url.cloud.com", api_key="your-api-key")
```
The top-level client exposes just the synchronous methods. We can access the autogenerated async clients and call the underlying methods.
It is possible to use asynchronous HTTP API by using `qdrant_client.http.api_client.AsyncApiClient`. However, the gRPC API is much more
efficient and is the recommended way of using Qdrant asynchronously.
### gRPC API
Every instance of `QdrantClient` has two properties we are going to use: `async_grpc_collections` and `async_grpc_points`. They expose
the autogenerated clients for collections and points respectively.
The gRPC client uses type definitions for all the interactions with the server. They are autogenerated from the source code of Qdrant
server, and we have to import them if we want to call any of the available methods.
```python
from qdrant_client import grpc
```
If your IDE does not support autocompletion for the autogenerated clients, you can refer to the documentation and [see the available
fields for each of them](https://github.com/qdrant/qdrant/blob/master/docs/grpc/docs.md).
#### Creating a collection
Let's create a collection and add some points to it.
```python
response = await client.async_grpc_collections.Create(
grpc.CreateCollection(
collection_name="my_collection",
vectors_config=grpc.VectorsConfig(
params=grpc.VectorParams(
size=100,
distance=grpc.Distance.Cosine,
)
),
quantization_config=grpc.QuantizationConfig(
scalar=grpc.ScalarQuantization(
type=grpc.QuantizationType.Int8,
always_ram=False,
)
),
timeout=10,
)
)
```
Our collection was set up for 100-dimensional vectors, cosine distance and 8-bit quantization. The timeout is set to 10 seconds, so
if the server does not respond in this time, the request will be aborted.
#### Checking collection info
We can check the collection info to see if it was created successfully, and configured as we expected:
```python
response = await client.async_grpc_collections.Get(
grpc.GetCollectionInfoRequest(
collection_name="my_collection"
)
)
```
The `response` object is going to be an instance of `grpc.GetCollectionInfoResponse` and we can access the fields we need or simply
display its content.
```python
print(response)
```
```text
result {
status: Green
optimizer_status {
ok: true
}
segments_count: 8
config {
params {
shard_number: 1
on_disk_payload: true
vectors_config {
params {
size: 100
distance: Cosine
}
}
replication_factor: 1
write_consistency_factor: 1
}
hnsw_config {
m: 16
ef_construct: 100
full_scan_threshold: 10000
max_indexing_threads: 0
on_disk: false
}
optimizer_config {
deleted_threshold: 0.2
vacuum_min_vector_number: 1000
default_segment_number: 0
indexing_threshold: 20000
flush_interval_sec: 5
max_optimization_threads: 1
}
wal_config {
wal_capacity_mb: 32
wal_segments_ahead: 0
}
quantization_config {
scalar {
type: Int8
always_ram: false
}
}
}
indexed_vectors_count: 0
}
time: 1.3022e-05
```
As you can see, the collection was created properly and is ready to be used.
##### Handling exceptions
If the collection does not exist, the server will return an error. We can handle it by catching the exception. We may expect
some specific exceptions, or simply catch all the errors that subclass `RpcError` from `grpc` package:
```python
from grpc import RpcError
try:
response = await client.async_grpc_collections.Get(
grpc.GetCollectionInfoRequest(
collection_name="my_collection_2"
)
)
except RpcError as e:
print(e)
```
Our exception contain the details about the error, including the status code and the message:
```text
<AioRpcError of RPC that terminated with:
status = StatusCode.NOT_FOUND
details = "Not found: Collection `my_collection_2` doesn't exist!"
debug_error_string = "UNKNOWN:Error received from peer {created_time:"2023-08-29T12:14:05.885634788+02:00", grpc_status:5, grpc_message:"Not found: Collection `my_collection_2` doesn\'t exist!"}"
>
```
We can use those details to properly handle some specific cases. For example, if we want to create a collection only if it does not
exist, we can check the status code and create it if it is `StatusCode.NOT_FOUND`:
#### Updating collection configuration
It's quite likely that we will want to update the collection configuration. For example, we may want to change the HNSW parameters to improve
the search precision. Let's change the `ef_construct` parameter to 200:
```python
await client.async_grpc_collections.Update(
grpc.UpdateCollection(
collection_name="my_collection",
hnsw_config=grpc.HnswConfigDiff(
ef_construct=200,
),
)
)
```
#### Deleting a collection
There is plenty of other methods available for collections, like creating aliases or checking the status of the cluster. We are not going
to call all of them in this tutorial, but you can always [check the list of available methods and their parameters in the
documentation](https://github.com/qdrant/qdrant/blob/master/docs/grpc/docs.md#collections_serviceproto).
Right now, we are just going to delete the collection we created:
```python
await client.async_grpc_collections.Delete(
grpc.DeleteCollection(
collection_name="my_collection"
)
)
```
That's it when it comes to collection management. Let's move on to points, as semantic search requires us to have some vectors in the
collection.
#### Adding points to the collection
Before we add any points, please make sure the collection is created, as we did in one of the previous steps. If so, we can add some
points to it. Let's add our first two points into the collection:
```python
import numpy as np
points = [
grpc.PointStruct(
id=grpc.PointId(num=1),
payload={
"int_param": grpc.Value(integer_value=32),
"bool_param": grpc.Value(bool_value=True),
},
vectors=grpc.Vectors(
vector=grpc.Vector(data=np.random.random(100).tolist()),
),
),
grpc.PointStruct(
id=grpc.PointId(num=2),
payload={
"int_param": grpc.Value(integer_value=64),
"bool_param": grpc.Value(bool_value=False),
},
vectors=grpc.Vectors(
vector=grpc.Vector(data=np.random.random(100).tolist()),
),
),
]
response = await client.async_grpc_points.Upsert(
grpc.UpsertPoints(
collection_name="my_collection",
points=points,
)
)
```
Those points are going to be indexed and available for search. We can add more points, or update the existing ones. All the point related
operations are [documented in the Qdrant server repository](https://github.com/qdrant/qdrant/blob/master/docs/grpc/docs.md#points_serviceproto).
The main operation we need from the vector database is semantic search. Let's see how to perform it.
#### Asynchronous semantic search
Search operation requires a query vector and a collection name. We can also specify the number of results we want to retrieve and some other
parameters, but let's take one step at a time. The minimal example may look like this:
```python
response = await client.async_grpc_points.Search(
grpc.SearchPoints(
collection_name="my_collection",
vector=np.random.rand(100).tolist(),
limit=1,
)
)
print(response)
```
The response is going to contain just the point id and similarity score. For example, that might be the output of the code above:
```text
result {
id {
num: 1
}
score: 0.786471784
version: 1
}
time: 0.000456134
```
##### Returning vectors and payloads
If you need the vectors and payloads of the points, you can specify it in the request:
```python
response = await client.async_grpc_points.Search(
grpc.SearchPoints(
collection_name="my_collection",
vector=np.random.rand(100).tolist(),
limit=1,
with_payload=grpc.WithPayloadSelector(enable=True),
with_vectors=grpc.WithVectorsSelector(enable=True),
)
)
```
Right now, our response is going to have both vector and payload.
##### Applying filters
If you ever used SQL, you are probably familiar with the `WHERE` clause. It allows you to filter the results of the query. Qdrant has a similar
functionality. Let's say we want to find all the points similar to given vector, but with `int_param` equal to 32. We can do it like this:
```python
response = await client.async_grpc_points.Search(
grpc.SearchPoints(
collection_name="my_collection",
vector=np.random.rand(100).tolist(),
limit=1,
with_payload=grpc.WithPayloadSelector(enable=True),
filter=grpc.Filter(
must=[
grpc.Condition(
field=grpc.FieldCondition(
key="int_param",
match=grpc.Match(integer=32)
)
)
]
)
)
)
```
We can build even more sophisticated filters with the `must`, `should` and `must_not` clauses. You can find more details [in the
documentation](https://qdrant.tech/documentation/concepts/filtering/#filtering).
Analogous to search, we can also [scroll our collection](https://qdrant.tech/documentation/concepts/points/#scroll-points) to retrieve all the
points or use the [recommendation API](https://qdrant.tech/documentation/concepts/search/#recommendation-api) to find points similar to positive
and dissimilar to negative examples.
## Support of Qdrant async API in Python libraries
There is plenty of Python libraries that Qdrant integrates with. Up till now, only [Langchain]() provides async API support.
Qdrant is the only vector database with full coverage of async API in Langchain. The documentation [describes how to use
it](https://python.langchain.com/docs/modules/data_connection/vectorstores/#asynchronous-operations).