Implementing Faceted Search

In this tutorial, you will build a complete faceted search system from scratch. Faceted search is what powers the filters on sites like Amazon, Airbnb, and most e-commerce platforms, allowing users to narrow down results by multiple criteria while seeing how many items match each option.

Time required: 15-20 minutes

What you will build: A faceted search interface for a real estate listings app with filters for property type, price range, bedrooms, amenities, and location.

Prerequisites

Before starting, make sure you have:

Faceted search combines full-text search with a multi-dimensional filtering system. The key features are:

  1. Dynamic counts - Each filter option shows how many results it would return
  2. Multi-select - Users can select multiple values within a facet
  3. Cross-facet filtering - Selecting a filter in one facet updates counts in other facets
  4. No dead ends - Users always see options that have results

Step 1: Set Up the Project

Create a new project and install dependencies:

terminal
mkdir faceted-search
cd faceted-search
npm init -y
npm install @dcupl/core

Create the main application file:

search.ts
import { Dcupl } from '@dcupl/core';

async function main() {
  const dcupl = new Dcupl();

  // We will build the faceted search here
}

main();

Step 2: Define the Property Model

Real estate listings have many filterable attributes. Define a model that captures them:

search.ts
import { Dcupl } from '@dcupl/core';

async function main() {
  const dcupl = new Dcupl();

  // Define the Property listing model
  dcupl.models.set({
    key: 'Property',
    properties: [
      { key: 'title', type: 'string' },
      { key: 'description', type: 'string' },
      { key: 'propertyType', type: 'string' },
      { key: 'price', type: 'int' },
      { key: 'bedrooms', type: 'int' },
      { key: 'bathrooms', type: 'int' },
      { key: 'sqft', type: 'int' },
      { key: 'city', type: 'string' },
      { key: 'neighborhood', type: 'string' },
      { key: 'amenities', type: 'Array<string>' },
      { key: 'yearBuilt', type: 'int' },
      { key: 'parking', type: 'boolean' },
      { key: 'petFriendly', type: 'boolean' },
    ],
  });

  console.log('Property model defined');
}

main();

Step 3: Load Sample Property Data

Add a realistic dataset with varied properties:

search.ts
// ... after dcupl.models.set() ...

