search.json reference

Every key of search.json — collections, sources, the from locators and fallback lists, field flags, typed attributes, locale fields, scope, validation and what version 3 adds — with the products app's collection as the worked example.

search.json declares which of your entities are indexed for search and what of each row goes into a document. The platform builds the index from it and keeps it in step with your rows. You write no indexing code, no triggers and no crawler. For when to reach for it at all, see Projections.

The JSON Schema at https://schemas.revenexx.com/search.schema.json is the machine-readable contract. Point your editor at it; this page explains what each key means.

A complete example

The products app ships this collection (trimmed):

search.json
{
  "$schema": "https://schemas.revenexx.com/search.schema.json",
  "version": "2",
  "collections": [
    {
      "name": "products",
      "source": "products",
      "key": "id",
      "scope": { "by": ["channel", "locale"], "mode": "facet" },
      "fields": [
        { "name": "sku",             "from": "column:sku",                    "sort": true },
        { "name": "kind",            "from": "column:kind",                   "facet": true },
        { "name": "enabled",         "from": "column:enabled",                "type": "bool", "facet": true },
        { "name": "name",            "from": ["attribute:locale.name", "column:label"], "type": "string", "locale": true, "sort": true },
        { "name": "description",     "from": "attribute:locale.description",  "locale": true, "optional": true },
        { "name": "manufacturer",    "from": "attribute:common.manufacturer", "facet": true, "optional": true },
        { "name": "categories",      "from": "relation:product_categories→categories.code[]", "type": "string[]", "facet": true, "optional": true },
        { "name": "category_labels", "from": "relation:product_categories→categories.labels.<locale>[]", "type": "string[]", "locale": true, "optional": true },
        { "name": "attrs",           "from": "attribute:common.*",            "type": "object", "types_from": "attributes", "index": false, "optional": true },
        { "name": "attrs_locale",    "from": "attribute:locale.*",            "type": "object", "types_from": "attributes", "locale": true, "index": false, "optional": true },
        { "name": "updated_at",      "from": "column:updated_at",             "type": "int64", "sort": true }
      ],
      "default_sort": "updated_at",
      "depends_on": ["product_categories", "categories"]
    }
  ]
}

Top level

KeyRequiredMeaning
versionYesContract version, the string "2". "3" adds the keys in Version 3 and is being rolled out (as of 2026-10).
collectionsYesOne entry per collection, at least one.

One app ships one search.json.

A collection

KeyRequiredMeaning
nameYesThe logical collection name, ^[a-z][a-z0-9_]{0,62}$. It is what a client names in a query. Each tenant gets its own copy, so the name only has to be unique inside your app.
sourceYesThe entity from your schema.json the documents are built from.
keyNoThe column that identifies a document. Default id.
fieldsYesWhat goes into each document, at least one field.
scopeNoHow channel and locale shape the documents. See Scope.
default_sortNoThe field results are ordered by when a query names no order. It must be a field with "sort": true.
depends_onNoOther entities whose changes affect a document, for example a join table and the entity it joins to. A change there re-indexes the documents it touches.

A field

KeyDefaultMeaning
name—The field name in the document, ^[a-z][a-z0-9_]{0,62}$.
from—Where the value comes from: one locator, or a list of locators tried in order. See Locators.
typeinferredstring, string[], int32, int64, float, bool, bool[], object, object[], auto or geopoint. Strings infer string, timestamps int64, relation arrays string[].
facetfalseThe field can be filtered on and counted per value.
sortfalseThe field can be sorted on.
optionalfalseThe value may be missing on a row. Mark every field whose source can be empty.
localefalseThe value is read per locale. One locale field makes the collection hold one document per row and locale.
indextrueWhether full-text search covers the field. Set false for data you only want back in a hit, such as an attribute map or an image URL.
types_fromnoneOnly on attribute:common.* and attribute:locale.*. Names the entity that declares your attributes (it needs code and type columns), and each value is typed by its attribute: boolean becomes true / false, number, integer and decimal become numbers when the stored text is numeric, everything else stays text. Typed values are what numeric ranges and boolean filters on attrs.<code> need. An unknown entity copies the values untyped.

Locators

from names the source of a value. You never write SQL.

