AACSearch / guide
Incremental indexing
Keep a collection in sync with your catalogue — upserts, partial updates, batch imports with per-row results, deletes, async ingest jobs and zero-downtime reindexing.
Indexing is a server job: every operation on this page needs a full key
(see keys and scopes). Examples use the TypeScript SDK client from
the quick start and curl with
-H "X-NOVA-API-KEY: $NOVA_API_KEY".
Look up a collection
getCollections (GET /collections) lists every collection of the
organization with its schema; getCollection
(GET /collections/{collectionName}) returns one:
const all = await client.collections.getCollections();
const products = await client.collections.getCollection({ collectionName: "products" });
// products.fields, products.num_documents, products.default_sorting_fieldWrite actions
Single-document and batch writes take an action that decides what happens
when a document with that id already exists — or doesn't:
action | Document exists | Document missing | Use it for |
|---|---|---|---|
create | error | created | a first load where a duplicate id is a bug |
upsert | replaced entirely | created | full syncs — send the whole document every time |
update | the fields you send are changed, the rest kept | error | changing a few fields of a known document |
emplace | the fields you send are changed, the rest kept | created | partial feeds (price and stock only) that may carry new items |
Put another way, the four actions differ on two axes — whether a missing document is created, and whether an existing one keeps its other fields:
Send every field, every time. The document is replaced. Correct for a full catalogue sync, and the only action that cannot silently drop a field.
Changes only the fields you send and creates the document when it is missing. Right for a price-and-stock feed that can also introduce new items.
A document's id is yours: use your catalogue's own primary key so repeated
syncs hit the same document.
dirty_values controls what happens when a field's value doesn't match the
schema (a string in a float field): coerce_or_reject, coerce_or_drop,
drop or reject.
One document
indexDocument — POST /collections/{collectionName}/documents:
curl -X POST "https://api.aacsearch.com/v1/collections/products/documents?action=upsert" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/json" \
-d '{"id":"42","name":"Cordless drill, 18V","price":84.0,"in_stock":true}'getDocument (GET /collections/{collectionName}/documents/{documentId})
reads one document back by id.
Change some fields of one document — updateDocument,
PATCH /collections/{collectionName}/documents/{documentId}:
await client.documents.updateDocument({
collectionName: "products",
documentId: "42",
body: { price: 79.0 },
});Change the same fields on every document matching a filter — updateDocuments,
PATCH /collections/{collectionName}/documents?filter_by=…, answers
{"num_updated": n}:
await client.documents.updateDocuments({
collectionName: "products",
updateDocumentsParameters: { filter_by: "brand:=Acme" },
body: { in_stock: false },
});Batches: import with per-row results
importDocuments — POST /collections/{collectionName}/documents/import —
takes JSONL (one document per line) and accepts action, dirty_values,
batch_size, return_id and return_doc as query parameters.
curl -X POST "https://api.aacsearch.com/v1/collections/products/documents/import?action=emplace&return_id=true" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/octet-stream" \
--data-binary @changes.jsonlThe response is JSONL with one line per input line, in the same order:
{"success": true, "id": "42"}
{"success": false, "error": "Bad JSON.", "document": "[bad doc"}The request as a whole answers 200 even when some rows failed, so a batch is
not all-or-nothing: read every line, and retry only the rows that failed —
the rest are already indexed.
async function importBatch(collectionName: string, docs: Record<string, unknown>[]) {
const jsonl = docs.map((doc) => JSON.stringify(doc)).join("\n");
const res = await client.documents.importDocuments(
{ data: new TextEncoder().encode(jsonl), filename: "batch.jsonl", contentType: "application/x-ndjson" },
collectionName,
{ importDocumentsParameters: { action: "emplace", dirty_values: "coerce_or_drop" } },
);
const results = new TextDecoder().decode(await res.arrayBuffer()).trim().split("\n")
.map((line) => JSON.parse(line) as { success: boolean; error?: string });
// results[i] belongs to docs[i]
return docs.filter((_, i) => !results[i]?.success);
}
let pending = changedProducts;
for (let attempt = 0; attempt < 3 && pending.length > 0; attempt++) {
pending = await importBatch("products", pending);
}
if (pending.length > 0) console.error("still failing", pending.map((doc) => doc.id));A row that fails for the same reason twice (a schema mismatch, a missing id)
will not succeed on a third try — log it and fix the data rather than retrying
forever. Pass action explicitly on every write rather than relying on a
default.
exportDocuments (GET /collections/{collectionName}/documents/export)
returns the collection as JSONL in the same format, so an export can be fed
straight back into an import.
Deleting documents
One document — deleteDocument,
DELETE /collections/{collectionName}/documents/{documentId}:
await client.documents.deleteDocument({ collectionName: "products", documentId: "43" });Every document matching a filter — deleteDocuments,
DELETE /collections/{collectionName}/documents?filter_by=…, answers
{"num_deleted": n}:
curl -X DELETE -G "https://api.aacsearch.com/v1/collections/products/documents" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" \
--data-urlencode "filter_by=id:[43,44,45]"truncate: true removes every document but keeps the collection and its
schema. For a sync that removes what your catalogue no longer has, either
delete the ids you know were removed, or stamp every document with a sync
marker (synced_at) and delete by filter_by: "synced_at:<…" after the run.
Async ingest jobs
When you don't want to hold a connection open — or the data is a URL, a
sitemap, a YML feed, CSV, Markdown or HTML rather than JSON documents — queue an
ingest job. It answers at once with status: queued.
createIngestJob — POST /ingest-jobs:
const job = await client.ingest.createIngestJob({
collection: "products",
source: "documents", // or url, sitemap, yml, youtube, csv, markdown, html
action: "upsert",
documents: [{ id: "42", price: 79.0 }], // up to 100 inline documents
});Poll getIngestJob (GET /ingest-jobs/{jobId}) until status is ready or
failed:
let current = job;
while (current.status === "queued" || current.status === "processing") {
await new Promise((resolve) => setTimeout(resolve, 2000));
current = await client.ingest.getIngestJob({ jobId: job.id });
}
console.log(current.stats); // { fetched, imported, failed, errors: [{ id, error }] }stats.errors names each failed document. retryIngestJob
(POST /ingest-jobs/{jobId}/retry) re-runs a failed job,
retryFailedIngestJobs (POST /ingest-jobs/retry-failed) every failed job,
and cancelIngestJob (POST /ingest-jobs/{jobId}/cancel) stops one.
listIngestJobs (GET /ingest-jobs) lists recent jobs newest first;
deleteIngestJob (DELETE /ingest-jobs/{jobId}) removes a job's record
without touching documents already imported. Large YML feed files go through
uploadIngestFeed — see YML feeds.
Preview and confirm a dataset first
Before committing to a collection schema, previewIngest
(POST /ingest/preview) inspects a bounded inline sample — up to 2 MiB, 200
fields per object, eight nested levels, 20,000 inspected values — and proposes
field roles and a mapping, without creating a collection, writing
documents, fetching remote URLs or generating embeddings:
const preview = await client.ingest.previewIngest({
source: "documents",
documents: [{ id: "42", title: "Cordless drill, 18V", price: 89 }],
});
// preview.profile, preview.proposal, preview.capabilitiesdocuments sources require documents and forbid content; text sources
(markdown, csv, html) require non-empty content and forbid documents.
roles overrides an inferred field role explicitly, keyed by observed field
path. The suggestions describe the sample, not a guarantee — nothing is
verified against the running engine until you act on them.
For an existing collection, confirmDatasetProfile
(PUT /collections/{collectionName}/profile) reanalyzes a sample with your
explicit overrides and stores the result, rejecting any mapping that conflicts
with the collection's real schema or names a field the collection doesn't
have; it still does not import documents or change the engine schema.
getDatasetProfile (GET /collections/{collectionName}/profile) reads the
confirmed profile back — a stored profile is not proof the collection is
runtime-ready, only that its roles were reviewed.
Reindexing
In place
reindexCollection — POST /collections/{collectionName}/reindex — queues a
reindex ingest job that exports every document of the collection and imports
it back (action, default upsert). Use it to pick up field-mapping or
embedding changes. It returns an ingest job; poll it like any other.
const reindex = await client.collections.reindexCollection({ collectionName: "products" });Zero downtime with an alias
For a change the existing collection can't take (a new field type, a different tokenizer), build a new collection next to the old one and switch traffic over with an alias — a name that points to a collection, like a symbolic link. Searches, imports and widgets that use the alias name don't change.
Point your application at an alias from day one:
await client.collections.upsertAlias({ aliasName: "products", collection_name: "products_v1" });
// search, widget and sync all use "products"To reindex:
Create the new collection
Create products_v2 with the new schema (createCollection).
Import everything into it
importDocuments, retrying failed rows. Keep writing live changes to both
collections while this runs.
Check it
searchCollection against products_v2 directly.
Swap
upsertAlias (PUT /aliases/{aliasName}) re-points the alias in one call:
curl -X PUT https://api.aacsearch.com/v1/aliases/products \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/json" \
-d '{"collection_name":"products_v2"}'Drop the old collection when you're sure
deleteCollection (DELETE /collections/products_v1). Until then, rolling
back is another upsertAlias to products_v1.
getAlias shows where an alias points; getAliases lists them all.
deleteAlias (DELETE /aliases/{aliasName}) removes the alias — the
collection it pointed to is untouched.
Launch checklist
Verify data, access, relevance, recovery and operations with concrete checks before sending visitors to search.
Review a dataset and propose a schema
Submit a sample of your data, get a profiled proposal for the collection schema and field roles, check which capabilities it enables, then confirm it.