dcupl.data.set(
  [
    {
      key: 'p1',
      title: 'Modern Downtown Loft',
      description: 'Stunning loft with city views',
      propertyType: 'Apartment',
      price: 450000,
      bedrooms: 1,
      bathrooms: 1,
      sqft: 850,
      city: 'San Francisco',
      neighborhood: 'SoMa',
      amenities: ['gym', 'rooftop', 'doorman'],
      yearBuilt: 2018,
      parking: true,
      petFriendly: true,
    },
    {
      key: 'p2',
      title: 'Cozy Family Home',
      description: 'Perfect for families',
      propertyType: 'House',
      price: 750000,
      bedrooms: 4,
      bathrooms: 2,
      sqft: 2200,
      city: 'San Francisco',
      neighborhood: 'Sunset',
      amenities: ['garage', 'backyard', 'fireplace'],
      yearBuilt: 1965,
      parking: true,
      petFriendly: true,
    },
    {
      key: 'p3',
      title: 'Luxury Penthouse',
      description: 'Top floor with panoramic views',
      propertyType: 'Apartment',
      price: 1200000,
      bedrooms: 3,
      bathrooms: 2,
      sqft: 1800,
      city: 'San Francisco',
      neighborhood: 'Pacific Heights',
      amenities: ['gym', 'rooftop', 'doorman', 'pool', 'concierge'],
      yearBuilt: 2020,
      parking: true,
      petFriendly: false,
    },
    {
      key: 'p4',
      title: 'Victorian Charm',
      description: 'Historic home with modern updates',
      propertyType: 'House',
      price: 980000,
      bedrooms: 3,
      bathrooms: 2,
      sqft: 1950,
      city: 'San Francisco',
      neighborhood: 'Haight',
      amenities: ['backyard', 'fireplace'],
      yearBuilt: 1905,
      parking: false,
      petFriendly: true,
    },
    {
      key: 'p5',
      title: 'Starter Condo',
      description: 'Great investment opportunity',
      propertyType: 'Condo',
      price: 320000,
      bedrooms: 1,
      bathrooms: 1,
      sqft: 650,
      city: 'Oakland',
      neighborhood: 'Lake Merritt',
      amenities: ['gym', 'laundry'],
      yearBuilt: 2010,
      parking: true,
      petFriendly: false,
    },
    {
      key: 'p6',
      title: 'Spacious Townhouse',
      description: 'Multi-level living',
      propertyType: 'Townhouse',
      price: 680000,
      bedrooms: 3,
      bathrooms: 2,
      sqft: 1600,
      city: 'Oakland',
      neighborhood: 'Rockridge',
      amenities: ['garage', 'patio'],
      yearBuilt: 1995,
      parking: true,
      petFriendly: true,
    },
    {
      key: 'p7',
      title: 'Urban Studio',
      description: 'Efficient city living',
      propertyType: 'Apartment',
      price: 280000,
      bedrooms: 0,
      bathrooms: 1,
      sqft: 450,
      city: 'Oakland',
      neighborhood: 'Downtown',
      amenities: ['gym', 'rooftop'],
      yearBuilt: 2015,
      parking: false,
      petFriendly: false,
    },
    {
      key: 'p8',
      title: 'Family Estate',
      description: 'Expansive property with pool',
      propertyType: 'House',
      price: 1500000,
      bedrooms: 5,
      bathrooms: 4,
      sqft: 3500,
      city: 'San Francisco',
      neighborhood: 'Sea Cliff',
      amenities: ['pool', 'backyard', 'garage', 'fireplace'],
      yearBuilt: 1985,
      parking: true,
      petFriendly: true,
    },
    {
      key: 'p9',
      title: 'Modern Condo',
      description: 'Sleek and stylish',
      propertyType: 'Condo',
      price: 520000,
      bedrooms: 2,
      bathrooms: 1,
      sqft: 950,
      city: 'San Francisco',
      neighborhood: 'Mission',
      amenities: ['gym', 'rooftop', 'bike-storage'],
      yearBuilt: 2019,
      parking: false,
      petFriendly: true,
    },
    {
      key: 'p10',
      title: 'Charming Bungalow',
      description: 'Craftsman details throughout',
      propertyType: 'House',
      price: 620000,
      bedrooms: 2,
      bathrooms: 1,
      sqft: 1100,
      city: 'Oakland',
      neighborhood: 'Temescal',
      amenities: ['backyard', 'fireplace'],
      yearBuilt: 1925,
      parking: true,
      petFriendly: true,
    },
  ],
  { model: 'Property' }
);

await dcupl.init();
console.log('Loaded 10 properties');

Step 4: Create the Property List

Create a list for querying properties:

search.ts
// ... after dcupl.init() ...

const propertyList = dcupl.lists.create({ modelKey: 'Property' });

console.log('\n=== Real Estate Faceted Search ===');
console.log('Total listings:', propertyList.catalog.query.count());

Step 5: Build the Facet System

Now create a function that generates facets for all filterable attributes:

search.ts
// Define which attributes are facetable
const facetableAttributes = [
  'propertyType',
  'city',
  'neighborhood',
  'bedrooms',
  'parking',
  'petFriendly',
  'amenities',
];

function displayFacets() {
  console.log('\n--- Available Filters ---\n');

  // Property Type facet
  console.log('Property Type:');
  propertyList.catalog.fn.facets({ attribute: 'propertyType' }).forEach((f) => {
    console.log(`  [ ] ${f.value} (${f.count})`);
  });

  // City facet
  console.log('\nCity:');
  propertyList.catalog.fn.facets({ attribute: 'city' }).forEach((f) => {
    console.log(`  [ ] ${f.value} (${f.count})`);
  });

  // Bedrooms facet
  console.log('\nBedrooms:');
  propertyList.catalog.fn.facets({ attribute: 'bedrooms' }).forEach((f) => {
    const label = f.value === 0 ? 'Studio' : `${f.value} bed`;
    console.log(`  [ ] ${label} (${f.count})`);
  });

  // Amenities facet (array type)
  console.log('\nAmenities:');
  propertyList.catalog.fn.facets({ attribute: 'amenities' }).forEach((f) => {
    if (f.value) {
      console.log(`  [ ] ${f.value} (${f.count})`);
    }
  });

  // Boolean facets
  console.log('\nParking:');
  propertyList.catalog.fn.facets({ attribute: 'parking' }).forEach((f) => {
    const label = f.value ? 'Has Parking' : 'No Parking';
    console.log(`  [ ] ${label} (${f.count})`);
  });

  console.log('\nPet Friendly:');
  propertyList.catalog.fn.facets({ attribute: 'petFriendly' }).forEach((f) => {
    const label = f.value ? 'Pet Friendly' : 'No Pets';
    console.log(`  [ ] ${label} (${f.count})`);
  });
}

