AACSearch

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:

OperationAnswersCost
schema.planIs this change additive? What breaks?free, reads the collection
schema.applyDo itseconds, 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": []
}
FieldMeaning
addedFields that will be added in place
breakingChanges the engine cannot make without a rebuild
requiresReindexTrue when breaking is non-empty
findingsConfig lint: antipatterns, not blockers

What counts as breaking

The engine can only add fields. Anything else goes through reindexing.

ChangeAdditive?
Add an optional fieldYes
Add a field with a defaultYes
Turn facet on for an existing fieldYes
Add sort to an existing fieldYes
Change string → floatNo
Remove a fieldNo — removals must be explicit
Change tokenization or stemmingNo

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 removeFields list, 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

KeyTypeEffect
namestringAttribute name, case-sensitive
typestring | float | int | bool | string[]Storage and comparison
facetbooleanEnables facet counts and filtering
optionalbooleanDocument may omit the field
sortbooleanUsable in sort_by
indexbooleanFull-text searched
localestringLanguage 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.

On this page