LocatorReads
column:<col>A column of the source entity.
attribute:common.<code>One attribute from the common bucket of attribute_values.
attribute:common.*The whole common bucket, as an object.
attribute:locale.<code>One attribute from the locale bucket of the document's locale. Use with "locale": true.
attribute:locale.*The whole locale bucket of the document's locale, as an object.
relation:<join>→<entity>.<col>[]A column of every row reached through a join entity, as an array. Example: the codes of a product's categories.
relation:<join>→<entity>.<col>.<locale>[]A translated label of every row reached through a join, in the document's locale. Use with "locale": true.
reference:<attr>.labelThe label of the reference record an attribute points at.
asset:<slot>.urlThe URL of an asset linked to the row.
computed:<name>A value the platform computes, such as completeness.

The arrow is the character → (U+2192), not ->.

A list of locators

from also takes a list. The first locator that yields a value wins, in list order, so a product without a translated name still gets one:

fallback list
{ "name": "name", "from": ["attribute:locale.name", "column:label"], "type": "string", "locale": true }

All locators in one list must yield the same kind of value: single values, arrays ([]) or objects. A list of single values of different types, such as a text column and a number, works only for a field of type string or auto, and the values arrive as text. A list with one locator behaves like the plain string. An empty list, or a list that mixes kinds, is refused when the app is deployed.

reference:, asset: and computed: are accepted by the schema but resolve to an empty value for now (as of 2026-10). Declare them optional if you add them early.

Locale fields

A field with "locale": true is read in the locale of the document. The platform creates one document per row and write key of the tenant, and the document's locale field says which. A tenant that keeps text per language produces de and en documents, a regional tenant de-DE and en-US. Keys no market configures are not published.

Which rows get a document in a language is the tenant's choice, not the app's. The main language (the write key of the tenant's fallback_locale) always has every row. The other languages follow the products setting search_untranslated: hide (default) publishes a row only where it has its own text in that language, fallback publishes every row and fills gaps from the main language. A fallback list in from decides what an untranslated row shows. A tenant without market locales gets one document per key that is stored. See Languages and locale codes.

A storefront asks for one locale with the X-Revenexx-Locale header and gets one hit per row back, even when a row has documents under both spellings. See Search collections.

Scope

scope
"scope": { "by": ["channel", "locale"], "mode": "facet" }
KeyValuesMeaning
bychannel, localeThe dimensions documents vary by.
modefacet (default), materializefacet keeps one document per row and locale and exposes the channel as a filterable field. Visibility is filtered at query time. materialize writes a separate document per scope value for hard isolation. It costs more index space, so use it only when a document must not exist outside its scope.

Validation

The file is checked against the published schema when you push or publish an app version, when you validate one without publishing, and before a build starts. Unknown keys are refused, so a typo such as type_from fails instead of being ignored. The response lists each error with its path, for example collections[0].fields[1], and a build log prints them too. An invalid file fails the deployment of that version.

Version 3

Version 3 is being rolled out (as of 2026-10). Set "version": "3" to use these keys; under "2" they are refused. A version 2 file keeps working unchanged.

KeyOnMeaning
locale_sourcecollectionWhere translated text comes from: attribute_values (default), labels, or { "type": "rows", "entity": "…", "locale_column": "locale", "where": { … } } for an entity that stores one row per locale.
untranslatedcollectionsetting (default) follows the tenant's search_untranslated. publish gives every write key a document, only with a locale_source other than attribute_values.
wherecollectionOnly rows matching these values are indexed. An object of column to string, number or boolean.
viewscollectionDefault relevance settings per view (search, suggest, …): fields searched and their weights, typo tolerance, sort, facets, per_page and similar. They apply until a tenant saves its own settings for that view, and never overwrite them.
analyzerfieldplain (default), language (stemming for the document's language) or identifier (codes and SKUs).

New locators in version 3:

LocatorReads
label:<col>A translated label column in the document's locale.
row:<col>A column of the per-locale row, with locale_source of type rows.
reference:<fk>→<entity>.<col>A column of the record a foreign key points at, optionally .<locale> for a translated one.
count:<entity>How many enabled rows of an entity point at this row, as a number.
ancestors:<col>[]The column along a tree, root first, optionally .<locale>[]. Example: the names of a category's parents.

What changing the file does

Add a field to the file and deploy. Existing rows are indexed in the background after the schema is applied, so results can be incomplete for a short while after a deploy. See Ship a schema change.

Where to go next

Was this page helpful?