lsmvec-client 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.
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""LSM-Vec Python client.
|
|
2
|
+
|
|
3
|
+
A thin, dependency-free client for the LSM-Vec HTTP server.
|
|
4
|
+
|
|
5
|
+
from lsmvec_client import Client
|
|
6
|
+
c = Client(api_key="sk-live-...", base_url="https://api.lsmvec.com")
|
|
7
|
+
c.insert(1, [0.1, 0.2, ...])
|
|
8
|
+
hits = c.search([0.1, 0.2, ...], k=10)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import Client, SearchResult
|
|
12
|
+
from .errors import (
|
|
13
|
+
InvalidArgument,
|
|
14
|
+
LSMVecError,
|
|
15
|
+
NotFound,
|
|
16
|
+
PayloadTooLarge,
|
|
17
|
+
RateLimited,
|
|
18
|
+
ServerError,
|
|
19
|
+
Unauthorized,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Client",
|
|
26
|
+
"SearchResult",
|
|
27
|
+
"LSMVecError",
|
|
28
|
+
"InvalidArgument",
|
|
29
|
+
"Unauthorized",
|
|
30
|
+
"NotFound",
|
|
31
|
+
"PayloadTooLarge",
|
|
32
|
+
"RateLimited",
|
|
33
|
+
"ServerError",
|
|
34
|
+
]
|
lsmvec_client/client.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""LSM-Vec HTTP client.
|
|
2
|
+
|
|
3
|
+
Thin wrapper over the LSM-Vec REST API (see docs/TRIAL_LAUNCH_PLAN.md
|
|
4
|
+
§5.3). Uses only the Python standard library — no third-party
|
|
5
|
+
dependencies required. numpy is optional and only used as a
|
|
6
|
+
convenience for `bulk_build` (lists work too).
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
from lsmvec_client import Client
|
|
10
|
+
c = Client(api_key="sk-live-...", base_url="http://localhost:8000")
|
|
11
|
+
c.insert(1, [0.1, 0.2, ...], metadata={"title": "doc"})
|
|
12
|
+
hits = c.search([0.1, 0.2, ...], k=10)
|
|
13
|
+
for h in hits:
|
|
14
|
+
print(h.id, h.distance)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import struct
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
23
|
+
|
|
24
|
+
from .errors import LSMVecError, from_status
|
|
25
|
+
|
|
26
|
+
__all__ = ["Client", "SearchResult"]
|
|
27
|
+
|
|
28
|
+
Vector = Union[Sequence[float], "numpy.ndarray"] # noqa: F821
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SearchResult:
|
|
33
|
+
id: int
|
|
34
|
+
distance: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Client:
|
|
38
|
+
"""Client for a single LSM-Vec HTTP server (one trial user/container)."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str = "",
|
|
43
|
+
base_url: str = "http://localhost:8000",
|
|
44
|
+
timeout: float = 30.0,
|
|
45
|
+
):
|
|
46
|
+
self.api_key = api_key
|
|
47
|
+
self.base_url = base_url.rstrip("/")
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
|
|
50
|
+
# ---- internal request helper ----
|
|
51
|
+
|
|
52
|
+
def _request(
|
|
53
|
+
self,
|
|
54
|
+
method: str,
|
|
55
|
+
path: str,
|
|
56
|
+
*,
|
|
57
|
+
json_body: Optional[Any] = None,
|
|
58
|
+
raw_body: Optional[bytes] = None,
|
|
59
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
60
|
+
parse_json: bool = True,
|
|
61
|
+
):
|
|
62
|
+
url = self.base_url + path
|
|
63
|
+
headers = {}
|
|
64
|
+
if self.api_key:
|
|
65
|
+
headers["Authorization"] = "Bearer " + self.api_key
|
|
66
|
+
|
|
67
|
+
data = None
|
|
68
|
+
if raw_body is not None:
|
|
69
|
+
data = raw_body
|
|
70
|
+
headers["Content-Type"] = "application/octet-stream"
|
|
71
|
+
elif json_body is not None:
|
|
72
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
73
|
+
headers["Content-Type"] = "application/json"
|
|
74
|
+
if extra_headers:
|
|
75
|
+
headers.update(extra_headers)
|
|
76
|
+
|
|
77
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
80
|
+
body = resp.read()
|
|
81
|
+
if not parse_json or not body:
|
|
82
|
+
return resp.status, None
|
|
83
|
+
return resp.status, json.loads(body.decode("utf-8"))
|
|
84
|
+
except urllib.error.HTTPError as e:
|
|
85
|
+
body = e.read()
|
|
86
|
+
code = None
|
|
87
|
+
message = e.reason or "http error"
|
|
88
|
+
try:
|
|
89
|
+
parsed = json.loads(body.decode("utf-8"))
|
|
90
|
+
code = parsed.get("code")
|
|
91
|
+
message = parsed.get("error", message)
|
|
92
|
+
except Exception:
|
|
93
|
+
if body:
|
|
94
|
+
message = body.decode("utf-8", "replace")
|
|
95
|
+
raise from_status(e.code, code, message) from None
|
|
96
|
+
except urllib.error.URLError as e:
|
|
97
|
+
raise LSMVecError("connection failed: " + str(e.reason)) from None
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _to_list(vector: Vector) -> List[float]:
|
|
101
|
+
# numpy array → list, list stays list.
|
|
102
|
+
if hasattr(vector, "tolist"):
|
|
103
|
+
return vector.tolist()
|
|
104
|
+
return list(vector)
|
|
105
|
+
|
|
106
|
+
# ---- vectors ----
|
|
107
|
+
|
|
108
|
+
def insert(
|
|
109
|
+
self,
|
|
110
|
+
id: int,
|
|
111
|
+
vector: Vector,
|
|
112
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
body: Dict[str, Any] = {"id": int(id), "vector": self._to_list(vector)}
|
|
115
|
+
if metadata is not None:
|
|
116
|
+
body["metadata"] = metadata
|
|
117
|
+
self._request("POST", "/v1/vectors", json_body=body, parse_json=False)
|
|
118
|
+
|
|
119
|
+
def upsert(self, id: int, vector: Vector) -> None:
|
|
120
|
+
self._request(
|
|
121
|
+
"PUT",
|
|
122
|
+
"/v1/vectors/%d" % int(id),
|
|
123
|
+
json_body={"vector": self._to_list(vector)},
|
|
124
|
+
parse_json=False,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get(self, id: int) -> Dict[str, Any]:
|
|
128
|
+
_, body = self._request("GET", "/v1/vectors/%d" % int(id))
|
|
129
|
+
return body
|
|
130
|
+
|
|
131
|
+
def delete(self, id: int) -> None:
|
|
132
|
+
self._request("DELETE", "/v1/vectors/%d" % int(id), parse_json=False)
|
|
133
|
+
|
|
134
|
+
# ---- payload ----
|
|
135
|
+
|
|
136
|
+
def get_payload(self, id: int) -> Dict[str, Any]:
|
|
137
|
+
_, body = self._request("GET", "/v1/vectors/%d/payload" % int(id))
|
|
138
|
+
return body
|
|
139
|
+
|
|
140
|
+
def set_payload(self, id: int, payload: Dict[str, Any]) -> None:
|
|
141
|
+
self._request(
|
|
142
|
+
"PUT",
|
|
143
|
+
"/v1/vectors/%d/payload" % int(id),
|
|
144
|
+
json_body=payload,
|
|
145
|
+
parse_json=False,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def merge_payload(self, id: int, partial: Dict[str, Any]) -> None:
|
|
149
|
+
"""RFC 7396 merge-patch: keys in `partial` overwrite; null deletes."""
|
|
150
|
+
self._request(
|
|
151
|
+
"PATCH",
|
|
152
|
+
"/v1/vectors/%d/payload" % int(id),
|
|
153
|
+
json_body=partial,
|
|
154
|
+
parse_json=False,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ---- search ----
|
|
158
|
+
|
|
159
|
+
def search(
|
|
160
|
+
self,
|
|
161
|
+
vector: Vector,
|
|
162
|
+
k: int = 10,
|
|
163
|
+
ef_search: Optional[int] = None,
|
|
164
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
165
|
+
) -> List[SearchResult]:
|
|
166
|
+
body: Dict[str, Any] = {"vector": self._to_list(vector), "k": int(k)}
|
|
167
|
+
if ef_search is not None:
|
|
168
|
+
body["ef_search"] = int(ef_search)
|
|
169
|
+
if filter is not None:
|
|
170
|
+
body["filter"] = filter
|
|
171
|
+
_, parsed = self._request("POST", "/v1/search", json_body=body)
|
|
172
|
+
results = parsed.get("results", []) if parsed else []
|
|
173
|
+
return [SearchResult(id=r["id"], distance=r["distance"]) for r in results]
|
|
174
|
+
|
|
175
|
+
# ---- bulk build (initial-load only) ----
|
|
176
|
+
|
|
177
|
+
def bulk_build(self, vectors, dim: Optional[int] = None, threads: int = 0) -> Dict[str, Any]:
|
|
178
|
+
"""Build the entire index from a batch of vectors in one call.
|
|
179
|
+
|
|
180
|
+
`vectors` may be a 2-D numpy array (n, dim) or a list of
|
|
181
|
+
equal-length float lists. The DB must be empty — bulk build is
|
|
182
|
+
initial-load only.
|
|
183
|
+
|
|
184
|
+
Returns the server's timing report dict
|
|
185
|
+
{n, elapsed_ms, vectors_per_sec, threads}.
|
|
186
|
+
"""
|
|
187
|
+
# Normalize to a flat float32 byte blob + (n, dim).
|
|
188
|
+
if hasattr(vectors, "shape"): # numpy array
|
|
189
|
+
import numpy as np # local import; numpy optional otherwise
|
|
190
|
+
|
|
191
|
+
arr = np.ascontiguousarray(vectors, dtype=np.float32)
|
|
192
|
+
if arr.ndim != 2:
|
|
193
|
+
raise ValueError("vectors must be a 2-D array (n, dim)")
|
|
194
|
+
n, d = arr.shape
|
|
195
|
+
if dim is not None and dim != d:
|
|
196
|
+
raise ValueError("dim=%d does not match array dim=%d" % (dim, d))
|
|
197
|
+
blob = arr.tobytes()
|
|
198
|
+
else: # list of lists
|
|
199
|
+
rows = list(vectors)
|
|
200
|
+
n = len(rows)
|
|
201
|
+
if n == 0:
|
|
202
|
+
raise ValueError("vectors is empty")
|
|
203
|
+
d = len(rows[0])
|
|
204
|
+
if dim is not None and dim != d:
|
|
205
|
+
raise ValueError("dim=%d does not match row length=%d" % (dim, d))
|
|
206
|
+
flat = []
|
|
207
|
+
for row in rows:
|
|
208
|
+
if len(row) != d:
|
|
209
|
+
raise ValueError("all rows must have the same length")
|
|
210
|
+
flat.extend(row)
|
|
211
|
+
blob = struct.pack("<%df" % (n * d), *flat)
|
|
212
|
+
|
|
213
|
+
headers = {
|
|
214
|
+
"X-LSMVec-N": str(n),
|
|
215
|
+
"X-LSMVec-Dim": str(d),
|
|
216
|
+
}
|
|
217
|
+
if threads > 0:
|
|
218
|
+
headers["X-LSMVec-Threads"] = str(threads)
|
|
219
|
+
|
|
220
|
+
_, parsed = self._request(
|
|
221
|
+
"POST",
|
|
222
|
+
"/v1/build/bulk",
|
|
223
|
+
raw_body=blob,
|
|
224
|
+
extra_headers=headers,
|
|
225
|
+
)
|
|
226
|
+
return parsed
|
|
227
|
+
|
|
228
|
+
# ---- diagnostics ----
|
|
229
|
+
|
|
230
|
+
def stats(self) -> Dict[str, Any]:
|
|
231
|
+
_, body = self._request("GET", "/v1/stats")
|
|
232
|
+
return body
|
|
233
|
+
|
|
234
|
+
def health(self) -> bool:
|
|
235
|
+
try:
|
|
236
|
+
status, _ = self._request("GET", "/health", parse_json=False)
|
|
237
|
+
return status == 200
|
|
238
|
+
except LSMVecError:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
def ready(self) -> bool:
|
|
242
|
+
try:
|
|
243
|
+
status, _ = self._request("GET", "/ready", parse_json=False)
|
|
244
|
+
return status == 200
|
|
245
|
+
except LSMVecError:
|
|
246
|
+
return False
|
lsmvec_client/errors.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Exception hierarchy for the LSM-Vec client.
|
|
2
|
+
|
|
3
|
+
HTTP status codes map to typed exceptions so callers can catch the
|
|
4
|
+
specific failure they care about:
|
|
5
|
+
|
|
6
|
+
400 -> InvalidArgument
|
|
7
|
+
401 -> Unauthorized
|
|
8
|
+
404 -> NotFound
|
|
9
|
+
413 -> PayloadTooLarge
|
|
10
|
+
429 -> RateLimited
|
|
11
|
+
5xx -> ServerError
|
|
12
|
+
other / network -> LSMVecError (base)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LSMVecError(Exception):
|
|
17
|
+
"""Base class for all client errors."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message, *, status=None, code=None):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status = status
|
|
22
|
+
self.code = code
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvalidArgument(LSMVecError):
|
|
26
|
+
"""400 — malformed request (bad vector, wrong dim, bad JSON)."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Unauthorized(LSMVecError):
|
|
30
|
+
"""401 — missing or invalid API key."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NotFound(LSMVecError):
|
|
34
|
+
"""404 — id does not exist / index empty."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PayloadTooLarge(LSMVecError):
|
|
38
|
+
"""413 — request body exceeds the server's limit."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RateLimited(LSMVecError):
|
|
42
|
+
"""429 — too many requests; back off and retry."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ServerError(LSMVecError):
|
|
46
|
+
"""5xx — server-side failure."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def from_status(status, code, message):
|
|
50
|
+
"""Build the right exception subtype from an HTTP status."""
|
|
51
|
+
if status == 400:
|
|
52
|
+
return InvalidArgument(message, status=status, code=code)
|
|
53
|
+
if status == 401:
|
|
54
|
+
return Unauthorized(message, status=status, code=code)
|
|
55
|
+
if status == 404:
|
|
56
|
+
return NotFound(message, status=status, code=code)
|
|
57
|
+
if status == 413:
|
|
58
|
+
return PayloadTooLarge(message, status=status, code=code)
|
|
59
|
+
if status == 429:
|
|
60
|
+
return RateLimited(message, status=status, code=code)
|
|
61
|
+
if 500 <= status < 600:
|
|
62
|
+
return ServerError(message, status=status, code=code)
|
|
63
|
+
return LSMVecError(message, status=status, code=code)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lsmvec-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the LSM-Vec vector database HTTP API
|
|
5
|
+
Author: LSM-Vec
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: numpy
|
|
10
|
+
Requires-Dist: numpy>=1.20; extra == "numpy"
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# lsmvec-client — Python client for LSM-Vec
|
|
15
|
+
|
|
16
|
+
A thin, dependency-free Python client for the LSM-Vec vector database
|
|
17
|
+
HTTP API. Uses only the Python standard library; `numpy` is optional
|
|
18
|
+
(a convenience for `bulk_build`).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install lsmvec-client # core, zero dependencies
|
|
24
|
+
pip install lsmvec-client[numpy] # + numpy for bulk_build convenience
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or run straight from the repo without installing:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import sys; sys.path.insert(0, "sdk/python")
|
|
31
|
+
from lsmvec_client import Client
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from lsmvec_client import Client
|
|
38
|
+
|
|
39
|
+
client = Client(
|
|
40
|
+
api_key="sk-live-...", # sent as Bearer token
|
|
41
|
+
base_url="https://api.lsmvec.com", # or http://localhost:8000 for local
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Insert with optional metadata
|
|
45
|
+
client.insert(1, [0.10, 0.20, 0.30, ...], metadata={"title": "intro"})
|
|
46
|
+
|
|
47
|
+
# Search
|
|
48
|
+
hits = client.search([0.10, 0.20, 0.30, ...], k=10)
|
|
49
|
+
for h in hits:
|
|
50
|
+
print(h.id, h.distance)
|
|
51
|
+
|
|
52
|
+
# Filtered search (metadata predicate, same syntax as the HTTP API)
|
|
53
|
+
hits = client.search(
|
|
54
|
+
[0.10, 0.20, ...], k=10,
|
|
55
|
+
filter={"$and": [{"category": {"$eq": "docs"}}]},
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Bulk build (initial load)
|
|
60
|
+
|
|
61
|
+
The fastest way to populate a **new, empty** database. Builds the
|
|
62
|
+
whole index in memory (RNN-Descent) and writes it in one pass —
|
|
63
|
+
2-3× faster than per-vector inserts and higher recall. Initial-load
|
|
64
|
+
only; the DB must be empty.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import numpy as np
|
|
68
|
+
from lsmvec_client import Client
|
|
69
|
+
|
|
70
|
+
client = Client(base_url="http://localhost:8000")
|
|
71
|
+
|
|
72
|
+
vectors = np.random.rand(100_000, 128).astype(np.float32)
|
|
73
|
+
report = client.bulk_build(vectors, threads=4)
|
|
74
|
+
print(report) # {'n': 100000, 'elapsed_ms': ..., 'vectors_per_sec': ..., 'threads': 4}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`bulk_build` also accepts a plain list of equal-length float lists
|
|
78
|
+
(no numpy required):
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
rows = [[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8], ...]
|
|
82
|
+
client.bulk_build(rows)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For incremental updates on an already-built index, use `insert()` /
|
|
86
|
+
`upsert()` instead — `bulk_build` rejects a non-empty DB.
|
|
87
|
+
|
|
88
|
+
## API
|
|
89
|
+
|
|
90
|
+
| Method | HTTP | Notes |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `insert(id, vector, metadata=None)` | `POST /v1/vectors` | metadata is any JSON object |
|
|
93
|
+
| `upsert(id, vector)` | `PUT /v1/vectors/:id` | insert-or-replace vector |
|
|
94
|
+
| `get(id) -> dict` | `GET /v1/vectors/:id` | `{"id", "vector"}` |
|
|
95
|
+
| `delete(id)` | `DELETE /v1/vectors/:id` | |
|
|
96
|
+
| `get_payload(id) -> dict` | `GET /v1/vectors/:id/payload` | |
|
|
97
|
+
| `set_payload(id, payload)` | `PUT /v1/vectors/:id/payload` | replace |
|
|
98
|
+
| `merge_payload(id, partial)` | `PATCH /v1/vectors/:id/payload` | RFC 7396 merge |
|
|
99
|
+
| `search(vector, k=10, ef_search=None, filter=None) -> [SearchResult]` | `POST /v1/search` | |
|
|
100
|
+
| `bulk_build(vectors, dim=None, threads=0) -> dict` | `POST /v1/build/bulk` | empty DB only |
|
|
101
|
+
| `stats() -> dict` | `GET /v1/stats` | tombstone / bloom counters |
|
|
102
|
+
| `health() -> bool` | `GET /health` | |
|
|
103
|
+
| `ready() -> bool` | `GET /ready` | DB open + responsive |
|
|
104
|
+
|
|
105
|
+
`search` returns a list of `SearchResult(id: int, distance: float)`.
|
|
106
|
+
|
|
107
|
+
## Errors
|
|
108
|
+
|
|
109
|
+
HTTP status codes map to typed exceptions (all subclass `LSMVecError`):
|
|
110
|
+
|
|
111
|
+
| Status | Exception |
|
|
112
|
+
|---|---|
|
|
113
|
+
| 400 | `InvalidArgument` |
|
|
114
|
+
| 401 | `Unauthorized` |
|
|
115
|
+
| 404 | `NotFound` |
|
|
116
|
+
| 413 | `PayloadTooLarge` |
|
|
117
|
+
| 429 | `RateLimited` |
|
|
118
|
+
| 5xx | `ServerError` |
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from lsmvec_client import NotFound
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
client.get(999999)
|
|
125
|
+
except NotFound:
|
|
126
|
+
print("no such id")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Notes
|
|
130
|
+
|
|
131
|
+
- Vectors are stored with 8-bit scalar quantization (SQ8). `get()`
|
|
132
|
+
returns the dequantized vector, which differs from the input by
|
|
133
|
+
up to ~`range/255` per element. Distances and recall are computed
|
|
134
|
+
on the quantized form.
|
|
135
|
+
- `id` is a 64-bit unsigned integer.
|
|
136
|
+
- The client is synchronous and connection-per-request (stdlib
|
|
137
|
+
`urllib`). For high-throughput batch ingestion, prefer
|
|
138
|
+
`bulk_build` over a loop of `insert`.
|
|
139
|
+
|
|
140
|
+
## Testing
|
|
141
|
+
|
|
142
|
+
Against a running server:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
LSMVEC_TEST_URL=http://localhost:8000 LSMVEC_TEST_DIM=8 \
|
|
146
|
+
python3 sdk/python/tests/test_client.py
|
|
147
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
lsmvec_client/__init__.py,sha256=sDDO-QJUuiPUXwgUGXqSMwIOFwr5mpJcOnMKRHgzozM,680
|
|
2
|
+
lsmvec_client/client.py,sha256=b5YBP5zX-CiaOAWyp-xNvQeycUZRZu7AS0PATRHyJlg,8074
|
|
3
|
+
lsmvec_client/errors.py,sha256=hKCO5Ue5cLQ5_MfiMScu3oyZ0vmvva2zKrdizrsPsAw,1779
|
|
4
|
+
lsmvec_client-0.1.0.dist-info/METADATA,sha256=KA_PB7JywXJ59e-tuQ815p0s1S3CSEll9e8DFA--Hz0,4333
|
|
5
|
+
lsmvec_client-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
lsmvec_client-0.1.0.dist-info/top_level.txt,sha256=yv_1Z78oFcVaa7qWw9pidDXF5xGeD951By32Tp52Z9Q,14
|
|
7
|
+
lsmvec_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lsmvec_client
|