displayFacets();

Run the app to see all facets:

terminal
npx tsx search.ts

Step 6: Implement Single-Select Filtering

Simulate a user selecting "House" as the property type:

search.ts
console.log('\n\n========================================');
console.log('User selects: Property Type = House');
console.log('========================================');

propertyList.catalog.query.addCondition({
  attribute: 'propertyType',
  operator: 'eq',
  value: 'House',
});

// Show filtered results
console.log(`\nMatching properties: ${propertyList.catalog.query.count()}`);
propertyList.catalog.query.items().forEach((p) => {
  console.log(`  - ${p.title} | ${p.city} | ${p.price.toLocaleString()}`);
});

// Show updated facets - notice how counts change
console.log('\n--- Updated Facets After Filter ---');
console.log('\nCity (within Houses):');
propertyList.catalog.fn.facets({ attribute: 'city' }).forEach((f) => {
  console.log(`  [ ] ${f.value} (${f.count})`);
});

console.log('\nBedrooms (within Houses):');
propertyList.catalog.fn.facets({ attribute: 'bedrooms' }).forEach((f) => {
  const label = f.value === 0 ? 'Studio' : `${f.value} bed`;
  console.log(`  [ ] ${label} (${f.count})`);
});

Notice how the facet counts update to reflect only houses.

Step 7: Implement Multi-Select Filtering

Allow users to select multiple values. First, clear the filters and try multi-select:

search.ts
propertyList.catalog.query.clear();

console.log('\n\n========================================');
console.log('User selects: City = San Francisco OR Oakland');
console.log('========================================');

// Multi-select using 'in' operator - this is OR logic within the facet
propertyList.catalog.query.addCondition({
  attribute: 'city',
  operator: 'in',
  value: ['San Francisco', 'Oakland'],
});

console.log(`\nMatching properties: ${propertyList.catalog.query.count()}`);

Step 8: Implement Price Range Filtering

Add a price range facet using comparison operators:

search.ts
console.log('\n\n========================================');
console.log('User adds: Price $400,000 - $800,000');
console.log('========================================');

// Using gte (greater than or equal) and lte (less than or equal)
propertyList.catalog.query.addCondition({
  attribute: 'price',
  operator: 'gte',
  value: 400000,
});

propertyList.catalog.query.addCondition({
  attribute: 'price',
  operator: 'lte',
  value: 800000,
});

console.log(`\nMatching properties: ${propertyList.catalog.query.count()}`);
propertyList.catalog.query.items().forEach((p) => {
  console.log(
    `  - ${p.title} | ${p.propertyType} | ${p.price.toLocaleString()}`
  );
});

// Show price statistics for filtered results
const priceStats = propertyList.catalog.fn.aggregate({ attribute: 'price' });
console.log(`\nPrice range in results: ${priceStats.min.toLocaleString()} - ${priceStats.max.toLocaleString()}`);

Step 9: Filter by Array Properties (Amenities)

Filter properties that have specific amenities:

search.ts
propertyList.catalog.query.clear();

console.log('\n\n========================================');
console.log('User selects: Must have "gym" amenity');
console.log('========================================');

// Use 'find' operator to search within arrays
propertyList.catalog.query.addCondition({
  attribute: 'amenities',
  operator: 'find',
  value: 'gym',
});

console.log(`\nProperties with gym: ${propertyList.catalog.query.count()}`);
propertyList.catalog.query.items().forEach((p) => {
  console.log(`  - ${p.title} | Amenities: ${p.amenities.join(', ')}`);
});

// Add another amenity requirement (AND logic)
console.log('\n--- Adding: Must also have "rooftop" ---');

propertyList.catalog.query.addCondition({
  attribute: 'amenities',
  operator: 'find',
  value: 'rooftop',
});

console.log(`\nProperties with gym AND rooftop: ${propertyList.catalog.query.count()}`);
propertyList.catalog.query.items().forEach((p) => {
  console.log(`  - ${p.title}`);
});

Step 10: Implement Boolean Filters

Filter by boolean attributes like parking and pet-friendly:

search.ts
propertyList.catalog.query.clear();

console.log('\n\n========================================');
console.log('User selects: Pet Friendly = true, Parking = true');
console.log('========================================');

