Schema evolution
Add fields to a live index without rebuilding it, and know before you commit whether a change is additive or needs a full reindex.
Most schema changes are additions. A new brand attribute, a stock_status you
want to filter on, a rating for a custom ranking criterion. Rebuilding
500 000 documents to add one field is the wrong trade.
Schema evolution applies additive changes to the live collection and skips the rebuild.
Plan before apply
Two separate operations, and the split is the feature:
| Operation | Answers | Cost |
|---|---|---|
schema.plan | Is this change additive? What breaks? | free, reads the collection |
schema.apply | Do it | seconds, or a full reindex |
The plan diffs your desired field list against the fields the engine actually has — not against what the database thinks it has. That distinction matters: a field declared in the console but never applied is not present in the collection, and the plan reports what the search engine will really do.
// console: POST /search/indexes/{indexId}/schema/plan
{
organizationId: "org_...",
indexId: "idx_...",
fields: [
{ name: "brand", type: "string", facet: true },
{ name: "stock_status", type: "string", facet: true },
{ name: "rating", type: "float", sort: true }
]
}The response:
{
"added": [{ "name": "brand", "type": "string", "facet": true }],
"breaking": [],
"requiresReindex": false,
"fromVersion": 3,
"toVersion": 4,
"findings": []
}| Field | Meaning |
|---|---|
added | Fields that will be added in place |
breaking | Changes the engine cannot make without a rebuild |
requiresReindex | True when breaking is non-empty |
findings | Config lint: antipatterns, not blockers |
What counts as breaking
The engine can only add fields. Anything else goes through reindexing.
| Change | Additive? |
|---|---|
| Add an optional field | Yes |
Add a field with a default | Yes |
Turn facet on for an existing field | Yes |
Add sort to an existing field | Yes |
Change string → float | No |
| Remove a field | No — removals must be explicit |
| Change tokenization or stemming | No |
Removals are never inferred. Submitting a shorter field list merges with the stored schema rather than deleting the missing fields — a partial payload from the console must not silently drop your facet configuration.
Note: removing a field is not exposed through the console or the REST API yet. The underlying planner accepts an explicit
removeFieldslist, but there is no supported way to pass it from a client today. Removing a field currently means creating a new index, or asking support to run the removal. Treat "delete a field" as a migration, not a toggle.
Applying
// console: POST /search/indexes/{indexId}/schema/apply
{ organizationId, indexId, fields: [ /* ... */ ] }The handler lints the configuration first. Lint errors block the apply; lint
warnings are returned as findings and do not. Then:
- additive → fields are added to the live collection, no rebuild;
- breaking → an alias-swap reindex runs, queries unaffected throughout.
Both paths report fromVersion / toVersion, so you can see whether you are on
the same version as your neighbours.
The consequence nobody warns you about
A newly added faceted field has no values on existing documents. The facet appears in the response, and its bucket list is empty until documents are written again.
That is not a bug — adding a field does not rewrite documents. But it surprises:
To populate a new field, update the source mapping and send documents that actually contain its values. Delta sync writes changed documents only; an unchanged payload may be skipped. If every existing document needs the field, verify the import covers every document and inspect sample results afterwards.
A reindex rebuilds stored payloads. It cannot invent values that are absent from those payloads, so reindexing alone does not backfill a newly mapped source field.
Field definitions
| Key | Type | Effect |
|---|---|---|
name | string | Attribute name, case-sensitive |
type | string | float | int | bool | string[] | Storage and comparison |
facet | boolean | Enables facet counts and filtering |
optional | boolean | Document may omit the field |
sort | boolean | Usable in sort_by |
index | boolean | Full-text searched |
locale | string | Language for tokenization |
Built-in fields exist on every index and cannot be redeclared: id,
external_id, title, content, url, source_type, organization_id,
tags, indexed_at, metadata.
type: string[] matters more than it looks. Faceting over an array produces one
count per distinct value — that is how multi-brand or multi-category products
work. Faceting over a string that happens to contain commas does not.
Case sensitivity
Field names are case-sensitive and must match the document attribute exactly.
Values in filter_by are case-insensitive. A field declared Color with
documents carrying color facets nothing — silently.
When a facet returns an empty list, check the name before checking anything else.
Inspecting what is live
curl -sS "$AAC_API/indexes/products" \
-H "Authorization: Bearer $AAC_KEY" | jq '.fields, .schemaVersion'The console shows the same at Search → Schema, with facet and sort as
toggles and a "Plan changes" button that runs schema.plan without applying it.
Queued breaking changes
A breaking change from the console queues a rebuild and returns promptly. REST
PATCH /indexes/{indexSlug}/schema requires allow_reindex: true and returns
202 with applied: false, migration_id and job_id. The target schema is
stored separately; the live fields and settings remain available while it loads.
The migration remains Planned until the alias switches successfully. Poll the job or open Synchronization to follow its state. Cancellation and processing errors mark the migration failed without claiming that the schema was applied. A configuration edit or a revoked publication grant/API key stops an obsolete request before publication; review and submit the current schema again.