kachedb 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kachedb/__init__.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ KacheDB — Python client for the KacheDB zero-copy storage engine.
3
+
4
+ Install::
5
+
6
+ pip install kachedb
7
+ pip install kachedb[torch] # for PyTorch tensor support
8
+
9
+ Quickstart::
10
+
11
+ from kachedb import KacheClient
12
+
13
+ with KacheClient(host="127.0.0.1", port=6379) as client:
14
+ client.set("user:1", "alice", ex=3600)
15
+ print(client.get("user:1")) # b"alice"
16
+
17
+ Async::
18
+
19
+ from kachedb import AsyncKacheClient
20
+
21
+ async with AsyncKacheClient() as client:
22
+ await client.set("key", "value")
23
+ print(await client.get("key"))
24
+ """
25
+
26
+ from ._version import __version__
27
+ from .async_client import AsyncKacheClient
28
+ from .client import KacheClient
29
+ from .connection import Connection
30
+ from .descriptor import (
31
+ TENSOR_DESCRIPTOR_MAGIC,
32
+ TensorBlockDescriptor,
33
+ TensorCodec,
34
+ TensorDType,
35
+ )
36
+ from .dma import KacheDBMemoryManager
37
+ from .exceptions import (
38
+ ConnectionError,
39
+ KacheDBError,
40
+ PoolExhaustedError,
41
+ ProtocolError,
42
+ ResponseError,
43
+ TimeoutError,
44
+ )
45
+ from .pipeline import AsyncPipeline, Pipeline
46
+ from .pool import AsyncConnectionPool, ConnectionPool
47
+ from .quantizer import sq8_decode, sq8_encode
48
+ from .semantic import AsyncSemanticCache, SearchResult, SemanticCache
49
+ from .sglang import KacheDBRadixAdapter, KacheDBSGLangConnector
50
+ from .tensor import attach_shm, detach_all, read_tensor, read_torch_tensor
51
+ from .vllm import KacheDBConnector, KacheDBPrefixCache
52
+
53
+ __all__ = [
54
+ "TENSOR_DESCRIPTOR_MAGIC",
55
+ "AsyncConnectionPool",
56
+ "AsyncKacheClient",
57
+ "AsyncPipeline",
58
+ "AsyncSemanticCache",
59
+ "Connection",
60
+ "ConnectionError",
61
+ "ConnectionPool",
62
+ "KacheClient",
63
+ "KacheDBConnector",
64
+ "KacheDBError",
65
+ "KacheDBMemoryManager",
66
+ "KacheDBPrefixCache",
67
+ "KacheDBRadixAdapter",
68
+ "KacheDBSGLangConnector",
69
+ "Pipeline",
70
+ "PoolExhaustedError",
71
+ "ProtocolError",
72
+ "ResponseError",
73
+ "SearchResult",
74
+ "SemanticCache",
75
+ "TensorBlockDescriptor",
76
+ "TensorCodec",
77
+ "TensorDType",
78
+ "TimeoutError",
79
+ "__version__",
80
+ "attach_shm",
81
+ "detach_all",
82
+ "read_tensor",
83
+ "read_torch_tensor",
84
+ "sq8_decode",
85
+ "sq8_encode",
86
+ ]
kachedb/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,588 @@
1
+ """
2
+ Async KacheDB client using ``asyncio``.
3
+
4
+ Provides the same high-level API as :class:`~kachedb.client.KacheClient`
5
+ but uses non-blocking ``asyncio`` streams for use in async ML inference
6
+ pipelines (vLLM, SGLang, etc.).
7
+
8
+ Usage::
9
+
10
+ import asyncio
11
+ from kachedb import AsyncKacheClient
12
+
13
+ async def main():
14
+ async with AsyncKacheClient() as client:
15
+ await client.set("user:1", "alice", ex=3600)
16
+ print(await client.get("user:1"))
17
+
18
+ asyncio.run(main())
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import contextlib
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ from .exceptions import ConnectionError
27
+ from .pipeline import AsyncPipeline
28
+ from .pool import AsyncConnectionPool
29
+ from .resp import AsyncRespReader, RespValue, encode_command
30
+
31
+ if TYPE_CHECKING:
32
+ import asyncio
33
+
34
+
35
+ class AsyncKacheClient:
36
+ """High-level async KacheDB client.
37
+
38
+ Parameters
39
+ ----------
40
+ host : str
41
+ Server hostname or IP address.
42
+ port : int
43
+ Server TCP port.
44
+ decode_responses : bool
45
+ If ``True``, decode byte responses to UTF-8 strings.
46
+ max_connections : int
47
+ Maximum async connection pool size.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ host: str = "127.0.0.1",
53
+ port: int = 6379,
54
+ *,
55
+ decode_responses: bool = False,
56
+ max_connections: int = 10,
57
+ password: str | None = None,
58
+ ssl: bool = False,
59
+ ssl_keyfile: str | None = None,
60
+ ssl_certfile: str | None = None,
61
+ ssl_ca_certs: str | None = None,
62
+ ssl_check_hostname: bool = True,
63
+ ) -> None:
64
+ self.host = host
65
+ self.port = port
66
+ self.decode_responses = decode_responses
67
+ self.password = password
68
+ self.ssl = ssl
69
+ self._pool = AsyncConnectionPool(
70
+ host=host,
71
+ port=port,
72
+ max_connections=max_connections,
73
+ decode_responses=decode_responses,
74
+ password=password,
75
+ ssl=ssl,
76
+ ssl_keyfile=ssl_keyfile,
77
+ ssl_certfile=ssl_certfile,
78
+ ssl_ca_certs=ssl_ca_certs,
79
+ ssl_check_hostname=ssl_check_hostname,
80
+ )
81
+ self._reader: asyncio.StreamReader | None = None
82
+ self._writer: asyncio.StreamWriter | None = None
83
+ self._resp_reader: AsyncRespReader | None = None
84
+
85
+ async def connect(self) -> AsyncKacheClient:
86
+ """Establish a dedicated async connection."""
87
+ self._reader, self._writer, self._resp_reader = await self._pool.get_connection()
88
+ return self
89
+
90
+ async def close(self) -> None:
91
+ """Release the connection back to the pool."""
92
+ if self._writer is not None and self._reader is not None and self._resp_reader is not None:
93
+ await self._pool.release_connection(self._reader, self._writer, self._resp_reader)
94
+ self._reader = None
95
+ self._writer = None
96
+ self._resp_reader = None
97
+
98
+ async def disconnect_all(self) -> None:
99
+ """Close all pooled connections."""
100
+ await self._pool.disconnect_all()
101
+
102
+ async def __aenter__(self) -> AsyncKacheClient:
103
+ return await self.connect()
104
+
105
+ async def __aexit__(self, *args: Any) -> None:
106
+ await self.close()
107
+
108
+ # ── Internal Helpers ──────────────────────────────────────────────────
109
+
110
+ async def _execute(self, *args: str | bytes) -> RespValue:
111
+ """Execute a single command and return the response."""
112
+ if self._writer is None or self._resp_reader is None:
113
+ raise ConnectionError("Not connected to KacheDB")
114
+
115
+ data = encode_command(list(args))
116
+ self._writer.write(data)
117
+ await self._writer.drain()
118
+
119
+ response = await self._resp_reader.read_response()
120
+
121
+ if self.decode_responses and isinstance(response, bytes):
122
+ return response.decode("utf-8")
123
+
124
+ return response
125
+
126
+ # ── Redis-Compatible Commands ─────────────────────────────────────────
127
+
128
+ async def ping(self, message: str | None = None) -> str:
129
+ """Send ``PING`` and return ``PONG`` or the echoed message."""
130
+ args: list[str | bytes] = ["PING"]
131
+ if message is not None:
132
+ args.append(message)
133
+ result = await self._execute(*args)
134
+ return str(result) if result is not None else "PONG"
135
+
136
+ async def get(self, key: str | bytes) -> bytes | str | None:
137
+ """Retrieve the value for *key*."""
138
+ return await self._execute("GET", key) # type: ignore[return-value]
139
+
140
+ async def set(
141
+ self,
142
+ key: str | bytes,
143
+ value: str | bytes,
144
+ *,
145
+ ex: int | None = None,
146
+ px: int | None = None,
147
+ ) -> bool:
148
+ """Store *value* under *key* with an optional TTL."""
149
+ args: list[str | bytes] = ["SET", key, value]
150
+ if ex is not None:
151
+ args.extend(["EX", str(ex)])
152
+ elif px is not None:
153
+ args.extend(["PX", str(px)])
154
+ result = await self._execute(*args)
155
+ return result == "OK"
156
+
157
+ async def mget(self, *keys: str | bytes) -> list[bytes | str | None]:
158
+ """Batch-retrieve values for multiple keys."""
159
+ if not keys:
160
+ return []
161
+ result = await self._execute("MGET", *keys)
162
+ return result if isinstance(result, list) else [] # type: ignore[return-value]
163
+
164
+ async def delete(self, *keys: str | bytes) -> int:
165
+ """Delete one or more keys."""
166
+ if not keys:
167
+ return 0
168
+ result = await self._execute("DEL", *keys)
169
+ return int(result) if isinstance(result, int) else 0
170
+
171
+ async def exists(self, *keys: str | bytes) -> int:
172
+ """Check existence of one or more keys."""
173
+ if not keys:
174
+ return 0
175
+ result = await self._execute("EXISTS", *keys)
176
+ return int(result) if isinstance(result, int) else 0
177
+
178
+ async def mset(self, mapping: dict[str | bytes, str | bytes]) -> bool:
179
+ """Set multiple keys to their respective values atomically."""
180
+ if not mapping:
181
+ return True
182
+ args: list[str | bytes] = ["MSET"]
183
+ for k, v in mapping.items():
184
+ args.extend([k, v])
185
+ result = await self._execute(*args)
186
+ return result == "OK"
187
+
188
+ async def incr(self, key: str | bytes, amount: int = 1) -> int:
189
+ """Increment the integer value of *key* by *amount* (default 1)."""
190
+ if amount == 1:
191
+ result = await self._execute("INCR", key)
192
+ else:
193
+ result = await self._execute("INCRBY", key, str(amount))
194
+ return int(result) if isinstance(result, int) else 0
195
+
196
+ async def incrby(self, key: str | bytes, amount: int) -> int:
197
+ """Increment the integer value of *key* by *amount*."""
198
+ return await self.incr(key, amount)
199
+
200
+ async def decr(self, key: str | bytes, amount: int = 1) -> int:
201
+ """Decrement the integer value of *key* by *amount* (default 1)."""
202
+ if amount == 1:
203
+ result = await self._execute("DECR", key)
204
+ else:
205
+ result = await self._execute("DECRBY", key, str(amount))
206
+ return int(result) if isinstance(result, int) else 0
207
+
208
+ async def decrby(self, key: str | bytes, amount: int) -> int:
209
+ """Decrement the integer value of *key* by *amount*."""
210
+ return await self.decr(key, amount)
211
+
212
+ async def append(self, key: str | bytes, value: str | bytes) -> int:
213
+ """Append *value* to *key*. Returns the new byte length of the value."""
214
+ result = await self._execute("APPEND", key, value)
215
+ return int(result) if isinstance(result, int) else 0
216
+
217
+ async def strlen(self, key: str | bytes) -> int:
218
+ """Return the byte length of the value stored at *key*."""
219
+ result = await self._execute("STRLEN", key)
220
+ return int(result) if isinstance(result, int) else 0
221
+
222
+ async def expire(self, key: str | bytes, seconds: int) -> bool:
223
+ """Set a timeout on *key* in seconds."""
224
+ result = await self._execute("EXPIRE", key, str(seconds))
225
+ return result == 1
226
+
227
+ async def pexpire(self, key: str | bytes, milliseconds: int) -> bool:
228
+ """Set a timeout on *key* in milliseconds."""
229
+ result = await self._execute("PEXPIRE", key, str(milliseconds))
230
+ return result == 1
231
+
232
+ async def expireat(self, key: str | bytes, timestamp: int) -> bool:
233
+ """Set an expiration deadline on *key* as a Unix timestamp (seconds)."""
234
+ result = await self._execute("EXPIREAT", key, str(timestamp))
235
+ return result == 1
236
+
237
+ async def pexpireat(self, key: str | bytes, timestamp_ms: int) -> bool:
238
+ """Set an expiration deadline on *key* as a Unix timestamp (milliseconds)."""
239
+ result = await self._execute("PEXPIREAT", key, str(timestamp_ms))
240
+ return result == 1
241
+
242
+ async def ttl(self, key: str | bytes) -> int:
243
+ """Return remaining time-to-live in seconds (-1 if no TTL, -2 if missing)."""
244
+ result = await self._execute("TTL", key)
245
+ return int(result) if isinstance(result, int) else -2
246
+
247
+ async def pttl(self, key: str | bytes) -> int:
248
+ """Return remaining time-to-live in milliseconds (-1 if no TTL, -2 if missing)."""
249
+ result = await self._execute("PTTL", key)
250
+ return int(result) if isinstance(result, int) else -2
251
+
252
+ async def persist(self, key: str | bytes) -> bool:
253
+ """Remove the existing timeout on *key*, persisting it indefinitely."""
254
+ result = await self._execute("PERSIST", key)
255
+ return result == 1
256
+
257
+ async def info(self, section: str | None = None) -> str:
258
+ """Return server information and runtime statistics."""
259
+ args: list[str | bytes] = ["INFO"]
260
+ if section is not None:
261
+ args.append(section)
262
+ result = await self._execute(*args)
263
+ if isinstance(result, bytes):
264
+ return result.decode("utf-8", errors="replace")
265
+ return str(result) if result is not None else ""
266
+
267
+ async def dbsize(self) -> int:
268
+ """Return the total number of keys stored in the database."""
269
+ result = await self._execute("DBSIZE")
270
+ return int(result) if isinstance(result, int) else 0
271
+
272
+ async def type(self, key: str | bytes) -> str:
273
+ """Return the string representation of the type of the value stored at *key*."""
274
+ result = await self._execute("TYPE", key)
275
+ if isinstance(result, bytes):
276
+ return result.decode("utf-8", errors="replace")
277
+ return str(result) if result is not None else "none"
278
+
279
+ async def flushdb(self) -> bool:
280
+ """Delete all keys in the current database."""
281
+ result = await self._execute("FLUSHDB")
282
+ return result in ("OK", b"OK", True)
283
+
284
+ async def flushall(self) -> bool:
285
+ """Delete all keys in all databases."""
286
+ result = await self._execute("FLUSHALL")
287
+ return result in ("OK", b"OK", True)
288
+
289
+ # ── Pipeline ──────────────────────────────────────────────────────────
290
+
291
+ def pipeline(self) -> AsyncPipeline:
292
+ """Create an async pipeline for batching commands."""
293
+ if self._writer is None or self._resp_reader is None:
294
+ raise ConnectionError("Not connected to KacheDB")
295
+ return AsyncPipeline(self._writer, self._resp_reader)
296
+
297
+ # ── Vector Search & Semantic Cache Commands ───────────────────────────
298
+
299
+ async def vadd(
300
+ self,
301
+ index: str | bytes,
302
+ item_id: str | bytes,
303
+ vector: bytes | list[float] | tuple[float, ...],
304
+ *,
305
+ payload: str | bytes | None = None,
306
+ ex: int | None = None,
307
+ ) -> bool:
308
+ """Store a vector embedding in a named vector index asynchronously."""
309
+ import struct
310
+
311
+ if isinstance(vector, (list, tuple)):
312
+ dim = len(vector)
313
+ vector_bytes = struct.pack(f"<{dim}f", *vector)
314
+ elif isinstance(vector, (bytes, bytearray)):
315
+ vector_bytes = bytes(vector)
316
+ dim = len(vector_bytes) // 4
317
+ else:
318
+ raise TypeError(f"Unsupported vector type: {type(vector)}")
319
+
320
+ args: list[str | bytes] = ["VADD", index, item_id, str(dim), vector_bytes]
321
+ if payload is not None:
322
+ args.extend(["PAYLOAD", payload])
323
+ if ex is not None:
324
+ args.extend(["EX", str(ex)])
325
+
326
+ result = await self._execute(*args)
327
+ return result == 1 or result == "OK"
328
+
329
+ async def vsearch(
330
+ self,
331
+ index: str | bytes,
332
+ query_vector: bytes | list[float] | tuple[float, ...],
333
+ *,
334
+ top_k: int = 1,
335
+ threshold: float = 0.0,
336
+ ) -> list[tuple[str | bytes, float, str | bytes | None]]:
337
+ """Search for nearest semantic vectors in a named index asynchronously."""
338
+ import struct
339
+
340
+ if isinstance(query_vector, (list, tuple)):
341
+ query_bytes = struct.pack(f"<{len(query_vector)}f", *query_vector)
342
+ elif isinstance(query_vector, (bytes, bytearray)):
343
+ query_bytes = bytes(query_vector)
344
+ else:
345
+ raise TypeError(f"Unsupported vector type: {type(query_vector)}")
346
+
347
+ args: list[str | bytes] = [
348
+ "VSEARCH",
349
+ index,
350
+ query_bytes,
351
+ "TOPK",
352
+ str(top_k),
353
+ "THRESHOLD",
354
+ str(threshold),
355
+ ]
356
+ raw_results = await self._execute(*args)
357
+ if not isinstance(raw_results, list):
358
+ return []
359
+
360
+ results: list[tuple[str | bytes, float, str | bytes | None]] = []
361
+ for item in raw_results:
362
+ if isinstance(item, list) and len(item) >= 2:
363
+ raw_id = item[0]
364
+ item_id: str | bytes = raw_id if isinstance(raw_id, (str, bytes)) else str(raw_id)
365
+ raw_score = item[1]
366
+ try:
367
+ if isinstance(raw_score, bytes):
368
+ score = float(raw_score.decode())
369
+ elif isinstance(raw_score, (int, float, str)):
370
+ score = float(raw_score)
371
+ else:
372
+ score = 0.0
373
+ except Exception:
374
+ score = 0.0
375
+ raw_payload = item[2] if len(item) > 2 else None
376
+ payload: str | bytes | None = (
377
+ raw_payload
378
+ if isinstance(raw_payload, (str, bytes)) or raw_payload is None
379
+ else str(raw_payload)
380
+ )
381
+ results.append((item_id, score, payload))
382
+ return results
383
+
384
+ async def vdel(self, index: str | bytes, item_id: str | bytes) -> bool:
385
+ """Delete a vector from a named index asynchronously."""
386
+ result = await self._execute("VDEL", index, item_id)
387
+ return result == 1
388
+
389
+ async def vadd_batch(
390
+ self,
391
+ index: str | bytes,
392
+ items: list[
393
+ tuple[str | bytes, bytes | list[float] | tuple[float, ...], str | bytes | None]
394
+ ],
395
+ *,
396
+ ex: int | None = None,
397
+ ) -> int:
398
+ """Add multiple vector items in a single batch command asynchronously."""
399
+ if not items:
400
+ return 0
401
+
402
+ import struct
403
+
404
+ args: list[str | bytes] = ["VADD_BATCH", index]
405
+ for item_id, vector, payload in items:
406
+ if isinstance(vector, (list, tuple)):
407
+ dim = len(vector)
408
+ vector_bytes = struct.pack(f"<{dim}f", *vector)
409
+ elif isinstance(vector, (bytes, bytearray)):
410
+ vector_bytes = bytes(vector)
411
+ else:
412
+ raise TypeError(f"Unsupported vector type: {type(vector)}")
413
+
414
+ p_val = payload if payload is not None else "-"
415
+ args.extend([item_id, vector_bytes, p_val])
416
+ if ex is not None:
417
+ args.extend(["EX", str(ex)])
418
+
419
+ result = await self._execute(*args)
420
+ return int(result) if isinstance(result, int) else 0
421
+
422
+ async def vsearch_batch(
423
+ self,
424
+ index: str | bytes,
425
+ query_vectors: list[bytes | list[float] | tuple[float, ...]],
426
+ *,
427
+ top_k: int = 1,
428
+ threshold: float = 0.0,
429
+ ) -> list[list[tuple[str | bytes, float, str | bytes | None]]]:
430
+ """Search multiple query vectors in a single batch command asynchronously."""
431
+ if not query_vectors:
432
+ return []
433
+
434
+ import struct
435
+
436
+ args: list[str | bytes] = ["VSEARCH_BATCH", index]
437
+ for q in query_vectors:
438
+ if isinstance(q, (list, tuple)):
439
+ q_bytes = struct.pack(f"<{len(q)}f", *q)
440
+ elif isinstance(q, (bytes, bytearray)):
441
+ q_bytes = bytes(q)
442
+ else:
443
+ raise TypeError(f"Unsupported vector type: {type(q)}")
444
+ args.append(q_bytes)
445
+
446
+ args.extend(["TOPK", str(top_k), "THRESHOLD", str(threshold)])
447
+ raw_batch = await self._execute(*args)
448
+ if not isinstance(raw_batch, list):
449
+ return []
450
+
451
+ all_results: list[list[tuple[str | bytes, float, str | bytes | None]]] = []
452
+ for raw_results in raw_batch:
453
+ if not isinstance(raw_results, list):
454
+ all_results.append([])
455
+ continue
456
+ query_res: list[tuple[str | bytes, float, str | bytes | None]] = []
457
+ for item in raw_results:
458
+ if isinstance(item, list) and len(item) >= 2:
459
+ raw_id = item[0]
460
+ item_id = raw_id if isinstance(raw_id, (str, bytes)) else str(raw_id)
461
+ raw_score = item[1]
462
+ try:
463
+ score = float(
464
+ raw_score.decode()
465
+ if isinstance(raw_score, bytes)
466
+ else float(str(raw_score))
467
+ )
468
+ except Exception:
469
+ score = 0.0
470
+ raw_payload = item[2] if len(item) > 2 else None
471
+ payload = (
472
+ raw_payload
473
+ if isinstance(raw_payload, (str, bytes)) or raw_payload is None
474
+ else str(raw_payload)
475
+ )
476
+ query_res.append((item_id, score, payload))
477
+ all_results.append(query_res)
478
+ return all_results
479
+
480
+ async def vstats(self, index: str | bytes) -> dict[str, Any] | None:
481
+ """Get statistics for a named vector index asynchronously."""
482
+ raw = await self._execute("VSTATS", index)
483
+ if not isinstance(raw, list):
484
+ return None
485
+ stats: dict[str, Any] = {}
486
+ for i in range(0, len(raw) - 1, 2):
487
+ raw_k = raw[i]
488
+ k = raw_k.decode() if isinstance(raw_k, bytes) else str(raw_k)
489
+ v = raw[i + 1]
490
+ stats[k] = v
491
+ return stats
492
+
493
+ async def auth(self, password: str, username: str | None = None) -> bool:
494
+ """Authenticate with the KacheDB server asynchronously.
495
+
496
+ Parameters
497
+ ----------
498
+ password : str
499
+ The authentication password.
500
+ username : str | None
501
+ Optional ACL username.
502
+ """
503
+ args: list[str | bytes] = ["AUTH"]
504
+ if username is not None:
505
+ args.append(username)
506
+ args.append(password)
507
+ result = await self._execute(*args)
508
+ return result == b"OK" or result == "OK"
509
+
510
+ async def bgrewriteaof(self) -> str:
511
+ """Rewrite the append-only file in the background asynchronously."""
512
+ result = await self._execute("BGREWRITEAOF")
513
+ if isinstance(result, bytes):
514
+ return result.decode("utf-8", errors="replace")
515
+ return str(result)
516
+
517
+ async def vindex_create(
518
+ self,
519
+ name: str | bytes,
520
+ dim: int,
521
+ m: int = 16,
522
+ ef_construction: int = 200,
523
+ ef_search: int = 50,
524
+ metric: str = "COSINE",
525
+ quantization: str = "SQ8",
526
+ ) -> bool:
527
+ """Create a new HNSW vector index asynchronously.
528
+
529
+ Parameters
530
+ ----------
531
+ name : str | bytes
532
+ Index name.
533
+ dim : int
534
+ Vector dimension.
535
+ m : int
536
+ Max bidirectional links per node (default: 16).
537
+ ef_construction : int
538
+ Size of dynamic candidate list during build (default: 200).
539
+ ef_search : int
540
+ Size of dynamic candidate list during search (default: 50).
541
+ metric : str
542
+ Distance metric ("COSINE", "L2", "IP").
543
+ quantization : str
544
+ Quantization type ("SQ8" or "NONE").
545
+ """
546
+ args: list[str | bytes] = [
547
+ "VINDEX",
548
+ "CREATE",
549
+ name,
550
+ "DIM",
551
+ str(dim),
552
+ "M",
553
+ str(m),
554
+ "EF_CONSTRUCTION",
555
+ str(ef_construction),
556
+ "EF_SEARCH",
557
+ str(ef_search),
558
+ "METRIC",
559
+ metric,
560
+ "QUANTIZATION",
561
+ quantization,
562
+ ]
563
+ result = await self._execute(*args)
564
+ return result == b"OK" or result == "OK"
565
+
566
+ async def vindex_drop(self, name: str | bytes) -> bool:
567
+ """Drop a vector index asynchronously.
568
+
569
+ Returns ``True`` if the index was found and deleted, ``False`` otherwise.
570
+ """
571
+ result = await self._execute("VINDEX", "DROP", name)
572
+ return bool(result == 1)
573
+
574
+ async def vindex_info(self, name: str | bytes) -> dict[str, Any] | None:
575
+ """Get detailed metadata and statistics for a vector index asynchronously."""
576
+ raw = await self._execute("VINDEX", "INFO", name)
577
+ if not isinstance(raw, list):
578
+ return None
579
+ info: dict[str, Any] = {}
580
+ for i in range(0, len(raw) - 1, 2):
581
+ raw_k = raw[i]
582
+ k = raw_k.decode() if isinstance(raw_k, bytes) else str(raw_k)
583
+ v = raw[i + 1]
584
+ if isinstance(v, bytes):
585
+ with contextlib.suppress(UnicodeDecodeError):
586
+ v = v.decode("utf-8")
587
+ info[k] = v
588
+ return info