Skip to content

AsyncClient + AsyncCollection — Phase 1 (universal wrapper)#19

Merged
thorwhalen merged 1 commit into
masterfrom
claude/async-protocols
May 27, 2026
Merged

AsyncClient + AsyncCollection — Phase 1 (universal wrapper)#19
thorwhalen merged 1 commit into
masterfrom
claude/async-protocols

Conversation

@thorwhalen
Copy link
Copy Markdown
Member

AsyncClient + AsyncCollection — Phase 1 (universal wrapper)

Closes #18. Refs #11 (fourth deferred item).

What ships

  • vd.connect_async(backend, ...) — async sibling of vd.connect. Returns an AsyncClient for every backend.
  • AsyncClient / AsyncCollection runtime-checkable protocols (vd.base).
  • Universal AsyncClientWrapper / AsyncCollectionWrapper (vd.asynchronous) that adapt any sync vd Client/Collection by dispatching every method through asyncio.to_thread.
  • vd.hybrid_search_async(collection, ...) — async sibling of vd.hybrid_search.
  • SupportsNativeAsync marker protocol with a native_async: bool flag — present on both the wrapper (False) and Phase 2 native adapters (True), so consumers in event-loop apps can prefer real non-blocking I/O without losing universal coverage.

Architecture

Mirrors the wrapper-default + native opt-in pattern from PR #16's SupportsHybrid + RRF fallback: one universal wrapper means every backend supports async day one; native adapters override the wrapper in follow-up PRs (Phase 2).

The AsyncCollection surface uses explicit get / set / delete / keys / count methods rather than dunders. There's no stdlib AsyncMutableMapping; this matches the Motor / aiopg convention.

Verification

  • 12 new async tests on the memory backend cover the entry point, protocol satisfaction, async context manager, CRUD on AsyncCollection, search with filter + egress, upsert / add_documents, escape hatches (.sync, .native), and hybrid_search_async happy + error paths.
  • Full suite: 262 passed, 189 skipped (server backends skipped — no docker harness needed for Phase 1). No regressions.

Phase 2 (follow-up issues — to be opened)

Per-backend native async implementations for the 10 backends shipping async SDKs:
chroma (AsyncClientAPI), qdrant (AsyncQdrantClient), weaviate (WeaviateAsyncClient), elasticsearch (AsyncElasticsearch), redis (redis.asyncio), mongodb (AsyncMongoClient), lancedb (AsyncConnection), milvus (AsyncMilvusClient, 2.5+), pinecone (PineconeAsyncio), turbopuffer. Each is its own clean piece, same shape as the SupportsHybrid follow-up (#17).

Phase 1 of #18 — the fourth deferred item from #11. Adds async/await
support across every vd backend through a universal wrapper, with
opt-in native implementations to follow in per-backend PRs.

Architecture (same pattern as PR #16's SupportsHybrid + RRF fallback):
the universal AsyncClientWrapper/AsyncCollectionWrapper adapts every sync
vd Collection/Client by dispatching every method to asyncio.to_thread.
Adapters with native async SDKs override the wrapper in Phase 2.

vd/base.py
- AsyncClient / AsyncCollection runtime-checkable Protocols mirror the
  Client/Collection contracts. Stdlib has no AsyncMutableMapping; the
  AsyncCollection exposes explicit get/set/delete/keys/count (Motor /
  aiopg convention) instead of dunders.
- SupportsNativeAsync marker — present on both the wrapper (with
  native_async=False) and native adapters (with native_async=True), so
  consumers can prefer truly non-blocking adapters in high-concurrency
  HTTP servers while still treating the wrapper as a valid AsyncClient.

vd/asynchronous.py (new)
- AsyncClientWrapper / AsyncCollectionWrapper — thin wrappers over any
  sync Client/Collection; every method dispatches through asyncio.to_thread.
  AsyncCollection.keys() / search() materialize once in a worker thread
  then stream from memory (most backends' sync iter is already O(N)).
  Both wrappers expose `.sync` as a documented escape hatch.
- connect_async(backend, **kwargs) — async sibling of vd.connect(),
  returns an awaitable. Phase 2 will plug per-backend native adapters
  in here; for Phase 1 every backend uses the universal wrapper.
- hybrid_search_async(collection, query, ...) — async sibling of
  vd.hybrid_search; dispatches the whole fused call to a worker thread.
- Module name is `asynchronous` to avoid the `async` keyword.

Exports
- vd.AsyncClient, vd.AsyncCollection, vd.SupportsNativeAsync.
- vd.AsyncClientWrapper, vd.AsyncCollectionWrapper.
- vd.connect_async, vd.hybrid_search_async.

Tests
- tests/test_async.py — 12 contract tests on the memory backend (no infra
  needed). Cover the entry point + protocol satisfaction, async context
  manager, create/get/delete/list_collections, CRUD on AsyncCollection,
  search with filter + egress, upsert/add_documents, escape hatches,
  hybrid_search_async happy + error paths.
- pyproject.toml — adds pytest-asyncio>=0.23 to dev deps; configures
  `asyncio_mode = "auto"` so `async def test_*` runs without per-test
  decorators.

Verification
- 12 async tests pass.
- Full suite: 262 passed, 189 skipped (server backends require docker).
  No regressions vs the prior 250 sync tests — 12 net new.

Phase 2 (one follow-up issue per backend, to be opened): native async
adapters for chroma, qdrant, weaviate, elasticsearch, redis, mongodb,
lancedb, milvus, pinecone, turbopuffer (the 10 backends with native
async SDKs). Each will set `native_async = True` and bypass to_thread.

Refs #11, closes #18
@thorwhalen thorwhalen merged commit 3762b8c into master May 27, 2026
12 checks passed
@thorwhalen thorwhalen deleted the claude/async-protocols branch May 27, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AsyncClient + AsyncCollection — Phase 1 (universal wrapper)

1 participant