Guides Markdown

Writing documents

Put, update and delete documents, in batches, with the durability, visibility and retry rules that apply to every write.

A document is {"id", "text", "meta", "vector"}. You write documents to a namespace by id: writing an id again replaces the whole document. One request can put and delete together, and it is atomic. This page covers every way to write, and what happens to a write after you send it.

Put and delete

POST /v1/namespaces/{name}/docs puts (inserts or replaces) documents and deletes ids in one request: 1 to 10,000 documents and deletes, 64 MB at most.

B=https://api.skimmerdb.com/v1
curl -s $B/namespaces/support/docs -H "Authorization: Bearer $KEY" -d '{
  "docs": [
    {"id": "t-3", "text": "Webhook timeouts since Tuesday", "meta": {"team": "api", "status": "open"}}
  ],
  "deletes": ["t-2"]
}'
# {"written": 1, "deleted": 1, "version": 3, "stored_bytes": 2817, "created": false}
from skimmerdb import SkimmerDB

db = SkimmerDB()                      # SKIMMERDB_KEY from the environment
ns = db.namespace("support")
r = ns.write(
    [{"id": "t-3", "text": "Webhook timeouts since Tuesday", "meta": {"team": "api", "status": "open"}}],
    deletes=["t-2"],
)
print(r.written, r.deleted)
import { SkimmerDB } from "skimmerdb";

const db = new SkimmerDB();            // SKIMMERDB_KEY from the environment
const ns = db.namespace("support");
const r = await ns.write(
  [{ id: "t-3", text: "Webhook timeouts since Tuesday", meta: { team: "api", status: "open" } }],
  { deletes: ["t-2"] },
);
console.log(r.written, r.deleted);
  • Within one request, deletes apply before puts, so deleting and putting the same id leaves the new document.
  • text may be left out when the document has a vector. meta is a JSON object of strings, numbers and booleans, up to 16 KB; see Data model for how to shape it.
  • The answer: written and deleted (counts), version (the namespace's version after the write), stored_bytes, and created (true when this write made the namespace).

Update

PATCH /v1/namespaces/{name}/docs changes existing documents without resending them whole: text is replaced if given, meta is merged key by key, and a null value removes a key. A vector is kept unless the patch gives a new one.

curl -s -X PATCH $B/namespaces/support/docs -H "Authorization: Bearer $KEY" \
  -d '{"docs": [{"id": "t-1", "meta": {"status": "closed", "priority": null}}]}'
ns.update([{"id": "t-1", "meta": {"status": "closed", "priority": None}}])
await ns.update([{ id: "t-1", meta: { status: "closed", priority: null } }]);

Updating a namespace that doesn't exist is a 404 (only puts create namespaces).

Delete

By id, with POST /v1/namespaces/{name}/delete:

curl -s $B/namespaces/support/delete -H "Authorization: Bearer $KEY" -d '{"ids": ["t-3"]}'
ns.delete(["t-3"])
await ns.delete(["t-3"]);

To delete everything, drop the namespace: DELETE /v1/namespaces/{name} (ns.drop()). Its documents are deleted from storage right after.

Read one document

GET /v1/namespaces/{name}/docs/{id} returns the whole document: id, text, meta, written_at, and the vector if it has one. Ids may hold slashes: GET .../docs/agent-1/run-42/step-3.

curl -s $B/namespaces/support/docs/t-1 -H "Authorization: Bearer $KEY"
# {"id": "t-1", "text": "...", "meta": {...}, "written_at": "..."}
doc = ns.get("t-1")
const doc = await ns.get("t-1");

Creating namespaces

The first write to a name that doesn't exist makes the namespace and answers "created": true, so a mistyped name shows. If the documents have vectors, the namespace gets vectors of the first vector's length, with the cosine metric; otherwise it's text only.

To choose the vector settings yourself (for example the euclidean metric), create it first:

curl -s $B/namespaces -H "Authorization: Bearer $KEY" \
  -d '{"name": "notes", "vector": {"dims": 768, "metric": "euclidean"}}'
notes = db.create_namespace("notes", dims=768, metric="euclidean", exist_ok=True)
const notes = await db.createNamespace("notes", { dims: 768, metric: "euclidean", existOk: true });

dims (1 to 2,048) and metric (cosine, the default, or euclidean) are fixed for the namespace's life. See Vector and hybrid search.

Durability and visibility

  • Durable when answered. A write is answered once it is stored in object storage. Writes from every tenant on a server are logged together every 250 ms, so a write takes about that long.
  • Searchable at once on the server that took it, and on every server within about a second.
  • "wait": true answers only once the write is searchable on that server. Use it when the next step reads what you just wrote (a test, an agent that writes then searches); leave it off otherwise.
  • Atomic. A request's documents and deletes become visible together or not at all.

More in Durability.

Batching

Many small requests work, since the server groups them every 250 ms anyway, but each one counts against your rate limit (100 requests a second on free, 1,000 on pro, per server). A batch of up to 10,000 documents is one request and one atomic entry.

For files of rows (CSV, NDJSON, Parquet), use import instead: up to 1,000,000 rows a request, parsed straight into columns.

Tip Append-only data (events, logs, traces written once) is the cheapest shape to store, compact and query. Updates are fine, but each one writes a new version and the old one waits for compaction.

Errors and retries

Writes are by id, so resending the same batch is always safe: it replaces what may already be there.

AnswerMeaningWhat to do
429 write_busyWrites to this namespace are arriving faster than they commit. Nothing was written.Wait Retry-After seconds (also retry_after in the body), resend the same batch.
429 rate_limitedOver the tenant's or the key's rate, concurrency or cap.Wait Retry-After seconds.
503 unavailableObject storage failed. Nothing was written.Retry.
503 write_unknownThe write may or may not have been applied.Retry it: the same ids replace whatever is there.
413 too_largeOver 10,000 documents or 64 MB.Split the batch.
422 invalidSomething in the body is wrong (fields says what), e.g. a vector of the wrong length.Fix the request.
507 quota_fullThe tenant's storage quota is full.Delete data or upgrade.

The Python and TypeScript clients retry 429 and 503 answers for you (three times, after Retry-After or backing off), then raise SkimmerDBError with status, code and the message. All codes: Errors.