Use this file to discover all available pages before exploring further.
In standard (conjunctive) faceted navigation, selecting “Red” in the color facet filters the entire result set, including the color facet counts. The result: “Blue” drops to 0 because no document is both Red and Blue. Users cannot compare options within the same facet group.Disjunctive facets solve this. When a user selects “Red”, the color facet still shows “Blue (15), Green (8)” with their unfiltered counts, while other facet groups (brand, size) update normally. This is the pattern used by most ecommerce sites.
Meilisearch does not have a built-in disjunctive facet mode. Instead, you implement it client-side using multi-search. The idea is to send multiple queries in a single request:
One main query with all active filters applied, returning the hits and facet counts for non-disjunctive groups
One query per disjunctive facet group where you remove the filters for that group, so its counts reflect the broader result set
For example, if the user has selected color = Red and brand = Nike:
For each facet group that has active selections, create an additional query that excludes that group’s filters:
function buildDisjunctiveQueries(query, activeFilters, allFacetGroups) { // Build filter string for a subset of groups function buildFilter(excludeGroup) { const parts = []; for (const [group, values] of Object.entries(activeFilters)) { if (group === excludeGroup || values.length === 0) continue; if (values.length === 1) { parts.push(`${group} = "${values[0]}"`); } else { const conditions = values.map(v => `${group} = "${v}"`).join(" OR "); parts.push(`(${conditions})`); } } return parts.join(" AND ") || undefined; } // Groups that have active selections are disjunctive const disjunctiveGroups = Object.entries(activeFilters) .filter(([_, values]) => values.length > 0) .map(([group]) => group); // Non-disjunctive groups have no active selections const nonDisjunctiveGroups = allFacetGroups .filter(g => !disjunctiveGroups.includes(g)); // Main query: all filters applied, only non-disjunctive facets const queries = [ { indexUid: "products", q: query, filter: buildFilter(null), facets: nonDisjunctiveGroups } ]; // One query per disjunctive group, excluding its own filter for (const group of disjunctiveGroups) { queries.push({ indexUid: "products", q: query, filter: buildFilter(group), facets: [group], limit: 0 // we only need facet counts, not hits }); } return queries;}
Setting limit: 0 on the per-group queries avoids fetching duplicate hits. You only need the facetDistribution from these queries.
Combine the facet distributions from all queries into a single object for your UI:
function mergeFacetDistributions(results) { const merged = {}; for (const result of results) { if (!result.facetDistribution) continue; for (const [attribute, values] of Object.entries(result.facetDistribution)) { merged[attribute] = values; } } return merged;}const hits = data.results[0].hits;const facetDistribution = mergeFacetDistributions(data.results);
The first result contains the actual search hits. The remaining results contribute their facet distributions. Since each facet group appears in exactly one query, merging is a simple assignment with no conflicts.
For most applications, the total response time is comparable to a single search request. If you have many facet groups (10+), consider only making disjunctive queries for groups that the user has actively filtered on.