Native AsyncClient / AsyncCollection for the 10 backends with async SDKs (Phase 2)
PR #19 landed Phase 1 — the universal AsyncClientWrapper / AsyncCollectionWrapper (asyncio.to_thread-based) that makes every backend usable from async code day one. This issue tracks Phase 2: replacing the wrapper with native async implementations for backends whose SDKs ship one.
Native implementations set native_async = True on the class and bypass asyncio.to_thread, doing real non-blocking I/O. Consumers in high-concurrency event-loop apps (FastAPI, Starlette) should prefer them.
The 10 candidates (probed in vd-hybrid-venv)
| Backend |
Async SDK |
Approx work |
| chroma |
chromadb.AsyncClientAPI (chromadb.AsyncHttpClient) |
Wrap the client into NativeAsyncChromaClient + NativeAsyncChromaCollection; reuse most of the sync adapter's data model. |
| qdrant |
qdrant_client.AsyncQdrantClient |
Async sibling of the existing qdrant adapter; same call shapes, all coroutines. |
| weaviate |
weaviate.WeaviateAsyncClient (weaviate.use_async_with_local) |
Async variant of connect_to_local / connect_to_weaviate_cloud. |
| elasticsearch |
elasticsearch.AsyncElasticsearch |
Identical query/index calls; only the client class differs. |
| redis |
redis.asyncio.Redis + redis.asyncio.search |
Mirror of the existing RediSearch adapter; bytes handling identical. |
| mongodb |
pymongo.AsyncMongoClient (or motor.motor_asyncio) |
Same Atlas Search index logic; await every aggregate/find. |
| lancedb |
lancedb.AsyncConnection |
All native; AsyncTable.search and friends. |
| milvus |
pymilvus.AsyncMilvusClient (2.5+) |
Same hybrid_search call shape with await. |
| pinecone |
pinecone.PineconeAsyncio |
Async client; same upsert/query shape. |
| turbopuffer |
httpx-based (already async-capable) |
Internally httpx — needs AsyncClient swap. |
Implementation pattern (per backend)
- Add a
Native<Backend>AsyncClient and Native<Backend>AsyncCollection class alongside the existing sync adapter (probably in the same module).
- Set
native_async: bool = True on both classes.
- Register the async constructor so
vd.connect_async(backend, ...) returns the native class instead of the wrapper.
- Add the backend to
tests/test_async.py's parametrization (skip-if-unreachable for server backends).
Registration hook
connect_async in vd/asynchronous.py currently always wraps the sync client. Phase 2 will introduce a small registry — e.g. register_async_backend(name, async_client_class) analogous to the sync register_backend — and connect_async will dispatch through it.
Refs
Follow-up to #19, refs #11. Each backend is its own clean piece — separate PRs welcome.
Native
AsyncClient/AsyncCollectionfor the 10 backends with async SDKs (Phase 2)PR #19 landed Phase 1 — the universal
AsyncClientWrapper/AsyncCollectionWrapper(asyncio.to_thread-based) that makes every backend usable from async code day one. This issue tracks Phase 2: replacing the wrapper with native async implementations for backends whose SDKs ship one.Native implementations set
native_async = Trueon the class and bypassasyncio.to_thread, doing real non-blocking I/O. Consumers in high-concurrency event-loop apps (FastAPI, Starlette) should prefer them.The 10 candidates (probed in
vd-hybrid-venv)chromadb.AsyncClientAPI(chromadb.AsyncHttpClient)NativeAsyncChromaClient+NativeAsyncChromaCollection; reuse most of the sync adapter's data model.qdrant_client.AsyncQdrantClientweaviate.WeaviateAsyncClient(weaviate.use_async_with_local)connect_to_local/connect_to_weaviate_cloud.elasticsearch.AsyncElasticsearchredis.asyncio.Redis+redis.asyncio.searchpymongo.AsyncMongoClient(ormotor.motor_asyncio)awaitevery aggregate/find.lancedb.AsyncConnectionAsyncTable.searchand friends.pymilvus.AsyncMilvusClient(2.5+)await.pinecone.PineconeAsyncioAsyncClientswap.Implementation pattern (per backend)
Native<Backend>AsyncClientandNative<Backend>AsyncCollectionclass alongside the existing sync adapter (probably in the same module).native_async: bool = Trueon both classes.vd.connect_async(backend, ...)returns the native class instead of the wrapper.tests/test_async.py's parametrization (skip-if-unreachable for server backends).Registration hook
connect_asyncinvd/asynchronous.pycurrently always wraps the sync client. Phase 2 will introduce a small registry — e.g.register_async_backend(name, async_client_class)analogous to the syncregister_backend— andconnect_asyncwill dispatch through it.Refs
Follow-up to #19, refs #11. Each backend is its own clean piece — separate PRs welcome.