Custom fields and faceted search, declared once.
Every catalog has attributes that don't fit the stock name / price / category / brand mould. Apparel stores need material, fit, and season. Electronics need refresh rate and port count. Beauty needs SPF. Trooply lets you declare fields once, index products with them, filter searches on them, and render a faceted filter UI driven by auto-generated facet values.
Declare, index, filter.
No schema migrations, no storefront code to write, no filter backend to maintain. The three steps below compose.
Declare the field
Call POST /v1/custom-fields with a key, label, type, and filterable: true. Takes under a second and no re-index needed — newly-indexed products pick up the field immediately.
Index with values
Put the field in each product's metadata dict. "material": "vegan leather". "refresh_rate": 120. Null values are ignored; fields can be present on some products and absent on others.
Filter + facet
Pass the field in the filters dict on any search call. Use GET /v1/products/facets to populate your storefront's filter sidebar with real values from your catalog.
A faceted search API without the faceted search backend.
Exact-match filtering
Pass a string or boolean. {"material": "leather"}. {"in_stock": true}. Simple and predictable.
List-any-of filtering
Pass an array. {"material": ["leather", "vegan leather"]} means "at least one of these values". Natural fit for checkbox filters in the sidebar.
Numeric range filtering
{"price": {"gte": 50, "lte": 250}}. Either bound is optional. Pair with a slider control on the storefront.
Auto-populated facets
GET /v1/products/facets returns a block per declared field with the unique values your catalog actually contains (for strings) or min/max (for numerics). Render the sidebar from the response — no hardcoded values.
Pre-filter retrieval
Filters apply in Qdrant before the vector search runs, not after. A visual search for "sneakers" with in_stock: true actually retrieves only in-stock sneakers — not 10 organic candidates that happen to all be OOS.
Portal UI for declarations
Merchandisers declare fields in the portal at /portal/custom-fields — a UI with type dropdowns and filterable / facet toggles. The API endpoint exists for automation.
Catalogs that earn it.
material · fit · season · fiber
Shoppers ask for "organic cotton", "relaxed fit", "SS26". Declare each as a field. Your sidebar now filters on the attributes your merchandising team already tracks in your PIM.
screen_size · refresh_rate · ports · energy_rating
Technical attributes that only make sense as typed fields. Number types support range filters — "screen_size between 24 and 27 inches" is one call.
spf · shade · skin_type · ingredients
Beauty shoppers filter on ingredient avoidance and shade match. List filters (skin_type: ["oily","combination"]) do the heavy lifting.
room · style · dimensions · material
"Mid-century, walnut, width under 180 cm." Three declarations, one filter call, the right products come back.
The technical shape.
Declaring a field
POST /v1/custom-fields
Authorization: Bearer $TOKEN
Content-Type: application/json
{
"key": "material",
"label": "Material",
"type": "string",
"filterable": true,
"facet": true
}
Indexing a product with the field
POST /v1/products
{
"product_id": "SKU-48120",
"image_url": "https://cdn.shop.com/bag.jpg",
"metadata": {
"name": "Marla Tote",
"price": 189,
"category": "Handbags",
"material": "vegan leather",
"fit": "oversized"
}
}
Filtering on search
POST /v1/search/text
{
"query": "tote",
"limit": 20,
"filters": {
"category": "Handbags",
"material": ["leather", "vegan leather"],
"price": {"gte": 50, "lte": 250},
"in_stock": true
}
}
Populating a filter UI
GET /v1/products/facets returns one block per filterable field with real value counts:
{
"facets": [
{
"key": "material", "label": "Material", "type": "string",
"values": [
{"value": "leather", "count": 128},
{"value": "vegan leather", "count": 57},
{"value": "canvas", "count": 22}
]
},
{
"key": "price", "label": "Price", "type": "number",
"min": 12, "max": 489
}
]
}
Render string facets as checkbox groups, number facets as range sliders, pass the shopper's selections straight back as filters. No custom backend between the catalog and the filter UI.
Common questions.
Do I need to re-index products when I declare a new field?
No — just include the field in metadata on future indexing calls. Existing products won't have the field until you re-index them. For a fast backfill, post the affected products through POST /v1/products/bulk.
Can I filter on a field I haven't declared?
Undeclared keys in filters are silently ignored — that's by design, so a storefront form can't crash when a field gets removed. Declared fields drive filtering; undeclared values are indexed and returned on results but not filterable.
Is there a limit on the number of custom fields?
Soft limit of around 50 per tenant. More is technically possible but merchandisers stop being able to use a filter sidebar with 80 checkboxes — it's a UX limit, not a platform one.
Are custom-field values searchable by text?
Metadata values are included in our text-search signal, so yes — a product indexed with material: "linen" will surface for queries that mention linen. But filtering is the precise path; text-search matches are best-effort.
Can I have multi-value (list) fields?
Yes — a product's metadata field can hold an array. "tags": ["new-arrival", "organic"]. Filtering with a list value (filters: {"tags": "organic"}) matches products whose list contains that value.
How does this interact with merchandising rules?
Cleanly. Merchandising rules scope to query / category / always — custom fields don't need their own rule scope because the filter layer already handled the narrowing before rules ran.
Stop hand-rolling your filter backend.
Declare once, index with values, filter on every search. Free tier handles up to 1,000 products.