Skip to content
Feature · Catalog

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.

Types: string · number · boolean Filter semantics: exact · list-any-of · numeric range Facet API: GET /v1/products/facets Editor: /portal/custom-fields
How it works

Declare, index, filter.

No schema migrations, no storefront code to write, no filter backend to maintain. The three steps below compose.

Step 1

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.

Step 2

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.

Step 3

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.

What you get

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.

Use cases

Catalogs that earn it.

Apparel

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.

Electronics

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.

Beauty

spf · shade · skin_type · ingredients

Beauty shoppers filter on ingredient avoidance and shade match. List filters (skin_type: ["oily","combination"]) do the heavy lifting.

Home & interiors

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.

FAQ

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.