--- title: Catalog metadata description: List a catalog's object types, the attributes each type carries, their value types, and the distinct values an attribute holds. slug: indexing/catalog-metadata docKind: guide hub: luigisbox-ai --- Three read-only endpoints report what a catalog holds: its object types, the attributes each type carries with their value types, and the distinct values of an attribute. Check them before debugging an empty filter. All three need a token for the `https://api..luigisbox.ai/catalog` audience, and a grant to read the catalog's metadata. ## Object types :::hub[Config → Catalogs]{path="/catalogs"} A browsable view of what a catalog holds: its object types, its objects, and each object's stored attributes as raw JSON. The attribute and value listings below have no browser of their own — they populate the condition builders in [Business Rules](/merchandising/business-rules/overview/). ::: ```bash curl -G 'https://api.eu1.luigisbox.ai/catalog/v1/lbc_8w3k2p/types' \ -H 'Authorization: Bearer ' ``` Every object type the catalog has actually received content for: ```json { "items": [ { "name": "brand" }, { "name": "category" }, { "name": "product" } ], "page": 1, "size": 20, "total": 3 } ``` `name` is the value in `@type`, and the value the `type` parameter takes on [discovery requests](/discovery/overview/#anatomy-of-a-request). If a type you expected is absent, no content of it has been indexed. Where a feed supplies that type, check its [run history](/indexing/feed-management-api/#run-history). `total: 0` means nothing of any type is indexed. An unknown `catalog_id` answers the same way: this endpoint reads a catalog's content and does not resolve its identity. To tell the two apart, read the catalogs you can see from [`get_entitlements`](/api/operations/get_entitlements/). ## Attributes and their types ```bash curl -G 'https://api.eu1.luigisbox.ai/catalog/v1/lbc_8w3k2p/types/product/attributes' \ --data-urlencode 'query=price' \ --data-urlencode 'size=50' \ -H 'Authorization: Bearer ' ``` Each attribute comes back with the shape of its values: ```json { "items": [ { "name": "list_price", "type": { "shape": "number" } }, { "name": "price", "type": { "shape": "number" } }, { "name": "price_history", "type": { "shape": "array", "element": "number" } } ], "page": 1, "size": 50, "total": 3 } ``` | Field | Meaning | |---|---| | `name` | The attribute name, exactly as you use it in filters, sorts and `return_fields`. Nested attributes appear as dot-paths, e.g. `params.color`. | | `type.shape` | `string`, `number`, `boolean`, `object` or `array` | | `type.element` | For an array, the shape of its items, when known | `query` matches the attribute name, case-insensitively, on a substring. ### Why types matter more than they look A field's type decides what the field can do: | If a field is | You get | You do not get | |---|---|---| | `number` | Range filters (`price < 50`), a range facet, numeric sorting | | | `string` | Equality and membership filters, a terms facet | Range filters, numeric sorting | A `price` reported as `string` is why `price < 50` returns nothing, why the price facet renders as a list of values, and why sorting by price puts `9.90` before `10.00`. The type is fixed by the **first value the field ever receives** in the catalog, and later conflicting values are rejected rather than coerced. One early feed run sending `"price": "29.90"` types the field as text permanently. The fix is at the [mapping](/indexing/mapping/), not the query — add `| to_f`, then [reset the feed's fingerprints](/indexing/feed-management-api/#forcing-a-full-resend) so every record is resent. Ask Luigi's Box support if the field needs to be re-typed outright. ## Distinct values ```bash curl -G 'https://api.eu1.luigisbox.ai/catalog/v1/lbc_8w3k2p/types/product/attributes/color/values' \ --data-urlencode 'query=bl' \ -H 'Authorization: Bearer ' ``` The values present in the catalog, most common first: ```json { "items": [ { "value": "blue" }, { "value": "black" } ], "page": 1, "size": 20, "total": 2 } ``` Values come back typed to the attribute's own shape — numbers as numbers, booleans as booleans, not as strings. - **At most 1,000 distinct values** are considered, ranked by how many objects carry them. `total` is capped there, so a high-cardinality field reports the 1,000 most common rather than everything. - **`query` works on text attributes only.** It is a case-insensitive substring match. - **A non-scalar attribute has no pickable values** and returns an empty page. An unknown attribute returns `404`. A filter or rule builder can read this endpoint to populate a value picker. ## Using it to debug | Symptom | Check | |---|---| | A filter returns nothing | Is the field there? Is its `shape` what you assumed? Is the value spelled that way? | | A range filter returns nothing | `shape` is `string` | | A facet renders as a list instead of a slider | Same — `shape` is `string` | | Sorting looks alphabetical | Same | | A field is missing from hits | Is it in the attribute list at all? If not, it never arrived. If it is, check `return_fields` and whether it is `private:` | | A `filter.unknown_field` error | The field is not in this catalog under that name | A script that lists every attribute and its shape: ```python BASE = "https://api.eu1.luigisbox.ai/catalog/v1" types = client.get(f"{BASE}/{catalog_id}/types").json()["items"] for object_type in types: attributes = client.get( f"{BASE}/{catalog_id}/types/{object_type['name']}/attributes", params={"size": 200}, ).json()["items"] print(object_type["name"], len(attributes), "attributes") for attribute in attributes: print(" ", attribute["name"], attribute["type"]["shape"]) ``` Run it after the first successful feed run to find mistyped fields before they reach a filter. ## See also - [Filters](/discovery/filters/) — where these names and types are used - [Facets](/discovery/facets/) — why a facet's type follows the field's - [Field mapping](/indexing/mapping/) — fixing a type at the source - [Feed troubleshooting](/indexing/feeds/troubleshooting/)