# 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:

```typescript title="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.

```typescript
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.

```typescript
// 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:

```typescript
// 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:

```typescript
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`:

```typescript
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:

```typescript
// 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.

```typescript
// 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

```typescript
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();
```

## Next steps

- [Facets Function](/sdk/fn/facets) — Complete facets() API reference
- [Query Filter](/sdk/queries/query-filter) — Filter operators and syntax
- [Query logic](/sdk/queries/query-logic) — AND/OR grouping details
- [Product Catalog Tutorial](/learn/tutorials/product-catalog) — A faceted catalog end to end