// Use isTruthy operator for boolean values
propertyList.catalog.query.addCondition({
  attribute: 'petFriendly',
  operator: 'isTruthy',
  value: true,
});

propertyList.catalog.query.addCondition({
  attribute: 'parking',
  operator: 'isTruthy',
  value: true,
});

console.log(`\nPet-friendly properties with parking: ${propertyList.catalog.query.count()}`);
propertyList.catalog.query.items().forEach((p) => {
  console.log(`  - ${p.title} | ${p.propertyType}`);
});

Step 11: Combine Multiple Facets

Build a realistic search combining multiple facet selections:

search.ts
propertyList.catalog.query.clear();

console.log('\n\n========================================');
console.log('Complex Search:');
console.log('  - Property Type: House OR Townhouse');
console.log('  - Bedrooms: 3+');
console.log('  - Price: Under $1,000,000');
console.log('  - Pet Friendly: Yes');
console.log('========================================');

// Property type (multi-select)
propertyList.catalog.query.addCondition({
  attribute: 'propertyType',
  operator: 'in',
  value: ['House', 'Townhouse'],
});

// Bedrooms (using gte - greater than or equal)
propertyList.catalog.query.addCondition({
  attribute: 'bedrooms',
  operator: 'gte',
  value: 3,
});

// Price cap (using lt - less than)
propertyList.catalog.query.addCondition({
  attribute: 'price',
  operator: 'lt',
  value: 1000000,
});

// Pet friendly (using eq for exact boolean match)
propertyList.catalog.query.addCondition({
  attribute: 'petFriendly',
  operator: 'eq',
  value: true,
});

const results = propertyList.catalog.query.items({
  sort: { attributes: ['price'], order: ['asc'] },
});

console.log(`\n${results.length} properties found:\n`);
results.forEach((p) => {
  console.log(`${p.title}`);
  console.log(`  ${p.propertyType} | ${p.bedrooms} bed | ${p.bathrooms} bath | ${p.sqft} sqft`);
  console.log(`  ${p.neighborhood}, ${p.city}`);
  console.log(`  ${p.price.toLocaleString()}`);
  console.log(`  Amenities: ${p.amenities.join(', ') || 'None listed'}`);
  console.log('');
});

// Show what facets are still available for further refinement
console.log('--- Refine Your Search ---');
console.log('\nNeighborhoods available:');
propertyList.catalog.fn.facets({ attribute: 'neighborhood' }).forEach((f) => {
  console.log(`  [ ] ${f.value} (${f.count})`);
});

Step 12: Build a Reusable Facet Manager

Create a class to manage faceted search state:

search.ts
interface FacetSelection {
  attribute: string;
  values: any[];
  operator: string;
}

class FacetedSearch {
  private list: any;
  private selections: Map<string, FacetSelection> = new Map();

  constructor(list: any) {
    this.list = list;
  }

  // Add or update a facet selection
  setFacet(attribute: string, values: any[], operator: string = 'in') {
    if (values.length === 0) {
      this.selections.delete(attribute);
    } else {
      this.selections.set(attribute, { attribute, values, operator });
    }
    this.applyFilters();
  }

  // Set a range filter (min/max)
  setRange(attribute: string, min?: number, max?: number) {
    // Remove existing range filters for this attribute
    this.list.catalog.query.clear();

    if (min !== undefined) {
      this.setFacet(`${attribute}_min`, [min], 'gte');
    }
    if (max !== undefined) {
      this.setFacet(`${attribute}_max`, [max], 'lte');
    }
  }

  // Clear a specific facet
  clearFacet(attribute: string) {
    this.selections.delete(attribute);
    this.selections.delete(`${attribute}_min`);
    this.selections.delete(`${attribute}_max`);
    this.applyFilters();
  }

  // Clear all facets
  clearAll() {
    this.selections.clear();
    this.list.catalog.query.clear();
  }

  // Apply all active filters
  private applyFilters() {
    this.list.catalog.query.clear();

    for (const [key, selection] of this.selections) {
      if (selection.values.length === 1) {
        // Single value - use eq or the specified operator
        const op = selection.operator === 'in' ? 'eq' : selection.operator;
        this.list.catalog.query.addCondition({
          attribute: selection.attribute,
          operator: op,
          value: selection.values[0],
        });
      } else {
        // Multiple values - use in operator
        this.list.catalog.query.addCondition({
          attribute: selection.attribute,
          operator: 'in',
          value: selection.values,
        });
      }
    }
  }

