Python and TypeScript
The official clients: install, connect, write, search, count, SQL, errors and retries, and every API operation by its operationId.
Two clients, both generated from the OpenAPI document with a small hand-written layer on top. Both are MIT-licensed and open source: skimmerdb-python and skimmerdb-js.
| Python | TypeScript | |
|---|---|---|
| Install | pip install skimmerdb | npm install skimmerdb |
| Needs | Python 3.11+ | Node 18+ (or any runtime with fetch) |
| Version | 0.1.0 | 0.1.0 |
Connect
Pass a key, or set SKIMMERDB_KEY in the environment. SKIMMERDB_URL (or base_url /
baseUrl) points the client at another server, such as a local one.
from skimmerdb import SkimmerDB
db = SkimmerDB("tb_...") # or SkimmerDB() with SKIMMERDB_KEY set
# new account: db = SkimmerDB.signup("ada@example.com", "correct horse"); db.key is its key
# existing account: db = SkimmerDB.login(email, password) makes a new write keyimport { SkimmerDB } from "skimmerdb";
const db = new SkimmerDB("tb_..."); // or new SkimmerDB() with SKIMMERDB_KEY set
// new account: const db = await SkimmerDB.signup("ada@example.com", "correct horse"); db.key
// existing account: await SkimmerDB.login(email, password) makes a new write keyTypes
Both clients are fully typed, generated from the OpenAPI document: every request and every answer has a type, so your editor completes field names and your type checker catches typos.
- TypeScript: a type for every request and answer (
SearchResult,Hit,Document,WriteResult,StoredDocument,SqlResult,CountResult, ...), exported from"skimmerdb". - Python: attrs classes in
skimmerdb._api.models(SearchResult,Hit,WriteResult, ...), and the package shipspy.typed, so mypy and pyright check your calls.
import { SkimmerDB, type SearchResult, type Hit, type WriteResult } from "skimmerdb";
const db = new SkimmerDB();
const w: WriteResult = await db.namespace("support").write([{ id: "t-1", text: "Refund for ACME-1042" }]);
const r: SearchResult = await db.namespace("support").search("refund");
const first: Hit | undefined = r.hits[0];
console.log(w.written, first?.id, first?.line, r.temperature);from skimmerdb import SkimmerDB
from skimmerdb._api.models import Hit, SearchResult, WriteResult
db = SkimmerDB()
w: WriteResult = db.namespace("support").write([{"id": "t-1", "text": "Refund for ACME-1042"}])
r: SearchResult = db.namespace("support").search("refund")
first: Hit = r.hits[0]
print(w.written, first.id, first.line, r.temperature)Typed metadata
Give a namespace your metadata's type, and write() takes it while search() and get()
return meta typed as it:
type Ticket = { status: "open" | "closed"; priority: number; ts: number };
const tickets = db.namespace<Ticket>("support");
await tickets.write([{ id: "t-1", text: "Refund for ACME-1042", meta: { status: "open", priority: 3, ts: 1760572800 } }]);
const { hits } = await tickets.search("refund", { filter: { status: "open" } });
hits[0]?.meta.priority; // numberIn Python every answer is a typed model (SearchResult, Hit, StoredDocument, ...) and
meta is a plain dict. To check your metadata's shape with mypy or pyright, describe it as a
TypedDict and cast where you read it:
from typing import Literal, TypedDict, cast
class Ticket(TypedDict, total=False):
status: Literal["open", "closed"]
priority: int
ts: int
for hit in ns.search("refund", filter={"status": "open"}).hits:
meta = cast(Ticket, hit.meta.to_dict())
meta["priority"] # intNamespaces
db.namespace(name) sends nothing by itself: the first write makes the namespace if it doesn't
exist. Create one explicitly to fix its vector settings.
ns = db.namespace("support")
notes = db.create_namespace("notes", dims=768, metric="cosine", exist_ok=True)
db.namespaces(limit=100) # .namespaces, .next
ns.info() # its counters and stored bytes
ns.drop() # deletes it and every document, for goodconst ns = db.namespace("support");
const notes = await db.createNamespace("notes", { dims: 768, metric: "cosine", existOk: true });
await db.namespaces({ limit: 100 }); // .namespaces, .next
await ns.info();
await ns.drop();Writing
ns.write([{"id": "t-1", "text": "Refund requested for order ACME-1042", "meta": {"status": "open"}}],
deletes=["t-0"], wait=True) # also ns.upsert(...)
ns.update([{"id": "t-1", "meta": {"status": "closed"}}]) # meta merged; None removes a key
ns.delete(["t-1"])await ns.write([{ id: "t-1", text: "Refund requested for order ACME-1042", meta: { status: "open" } }],
{ deletes: ["t-0"], wait: true });
await ns.update([{ id: "t-1", meta: { status: "closed" } }]); // meta merged; null removes a key
await ns.delete(["t-1"]);Each call writes up to 10,000 documents. All three answer a WriteResult
(written, deleted, version, stored_bytes, created).
Reading
r = ns.search("refund", filter={"status": "open"}, limit=50)
for hit in r.hits:
print(hit.id, hit.line)
more = ns.search("refund", filter={"status": "open"}, cursor=r.next)
ns.search("E[0-9]{3}", regex=True)
ns.search(vector=embedding, limit=10, filter={"team": "api"}) # nprobe=, rerank=, full= too
ns.count("refund") # an int; ns.count_result(...) for the whole answer
ns.get("t-1") # one document whole
ns.sql("SELECT status, COUNT(*) FROM docs GROUP BY status") # .columns, .types, .rows
ns.warm(["refund", "ERR-404"])const r = await ns.search("refund", { filter: { status: "open" }, limit: 50 });
r.hits.forEach((h) => console.log(h.id, h.line));
const more = await ns.search("refund", { filter: { status: "open" }, cursor: r.next });
await ns.search("E[0-9]{3}", { regex: true });
await ns.search("", { vector: embedding, limit: 10, filter: { team: "api" } }); // nprobe, rerank, full too
await ns.count("refund"); // a number; ns.countResult(...) for the whole answer
await ns.get("t-1");
await ns.sql("SELECT status, COUNT(*) FROM docs GROUP BY status");
await ns.warm(["refund", "ERR-404"]);Keys, account and usage
agent_key = db.create_key("support agent", scope="read", namespaces=["support"]) # needs an admin key
db.me()
db.usage()const agentKey = await db.createKey("support agent", { scope: "read", namespaces: ["support"] });
await db.me();
await db.usage();create_key / createKey also take caps: rate_per_s, cap_queries, cap_writes.
Embeddings
Let SkimmerDB embed documents' text with your own provider key, or hosted (Embeddings), then search by meaning:
db.set_embedding_key("openai", "sk-proj-...") # once; needs an admin key
kb = db.create_namespace("kb", embed={"provider": "openai", "dims": 512})
kb.write([{"id": "refunds", "text": "To get your money back, open the order..."}], wait=True)
kb.search(semantic="how do I get reimbursed", limit=5)
kb.set_embedding("voyage", "voyage-3.5") # or None to stopawait db.setEmbeddingKey("openai", "sk-proj-..."); // once; needs an admin key
const kb = await db.createNamespace("kb", { embed: { provider: "openai", dims: 512 } });
await kb.write([{ id: "refunds", text: "To get your money back, open the order..." }], { wait: true });
await kb.search("", { semantic: "how do I get reimbursed", limit: 5 });
await kb.setEmbedding({ provider: "voyage", model: "voyage-3.5" }); // or null to stopErrors and retries
An error answer raises SkimmerDBError with status (HTTP), message, code (a word to act
on: not_found, invalid, rate_limited, write_busy, ...), fields (what's wrong with each
invalid field) and retry_after / retryAfter (seconds, on 429).
Both clients retry 429 and 503 answers by themselves, after Retry-After or backing off: 3
retries by default (in TypeScript, new SkimmerDB(key, { retries })). Writes are by id, so a
retried write is safe.
from skimmerdb import SkimmerDBError
try:
ns.get("missing")
except SkimmerDBError as e:
if e.code == "not_found":
...import { SkimmerDBError } from "skimmerdb";
try {
await ns.get("missing");
} catch (e) {
if (e instanceof SkimmerDBError && e.code === "not_found") { /* ... */ }
}The error codes are listed in Errors.
Every operation
The hand-written layer covers the common calls. Every operation in the API reference is also generated, named by its operationId, with typed models:
from skimmerdb._api.api.search import search
from skimmerdb._api.models import SearchRequest
search.sync(name="support", client=db.api, body=SearchRequest(q="refund"))import { search } from "skimmerdb/api";
const { data } = await search({ client: db.client, path: { name: "support" }, body: { q: "refund" } });db.api (Python) and db.client (TypeScript) are the authenticated clients to pass them.