Get started Markdown

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.

PythonTypeScript
Installpip install skimmerdbnpm install skimmerdb
NeedsPython 3.11+Node 18+ (or any runtime with fetch)
Version0.1.00.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 key
import { 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 key

Types

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 ships py.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:

TypeScript
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; // number

In 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:

Python
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"]  # int

Namespaces

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 good
const 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 stop
await 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 stop

Errors 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.