  // Get current results
  getResults(options?: { sort?: any; limit?: number }) {
    return this.list.catalog.query.items(options);
  }

  // Get facet counts for an attribute
  getFacets(attribute: string) {
    return this.list.catalog.fn.facets({ attribute });
  }

  // Get result count
  getCount() {
    return this.list.catalog.query.count();
  }

  // Get active filters summary
  getActiveFilters() {
    const active: string[] = [];
    for (const [key, selection] of this.selections) {
      active.push(`${selection.attribute}: ${selection.values.join(', ')}`);
    }
    return active;
  }
}

Use the FacetedSearch class:

search.ts
// Reset and demonstrate the reusable class
propertyList.catalog.query.clear();

console.log('\n\n========================================');
console.log('Using FacetedSearch Class');
console.log('========================================');

const search = new FacetedSearch(propertyList);

// User selects apartments
search.setFacet('propertyType', ['Apartment']);
console.log(`\nAfter selecting Apartment: ${search.getCount()} results`);

// User adds San Francisco
search.setFacet('city', ['San Francisco']);
console.log(`After adding San Francisco: ${search.getCount()} results`);

// Show active filters
console.log('\nActive filters:', search.getActiveFilters());

// Show remaining options
console.log('\nBedroom options:');
search.getFacets('bedrooms').forEach((f) => {
  console.log(`  ${f.value} bed: ${f.count} properties`);
});

// Clear and start over
search.clearAll();
console.log(`\nAfter clearing all: ${search.getCount()} results`);

Complete Code

Here is the complete faceted search implementation:

search.ts
import { Dcupl } from '@dcupl/core';

async function main() {
  const dcupl = new Dcupl();

  // Define Property model
  dcupl.models.set({
    key: 'Property',
    properties: [
      { key: 'title', type: 'string' },
      { key: 'description', type: 'string' },
      { key: 'propertyType', type: 'string' },
      { key: 'price', type: 'int' },
      { key: 'bedrooms', type: 'int' },
      { key: 'bathrooms', type: 'int' },
      { key: 'sqft', type: 'int' },
      { key: 'city', type: 'string' },
      { key: 'neighborhood', type: 'string' },
      { key: 'amenities', type: 'Array<string>' },
      { key: 'yearBuilt', type: 'int' },
      { key: 'parking', type: 'boolean' },
      { key: 'petFriendly', type: 'boolean' },
    ],
  });

  // Load sample data
  dcupl.data.set(
    [
      { key: 'p1', title: 'Modern Downtown Loft', propertyType: 'Apartment', price: 450000, bedrooms: 1, bathrooms: 1, sqft: 850, city: 'San Francisco', neighborhood: 'SoMa', amenities: ['gym', 'rooftop', 'doorman'], yearBuilt: 2018, parking: true, petFriendly: true },
      { key: 'p2', title: 'Cozy Family Home', propertyType: 'House', price: 750000, bedrooms: 4, bathrooms: 2, sqft: 2200, city: 'San Francisco', neighborhood: 'Sunset', amenities: ['garage', 'backyard', 'fireplace'], yearBuilt: 1965, parking: true, petFriendly: true },
      { key: 'p3', title: 'Luxury Penthouse', propertyType: 'Apartment', price: 1200000, bedrooms: 3, bathrooms: 2, sqft: 1800, city: 'San Francisco', neighborhood: 'Pacific Heights', amenities: ['gym', 'rooftop', 'doorman', 'pool', 'concierge'], yearBuilt: 2020, parking: true, petFriendly: false },
      { key: 'p4', title: 'Victorian Charm', propertyType: 'House', price: 980000, bedrooms: 3, bathrooms: 2, sqft: 1950, city: 'San Francisco', neighborhood: 'Haight', amenities: ['backyard', 'fireplace'], yearBuilt: 1905, parking: false, petFriendly: true },
      { key: 'p5', title: 'Starter Condo', propertyType: 'Condo', price: 320000, bedrooms: 1, bathrooms: 1, sqft: 650, city: 'Oakland', neighborhood: 'Lake Merritt', amenities: ['gym', 'laundry'], yearBuilt: 2010, parking: true, petFriendly: false },
      { key: 'p6', title: 'Spacious Townhouse', propertyType: 'Townhouse', price: 680000, bedrooms: 3, bathrooms: 2, sqft: 1600, city: 'Oakland', neighborhood: 'Rockridge', amenities: ['garage', 'patio'], yearBuilt: 1995, parking: true, petFriendly: true },
      { key: 'p7', title: 'Urban Studio', propertyType: 'Apartment', price: 280000, bedrooms: 0, bathrooms: 1, sqft: 450, city: 'Oakland', neighborhood: 'Downtown', amenities: ['gym', 'rooftop'], yearBuilt: 2015, parking: false, petFriendly: false },
      { key: 'p8', title: 'Family Estate', propertyType: 'House', price: 1500000, bedrooms: 5, bathrooms: 4, sqft: 3500, city: 'San Francisco', neighborhood: 'Sea Cliff', amenities: ['pool', 'backyard', 'garage', 'fireplace'], yearBuilt: 1985, parking: true, petFriendly: true },
      { key: 'p9', title: 'Modern Condo', propertyType: 'Condo', price: 520000, bedrooms: 2, bathrooms: 1, sqft: 950, city: 'San Francisco', neighborhood: 'Mission', amenities: ['gym', 'rooftop', 'bike-storage'], yearBuilt: 2019, parking: false, petFriendly: true },
      { key: 'p10', title: 'Charming Bungalow', propertyType: 'House', price: 620000, bedrooms: 2, bathrooms: 1, sqft: 1100, city: 'Oakland', neighborhood: 'Temescal', amenities: ['backyard', 'fireplace'], yearBuilt: 1925, parking: true, petFriendly: true },
    ],
    { model: 'Property' }
  );

  await dcupl.init();
  const propertyList = dcupl.lists.create({ modelKey: 'Property' });

  // Display facets
  console.log('=== Real Estate Faceted Search ===');
  console.log(`Total: ${propertyList.catalog.query.count()} properties\n`);

  console.log('Property Types:');
  propertyList.catalog.fn.facets({ attribute: 'propertyType' }).forEach((f) => {
    console.log(`  ${f.value}: ${f.count}`);
  });

  console.log('\nCities:');
  propertyList.catalog.fn.facets({ attribute: 'city' }).forEach((f) => {
    console.log(`  ${f.value}: ${f.count}`);
  });

  // Apply filters
  console.log('\n--- Filtering: Houses under $1M ---\n');
  propertyList.catalog.query.addCondition({ attribute: 'propertyType', operator: 'eq', value: 'House' });
  propertyList.catalog.query.addCondition({ attribute: 'price', operator: 'lt', value: 1000000 });

  propertyList.catalog.query.items().forEach((p) => {
    console.log(`${p.title} - ${p.price.toLocaleString()}`);
  });
}

