Indexing
To index documents, you create an index and write documents into it. A document is searchable as soon as its write is acknowledged. The examples on this page build acme/support, an index of support tickets, and acme/monorepo, an index that mirrors a git repository.
Create an index.
Section titled “Create an index.”The description is Markdown, and agents read it to decide whether an index is the one they need, so say what the index holds and how its keys are laid out. Concepts gives the rules for names, descriptions and tags.
import osimport grepvinefrom grepvine.models import CreateIndexRequest
client = grepvine.Client(api_key=os.environ["GREPVINE_API_KEY"])
client.indexes.create("acme", body=CreateIndexRequest( name="support", description="Support tickets, one Markdown file per ticket, keyed `tickets/{yyyy}/{mm}/{id}.md`.",))client.indexes.wait_until_ready("acme", "support")import { GrepvineClient } from "@grepvine/sdk";
const client = new GrepvineClient({ apiKey: process.env.GREPVINE_API_KEY! });
await client.indexes.create({ organization: "acme", name: "support", description: "Support tickets, one Markdown file per ticket, keyed `tickets/{yyyy}/{mm}/{id}.md`.",});await client.indexes.waitUntilReady({ organization: "acme", index: "support" });gv index create acme/support \ --description 'Support tickets, one Markdown file per ticket, keyed `tickets/{yyyy}/{mm}/{id}.md`.'curl -X POST "$API/organizations/acme/indexes" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"name": "support", "description": "Support tickets, one Markdown file per ticket."}'The curl examples on this page assume API=https://api.grepvine.ai/v1 and AUTH="Authorization: Bearer $GREPVINE_API_KEY".
Write documents in batches.
Section titled “Write documents in batches.”Batches are the fast way to write. put_many, putMany and gv upload split any number of documents into requests of up to 2,048 documents and 16 MiB of text each, and return one result per document in input order. A result has a revision when the document was stored, or an error when it wasn’t.
from datetime import datetime, UTC
tickets = [ { "key": "tickets/2026/10/T-48213.md", "content": "# Checkout returns 502\n\nupstream timeout after 30000ms on POST /v2/orders\n", "metadata": { "product": "checkout", "priority": 1, "status": "open", "opened_at": datetime(2026, 10, 14, 9, 12, tzinfo=UTC), }, }, # ...]
results = client.documents.put_many( "acme", "support", tickets, idempotency_key="support-import-2026-10-14")for result in results: if result.error: print(f"{result.key}: {result.error.code}")const tickets = [ { key: "tickets/2026/10/T-48213.md", content: "# Checkout returns 502\n\nupstream timeout after 30000ms on POST /v2/orders\n", metadata: { product: "checkout", priority: 1, status: "open", opened_at: new Date("2026-10-14T09:12:00Z"), }, }, // ...];
const results = await client.documents.putMany({ organization: "acme", index: "support", documents: tickets, idempotencyKey: "support-import-2026-10-14",});for (const result of results) { if (result.error) console.log(`${result.key}: ${result.error.code}`);}# tickets.ndjson holds one {"key", "content", "metadata"} object per line,# with metadata in the API's typed form. Gzipped files work too.gv upload tickets.ndjson --records --index acme/support --jsoncurl -X POST "$API/organizations/acme/indexes/support/objects:batch" -H "$AUTH" \ -H "Content-Type: application/x-ndjson" -H "Idempotency-Key: support-import-2026-10-14" \ --data-binary @- <<'EOF'{"key":"tickets/2026/10/T-48213.md","content":"# Checkout returns 502\n\nupstream timeout after 30000ms on POST /v2/orders\n","metadata":{"product":{"type":"string","value":"checkout"},"priority":{"type":"integer","value":1},"status":{"type":"string","value":"open"},"opened_at":{"type":"timestamp","value":"2026-10-14T09:12:00Z"}}}EOFWriting a key that already exists replaces that document. If one request of a multi-request call fails, the SDKs raise PartialBatchError with the results so far, the keys of the failed request and the keys never sent. To finish the call safely, repeat it with the same idempotency_key and the same documents. No document is written twice.
To upload files, point gv upload at a file, a directory or an S3 prefix. Each key is the file’s path relative to the source, after an optional key prefix:
gv upload --recursive ./runbooks --index acme/support runbooks/gv upload --recursive s3://acme-exports/tickets/ --index acme/supportgv upload exports.zip --zip --index acme/supportDirectory uploads skip what .gitignore excludes. gv upload never deletes documents.
Write one document.
Section titled “Write one document.”A batch carries text only, so a binary document or text over 16 MiB needs a single-document write, of up to 1 GiB. Which content is searchable is set out in concepts. The single-document calls take metadata as a JSON string in the API’s typed form.
import jsonfrom grepvine.models import File, PutDocumentForm
with open("exports/2026-10.csv", "rb") as payload: client.documents.put("acme", "support", body=PutDocumentForm( key="exports/2026-10.csv", content=File(payload=payload, file_name="2026-10.csv"), metadata=json.dumps({"source": {"type": "string", "value": "export"}}), ))import { openAsBlob } from "node:fs";
// putMany sends a Blob, or text over 16 MiB, as a single-document write.await client.documents.putMany({ organization: "acme", index: "support", documents: [ { key: "exports/2026-10.csv", content: await openAsBlob("exports/2026-10.csv"), metadata: { source: "export" }, }, ],});gv put acme/support exports/2026-10.csv exports/2026-10.csv --metadata source=exportcurl -X PUT "$API/organizations/acme/indexes/support/objects" -H "$AUTH" \ -H "Idempotency-Key: $(uuidgen)" \ -F key=exports/2026-10.csv -F content=@exports/2026-10.csv \ -F 'metadata={"source":{"type":"string","value":"export"}}'Attach metadata.
Section titled “Attach metadata.”Metadata is what filters select on. Every value carries one of five types, and each client maps your values to them:
| Type | Python | TypeScript | gv put --metadata |
|---|---|---|---|
string |
str |
string |
team=payments |
integer |
int |
a whole number |
pages:integer=3 |
number |
float |
a number with a fraction |
score:number=0.91 |
boolean |
bool |
boolean |
generated:boolean=false |
timestamp |
datetime, UTC when naive |
Date |
opened_at:timestamp=2026-10-14T09:12:00Z |
In TypeScript, 4 becomes an integer and 4.5 a number. To store a whole value as a number, pass it tagged: { type: "number", value: 4 }. gv put --metadata-file reads a JSON object in the same tagged form.
Some clients add fields for you. Your own fields of the same name win.
| Field | Type | Value | Added by |
|---|---|---|---|
path |
string |
The key. | put_many, putMany, gv upload, gv sync |
size |
integer |
Content length in bytes. | put_many, putMany, gv upload, gv sync |
mtime |
timestamp |
The file’s modification time. | gv upload, gv sync |
gv upload --records, gv put, Python’s documents.put and the REST API store only the fields you send.
Write conditionally.
Section titled “Write conditionally.”A conditional write changes a document only if it is in the state you expect. Use if_none_match to create a key only if it doesn’t exist yet, and if_match with a revision to replace a document only if nobody changed it since you read it. In a batch, a document whose condition fails gets the error precondition_failed and the others are stored.
[created] = client.documents.put_many("acme", "support", [ {"key": "tickets/2026/10/T-48214.md", "content": draft, "if_none_match": True},])client.documents.put_many("acme", "support", [ {"key": created.key, "content": edited, "if_match": created.revision},])const where = { organization: "acme", index: "support" };const [created] = await client.documents.putMany({ ...where, documents: [{ key: "tickets/2026/10/T-48214.md", content: draft, ifNoneMatch: true }],});await client.documents.putMany({ ...where, documents: [{ key: created.key, content: edited, ifMatch: created.revision }],});gv put acme/support tickets/2026/10/T-48214.md draft.md --if-absentgv put acme/support tickets/2026/10/T-48214.md edited.md --if-match "$REVISION"Guarantees gives the exact ordering rules.
Delete documents.
Section titled “Delete documents.”A batch delete takes up to 2,048 keys, and each key succeeds or fails on its own.
from grepvine.models import DeleteObjectRecord, DeleteObjectsRequest
client.documents.delete("acme", "support", "tickets/2026/10/T-48213.md")client.documents.delete_batch("acme", "support", body=DeleteObjectsRequest(objects=[ DeleteObjectRecord(key="tickets/2026/09/T-47001.md"), DeleteObjectRecord(key="tickets/2026/09/T-47002.md"),]))const where = { organization: "acme", index: "support" };await client.documents.delete({ ...where, key: "tickets/2026/10/T-48213.md" });await client.documents.deleteBatch({ ...where, objects: [{ key: "tickets/2026/09/T-47001.md" }, { key: "tickets/2026/09/T-47002.md" }],});gv delete acme/support tickets/2026/09/T-47001.md tickets/2026/09/T-47002.mdcurl -X POST "$API/organizations/acme/indexes/support/objects:delete" -H "$AUTH" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"objects": [{"key": "tickets/2026/09/T-47001.md"}, {"key": "tickets/2026/09/T-47002.md"}]}'Mirror a directory with gv sync.
Section titled “Mirror a directory with gv sync.”gv sync makes an index match a directory. It uploads files that are new or changed and deletes documents whose files are gone.
gv sync . acme/monorepo --dry-run --json # one line per planned upload, delete or tag changegv sync . acme/monorepo- In a git work tree, it uploads the tracked files that changed since the commit in the index’s
git.committag, deletes the documents of files removed since then, and records the new commit and branch. Uncommitted changes to tracked files stop it before it writes anything. - Outside git, on the first sync, or when the recorded commit isn’t in the repository, it compares the whole directory with the whole index.
Check what an index holds.
Section titled “Check what an index holds.”gv index info acme/support --json # document_count, content_bytesgv list acme/support --prefix tickets/2026/ --delimiter / --json # browse keys like a directoryA listing marks each document searchable: true or searchable: false. After a large upload, gv index info reports optimization_progress_percent until background optimization is done. Search results stay complete in the meantime.