.md

Faceted search

Faceted search lets users narrow down results by selecting filter values. dcupl provides facets() to get available options with counts and addCondition() to filter results.

Key dcupl functions

Function Purpose
catalog.fn.facets() Get filter options with counts for an attribute
query.addCondition() Add a value; values on the same attribute are ORed
query.setCondition() Replace the conditions on that condition's attribute
query.removeCondition() Remove one value that addCondition() added
query.removeGroup() Remove a whole group, for example every value of one facet
query.addGroup() Add a group of conditions with its own key and AND/OR type
query.clear() Remove all active filters (and sort/paging options)

Example data

The outputs on this page come from this setup:

Setup
import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl();
dcupl.models.set({
  key: 'Product',
  properties: [
    { key: 'name', type: 'string' },
    { key: 'category', type: 'string' },
    { key: 'brand', type: 'string' },
  ],
});
dcupl.data.set(
  [
    { key: 'p1', name: 'Laptop', category: 'Electronics', brand: 'Apple' },
    { key: 'p2', name: 'Phone', category: 'Electronics', brand: 'Samsung' },
    { key: 'p3', name: 'Tablet', category: 'Electronics', brand: 'Apple' },
    { key: 'p4', name: 'Jacket', category: 'Clothing', brand: 'Patagonia' },
    { key: 'p5', name: 'Novel', category: 'Books', brand: 'Penguin' },
    { key: 'p6', name: 'Cookbook', category: 'Books', brand: 'Penguin' },
    { key: 'p7', name: 'Headphones', category: 'Electronics', brand: 'Sony' },
  ],
  { model: 'Product' },
);
await dcupl.init();

Getting facets

Use facets() to get available filter values with their counts.

const list = dcupl.lists.create({ modelKey: 'Product' });

const categoryFacets = list.catalog.fn.facets({
  attribute: 'category',
});

// categoryFacets[0]:
// { key: 'Electronics', value: 'Electronics', selected: false, enabled: true, size: 4,
//   entry: { key: 'Electronics' }, resultKeys: ['p1', 'p2', 'p3', 'p7'] }

Each entry's count is size. Counts are disjunctive: each value is counted under every other group of the list's query. The group keyed by the faceted attribute is left out, so after selecting "Electronics" the category facet still lists every category with its count, while a filter on another attribute (for example brand) reduces the counts. Values without matching items are left out unless you pass excludeZeros: false.

Applying filters

Use addCondition() to filter results based on user selections.

// Single filter
list.catalog.query.addCondition({
  operator: 'eq',
  attribute: 'category',
  value: 'Electronics',
});

const results = list.catalog.query.items(); // p1, p2, p3, p7

Multi-select filters (OR logic)

Call addCondition() once per selected value. Conditions on the same attribute go into one or group keyed by the attribute, so they are ORed, and facets({ attribute }) leaves that group out without any option:

// User ticks "Electronics" and "Clothing"
list.catalog.query.addCondition({ operator: 'eq', attribute: 'category', value: 'Electronics' });
list.catalog.query.addCondition({ operator: 'eq', attribute: 'category', value: 'Clothing' });

list.catalog.query.items(); // p1, p2, p3, p7, p4

list.catalog.fn.facets({ attribute: 'category' }).map((f) => `${f.value}: ${f.size}${f.selected ? ' ✓' : ''}`);
// ['Electronics: 4 ✓', 'Clothing: 1 ✓', 'Books: 2']

One in condition gives the same result and the same facets, and is handy when the UI hands you the whole selection at once:

list.catalog.query.setCondition({ operator: 'in', attribute: 'category', value: ['Electronics', 'Clothing'] });

If the selection lives in a group with another key, name that group with excludeGroup:

list.catalog.query.addGroup({
  groupKey: 'category_filter',
  groupType: 'or',
  queries: [
    { operator: 'eq', attribute: 'category', value: 'Electronics' },
    { operator: 'eq', attribute: 'category', value: 'Clothing' },
  ],
});

const categoryFacets = list.catalog.fn.facets({
  attribute: 'category',
  excludeGroup: 'category_filter',
});

Without excludeGroup, the facet would count the selection against itself and list only the selected values (with selected: false).

Deselecting a value

To untick one value, remove its condition; the other selections stay. To reset one facet, remove its group:

// Untick "Clothing" (a condition added with addCondition)
list.catalog.query.removeCondition({ operator: 'eq', attribute: 'category', value: 'Clothing' });

// Reset the whole category facet; filters on other attributes stay
list.catalog.query.removeGroup('category');

removeCondition() finds conditions in the group keyed by their attribute, which is where addCondition() and setCondition() put them. For a group with its own key, remove the condition from that group with list.catalog.query.remove({ groupKey: 'category_filter', queries: [condition] }), or remove the group with removeGroup('category_filter'). clear() removes every filter, including the other facets' selections.

Combining filters (AND logic)

Filters across different attributes combine with AND logic.

// Category filter
list.catalog.query.addCondition({ operator: 'eq', attribute: 'category', value: 'Electronics' });

// Brand filter (OR within) - ANDed with category
list.catalog.query.addCondition({ operator: 'eq', attribute: 'brand', value: 'Apple' });
list.catalog.query.addCondition({ operator: 'eq', attribute: 'brand', value: 'Samsung' });

// Results: Electronics items that are Apple OR Samsung (p1, p2, p3)

Complete flow

const list = dcupl.lists.create({ modelKey: 'Product' });

// 1. Get initial facets
let categories = list.catalog.fn.facets({ attribute: 'category' });
let brands = list.catalog.fn.facets({ attribute: 'brand' });

// 2. User selects "Electronics"
list.catalog.query.addCondition({ operator: 'eq', attribute: 'category', value: 'Electronics' });

// 3. Refresh facets - brand counts now reflect only Electronics,
//    categories still lists every category with "Electronics" selected
categories = list.catalog.fn.facets({ attribute: 'category' });
brands = list.catalog.fn.facets({ attribute: 'brand' });

// 4. Get filtered results
const results = list.catalog.query.items();

// 5. User unticks "Electronics" again
list.catalog.query.removeCondition({ operator: 'eq', attribute: 'category', value: 'Electronics' });

// 6. Clear all filters
list.catalog.query.clear();