main();

What You Learned

In this tutorial, you learned how to:

  1. Build faceted navigation with dynamic counts using catalog.fn.facets()
  2. Implement single-select filtering with the eq operator
  3. Implement multi-select filtering with the in operator
  4. Create price range filters using gte and lte operators
  5. Filter array properties (amenities) using the find operator
  6. Filter boolean properties with isTruthy and eq operators
  7. Combine multiple facets for complex searches
  8. Build a reusable FacetedSearch class

Key Operators Used

Operator Purpose Example
eq Exact match { attribute: 'city', operator: 'eq', value: 'Oakland' }
in Match any value in array { attribute: 'type', operator: 'in', value: ['House', 'Condo'] }
gte Greater than or equal { attribute: 'price', operator: 'gte', value: 500000 }
lte Less than or equal { attribute: 'price', operator: 'lte', value: 1000000 }
gt Greater than { attribute: 'bedrooms', operator: 'gt', value: 2 }
lt Less than { attribute: 'yearBuilt', operator: 'lt', value: 2000 }
find Find value in array property { attribute: 'amenities', operator: 'find', value: 'pool' }
isTruthy Check truthiness { attribute: 'parking', operator: 'isTruthy', value: true }

Next Steps

Common Issues

Facet counts don't update

Make sure you are calling facets() after applying filter conditions. Facets automatically reflect the current query state.

Multi-select not working

Use the in operator for multi-select, not multiple eq conditions:

// Correct - OR logic within facet
{ attribute: 'type', operator: 'in', value: ['A', 'B'] }

// Wrong - second condition overwrites first
{ attribute: 'type', operator: 'eq', value: 'A' }
{ attribute: 'type', operator: 'eq', value: 'B' }

Array property filtering issues

Use find for checking if an array contains a value:

// For property amenities: ['gym', 'pool']
{ attribute: 'amenities', operator: 'find', value: 'gym' } // Matches!