Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
Two entities decide how an account is modelled.
| Is | Answers | |
|---|---|---|
| Catalog | Your data, in one language | What exists? |
| Channel | A view over one catalog, for one serving destination | What does this storefront see, and under which names? |
A catalog holds the objects and owns the language. A channel decides which of those objects a particular storefront may see and which of their values it gets. You index once per language and serve many destinations.
The case this is built for
Section titled “The case this is built for”A German-language business sells in Germany, Austria and Switzerland. Same products, same German text — but different prices, different availability, and a handful of products that may not be sold across every border.
That is one catalog and four channels:
Each product is indexed once, carrying every market’s values side by side:
{ "@id": "product/sku-1001", "@type": "product", "@title": "Wanderschuh Alpin", "price_de": 129.9, "price_at": 134.9, "price_ch": 149.0, "stock_de": 12, "stock_at": 4, "stock_ch": 0, "markets": ["de", "at"]}Then each channel is configured to show its own slice. Your storefront asks for price and
gets that market’s value.
Why not a catalog per domain
Section titled “Why not a catalog per domain”Four catalogs means four copies of the same German content — four times the indexing, four places to fix a typo, and four separate pools of behavioural data. Ranking learns from what shoppers do, so splitting one market’s traffic four ways slows down every copy.
One catalog keeps the content and the learning together. Channels differ only where the markets differ.
Synonyms and function words
Section titled “Synonyms and function words”With synonym matching enabled, ignored function words can appear inside a synonym
phrase. A rule for speaker for tv can match that query, and a multiword target
such as barre de son can match the corresponding title phrase. Phrase matching
ignores the removed function words. Other query words still constrain results
when all query terms are required.
Language belongs to the catalog
Section titled “Language belongs to the catalog”A catalog is created with a language, which decides how text is analyzed:
{ "organization_id": "lbo_r2vn8c", "name": "Example — German", "language": "de_DE" }de_DE selects German stemming and compound handling, so a search for “Schuhe” finds
Wanderschuhe. A different language is always a different catalog. A different market in
the same language is a channel.
Austrian German and German German share a catalog. German and French do not.
See European Portuguese text matching for accent and plural matching, searchable product terms and brand-query limitations.
Hungarian text matching
Section titled “Hungarian text matching”Covered appliance compounds match constituent queries: gép matches mosogatógép,
fűtő matches teraszfűtő, and kávé matches kávéfőző, including covered inflections.
Autocomplete accepts constituent prefixes such as gep for mosogatógép.
The whole compound remains searchable. With all query terms required, a compound
query requires the compound and any additional query words.
Accentless queries are accepted. szék (chair) and szén (coal) remain distinct;
spellings that differ only in accents, such as fűtő and futó, match and rank equally.
Finnish text matching
Section titled “Finnish text matching”Finnish text matching joins noun and adjective inflections with their base forms and
accepts queries without accents: tuulettimet matches tuuletin, and jaakaappi
matches jääkaappi. Product terms such as SE, on/off and EN stay searchable, as do ME/TE codes,
Jolla phones, the Noita game, NAMA juicers, JOKA flooring, KOSKA foods and JA Solar
panels. Negation such as ei and eivät remains part of the query.
Covered appliance compounds match constituent queries: kaappi matches jääkaappi,
kone matches astianpesukoneen, and keitin matches kahvinkeittimet.
Autocomplete accepts constituent prefixes such as kaap for jääkaappi.
The whole compound and additional query words remain required when all terms are required.
black & decker, black decker and BLACK+DECKER match each other, including
when all query terms are required. Any other & is treated as ja, which remains
searchable for JA Solar, and + as plus. When all query terms are required,
bang & olufsen therefore matches Bang & Olufsen but doesn’t match Bang Olufsen
or BANG+OLUFSEN; bang olufsen matches all three.
A colon case ending is removed from abbreviations and codes, so USB:n matches usb
and HDMI:ssä matches hdmi. The labels koko, materiaali, väri and vari
protect their values from case-ending removal:
xl matches koko:xl, and puu matches materiaali:puu. Times and ratios
such as 12:30 and 16:9 remain unchanged. Other labels can still lose a value
that is a case ending, for example merkki:lla loses lla.
Slovenian text matching
Section titled “Slovenian text matching”Slovenian matches covered noun and adjective inflections and accepts unaccented
spellings, so zarnice matches žarnice. Terms under four characters skip
inflection normalization, so miz doesn’t match miza. Uncovered inflections may
also fail to match their base forms.
Product words such as brez (without), med (honey), pod (floor), samo
(only) and nič (zero/nothing) remain searchable.
NAS/NIC networking terms, ME/TE/LE codes, SO-DIMM and TO-220 codes, units such as
in, GA, OD and Pa, and element symbols also stay searchable.
black & decker, black decker and BLACK+DECKER match each other, including
when all query terms are required. Any other & is treated as in, which remains
searchable for inch measurements, and + as plus. When all query terms are required,
bang & olufsen therefore matches Bang & Olufsen but doesn’t match Bang Olufsen
or BANG+OLUFSEN; bang olufsen matches all three.
Greek text matching
Section titled “Greek text matching”Greek matches covered noun and adjective inflections, ignores accent marks and
normalizes final sigma, so ψυγειο matches Ψυγείο and ΨΥΓΕΙΟ. Latin brand
names remain searchable. Greek typed in Latin letters is not transliterated.
Terms under four characters skip inflection normalization.
Negations δε, δεν, μη and μην remain searchable. So does every form of
ένα (one): όλα σε ένα doesn’t reduce to a search for όλα alone, and μιας
still counts in γάντια μιας χρήσης (disposable gloves).
& is treated as the conjunction και, which is removed from text matching.
Swedish text matching
Section titled “Swedish text matching”Closed Swedish compounds can match their constituent words: fläkt matches
värmefläkt, maskin matches diskmaskin, and möbler matches trädgårdsmöbler.
The whole compound remains searchable, and a phrase that contains a compound
matches the same phrase in a title.
Common product compounds such as diskmaskin, dammsugare and kylskåp match
across all their inflections. Other compounds can split differently in different
inflections, and with all query terms required, a query in one inflection can then
miss a title in another: vinkylar doesn’t match Vinkyl, although vinkyl
matches Vinkylar.
Autocomplete also accepts prefixes of constituents, such as flä for värmefläkt.
With all query terms required, a compound query still requires the compound; diskmaskin does not match a title
that contains only disk, only maskin, or the two words separately.
Swedish matches covered noun and adjective inflections and accepts unaccented
spellings, so kylskapet matches kylskåp. Terms under four characters skip
inflection normalization.
Product names and codes such as MAN, OM SYSTEM, Sony RX100 VI, EN standards,
Ni-MH, AV and MIG remain searchable. Negations inte, icke, ej and ingen
also remain searchable, along with utan (without) and allt (all).
Colon endings on common acronyms and identifiers are removed before matching:
USB:n matches usb, iPhone:s matches iphone, and tv:arna matches tv.
This applies to a fixed set of identifiers; arbitrary colon endings are kept.
Times, ratios and attribute values such as 12:30, 16:9 and färg:s are kept.
Visibility filters
Section titled “Visibility filters”Which objects a channel may serve at all.
A channel can carry a visibility_filter: a condition that every request on that channel
is narrowed by. An object the filter excludes is not reachable on the channel at all — not
by search, not by a listing, not by a direct lookup, not by a recommendation.
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/channels' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "catalog_id": "lbc_8w3k2p", "name": "example.ch", "visibility_filter": { "operator": "and", "conditions": [ { "field": "markets", "operator": "in", "value": ["ch"] } ] } }'That channel now serves only products whose markets includes ch. The product above,
sold in Germany and Austria only, is not served on example.ch.
A shopper’s own filters are applied on top of the visibility filter, never instead of it. No request widens past it.
Writing one
Section titled “Writing one”The shape is the same condition tree used by
merchandising rules — an
operator of and, or or not, and a conditions array whose entries are either leaves
(field, operator, value) or nested groups.
The map syntax a discovery request takes — {"stock": {"gt": 0}} —
is accepted too. Either way the filter is read back in the group syntax above.
It is validated against your catalog when you save it, not when it first runs. A field
your catalog has never carried, or an operator its type does not support — gt on a text
field — is a 422 at write time. Such a filter would match nothing at query time without
reporting an error, leaving the channel serving no products.
It is held to the same limits as a merchandising rule’s condition tree: up to 100
conditions, in groups nested up to 10 deep, and up to 100 values in one in / not_in,
counted as stored. A nested group must hold a condition.
See the limits for how the
conditions are counted. A filter too deep or with too many conditions is a 422 whose loc
points at the condition that put it over, with type filter_too_complex, reported on its own
before the filter’s fields are checked. A problem with one of the filter’s conditions is
placed in the filter as you sent it. In the group syntax that is conditions and an index,
then field or operator when one of them is the problem. In the map syntax it is your own
keys down to the field and its operator — ["body", "visibility_filter", "$and", 3, "brand", "eq"], or […, "brand"] when the field itself is the problem. A list over 100
values is reported with the filter’s other conditions, as filter_condition_invalid (or
catalog_field_unknown when the field itself is unknown). A filter many times past the
limits is refused before it is parsed, as filter_too_complex on visibility_filter itself.
Validation needs a catalog that has content. Neither visibility_filter nor
attribute_aliases can be set before the catalog’s first ingest — until then there is
nothing to check the names against, and a write naming either field is a 422. Create the
channel without them and set them once the catalog has been fed.
A negative condition (neq, not_in) is saved with an exists on the same field beside it,
so markets neq de scopes the channel to products that have a markets value other than
de rather than also admitting every product with no markets at all — see
Missing attributes for the full rule and for how to
keep the original reading. The response to your write carries the stored form.
Send null to clear a visibility filter. Omitting the field leaves it unchanged — see
Partial updates.
Attribute aliases
Section titled “Attribute aliases”Which values a channel serves, and under which names.
attribute_aliases maps a channel-facing name to the catalog field that holds
the value:
{ "catalog_id": "lbc_8w3k2p", "name": "example.at", "attribute_aliases": { "price": "price_at", "stock": "stock_at" }}On this channel, price means price_at. The key is the name your storefront uses; the
value is the catalog field that holds the value.
It works in both directions:
| Direction | Effect |
|---|---|
| Inbound | A filters, sort, facet or return_fields entry naming price resolves to price_at |
| Outbound | The hit comes back keyed price, holding the Austrian number |
One storefront codebase serves every market: it filters on price, sorts by price and
renders hit.price, and the channel in the request decides which catalog field that is.
curl -X POST 'https://api.eu1.luigisbox.ai/discovery/v1/search?channel_id=lbn_7q2m4d&surface_id=lbs_search_main' \ -H 'Authorization: Bearer <token>' \ -H 'X-Lbx-Visitor-Id: 8f14e45f-ea0f-4b5c-9a1d-2b3c4d5e6f70' \ -H 'Content-Type: application/json' \ -d '{ "type": "product", "touchpoint": "serp", "query": "wanderschuh", "filters": { "price": { "lt": 140 } }, "return_fields": ["@title", "price", "stock"] }'{ "hits": [ { "@id": "product/sku-1001", "@title": "Wanderschuh Alpin", "price": 134.9, "stock": 4 } ]}The same request against lbn_9x5t3b returns 129.9 and 12. The channel is the only
part of the request that differs.
A response answers in the names the request used
Section titled “A response answers in the names the request used”An alias does not hide the catalog field. Both names address it, and each hit is keyed by whichever of the two the request wrote.
On the example.at channel above, which aliases price and stock:
| Request | Result |
|---|---|
"filters": { "price": { "lt": 140 } } | Resolves to price_at; the hit comes back keyed price |
"sort": { "field": "price", "direction": "asc" } | Sorts on price_at |
"filters": { "price_at": { "lt": 140 } } | Works too; the hit comes back keyed price_at |
"return_fields": ["price_at"] | Returns that field, keyed price_at |
"return_fields": ["price", "price_at"] | Returns it once, keyed both ways |
"filters": { "stock_de": { "gt": 0 } } | Works — this channel has no alias for stock_de, so it is an ordinary field |
A request that names one field both ways receives it once, keyed both ways. Nothing is rejected.
return_fields=* and an omitted return_fields both mean every field, and a request that
enumerates nothing has named neither vocabulary. Every aliased field then comes back under
both names — price and price_at, stock and stock_at. The keys such a hit carries
depend on the channel alone: adding a filter or a sort does not change them.
When the request does enumerate its fields, it has stated which vocabulary it wants, and each field is keyed the way that request wrote it — see the table above.
Error messages speak the names you used. A rejected filter on price quotes price; the
same filter written on price_at quotes price_at.
What passes through untouched
Section titled “What passes through untouched”Anything you do not bind. @title, brand, color — if the channel has no entry for a
name, it means what it means in the catalog, in both directions. Alias the fields that vary
by destination and leave the rest unbound.
They are also single-pass: a name resolves once and is never fed back through the map.
What the API rejects, and why
Section titled “What the API rejects, and why”Each of these is a 422 at write time:
| Rejected | Because |
|---|---|
| A key naming a field the catalog already has | It would leave that field reachable under no name at all |
| Two keys bound to the same catalog field | Renaming the response back would be ambiguous |
| A value naming no field in the catalog | The alias could never resolve |
@id or @type on either side | Every hit carries its identity under its own name |
| A dotted path on either side | Nested data comes back nested, so aliasing it to a flat name is not supported |
* as a key | It is the return-fields wildcard, and binding it would rewrite the wildcard itself |
| Empty, blank or space-padded names | A catalog attribute never has a padded name |
Checking a name against the catalog needs a catalog with content, so an alias map cannot be set before the catalog’s first ingest either.
When the catalog grows an aliased name
Section titled “When the catalog grows an aliased name”The first rule in that table is checked when you write the alias, against the fields the catalog carries at that moment. Field types follow ingested data, so a catalog can grow a field later that an existing alias already uses as its key.
If that happens, the real field wins: the alias stops taking effect on that channel, in both directions. The name resolves to the catalog’s own field, and hits carry that field’s value under it. A field your catalog carries is always reachable under its own name; the alias gives way.
The alias is not deleted, and nothing else about the channel changes. Remove it, or rename
its key, and the binding works again. Until then it has no effect. Prefer alias keys your
catalog will not grow on its own — a storefront-facing name like price rather than a
feed-shaped one like price_at_net.
Modelling your data for this
Section titled “Modelling your data for this”Put each market’s value in its own field on one object. price_de, price_at,
price_ch — flat, one object per product.
- Do not create one product per market. It duplicates content and splits ranking signal per market.
- Do not nest per-market values (
prices.de). Nested data comes back nested, and aliasing a nested path to a flat channel name is not supported. - Do carry a market list (
markets: ["de", "at"]) if products differ by where they may be sold. It gives visibility filters something to read, and adding a market needs no schema change. - Do keep names predictable. A consistent
<field>_<market>convention makes each new channel a two-line configuration.
Where surfaces fit
Section titled “Where surfaces fit”A surface is a configured discovery experience — a search results page, an autocomplete panel, a recommendation strip — and it is scoped to the catalog, not the channel. Every channel over a catalog can serve every surface on it.
Adding Switzerland is one channel and no surface work: the search configuration, the
ranking, and the merchandising are already there. A request names both — surface_id for
which experience, channel_id for which market.
Common mistakes
Section titled “Common mistakes”- A catalog per domain. Four copies of one language, and ranking learning split four ways.
- A channel per language. Language decides text analysis, and that lives on the catalog. Two languages are two catalogs.
- Naming one field both ways in one request. On a channel that aliases
pricetoprice_at, those are one field, and a request writing both is rejected. Send the alias. - Aliasing a name your catalog may grow later. The write is rejected only if the field exists then. If a later ingest adds it, the real field takes the name back and the alias goes inert — see When the catalog grows an aliased name.
- Expecting per-market products to be filtered automatically. Nothing infers which markets a product belongs to — a visibility filter reads a field you populate.
- Setting a visibility filter and forgetting it. It narrows every request on the channel. When a channel returns fewer results than you expect, read its filter first.
See also
Section titled “See also”- Domain model — how organizations, catalogs, channels and surfaces relate
- Account structure — creating and managing all four over the API
- Catalog object model — the shape of the objects you index
- Filters — the operators a visibility filter uses
Was this page helpful?
Thanks.