Facets

Facets let a reader narrow the results themselves, inside the boundary you set.

Set by Job
fq You Remove content the reader must never see, across every request.
Facets The reader Narrow what is left.

The two never fight. fq decides the pool, facets pick from that pool, and a reader cannot widen past your filter. See Filter results (fq).

The SDK owns the facet state. It reads the counts, renders the groups, tracks what is selected, sends the filter, and refreshes the results. Your part is to name the fields and style the block. There is no filter logic to write.

Facets belong to Hybrid Search and Search. An answer has no result list to narrow.

Turn them on

Two steps: name the fields, and put the element on the page.

const workflow = client.ui.hybridSearch;

workflow.useApi({
  facets: ['tags', 'categories'],
});

Miso counts only the fields you name. A custom attribute uses its dotted path, such as custom_attributes.section.

await client.ui.ready;
document.querySelector('#my-root').innerHTML =
  MisoClient.ui.defaults.hybridSearch.templates.root();

The default markup already holds <miso-facets></miso-facets>. To place the block yourself, put that element where you want it.

What a click sends

A reader selecting a value does not change q or fq. The SDK adds a separate field, facet_filters, and runs the search again:

{
  "q": "linux",
  "facets": ["tags"],
  "facet_filters": { "tags": { "terms": ["Computing"] } }
}

Two consequences worth knowing.

Your fq still applies. A reader narrows inside the rules you set, and cannot widen past them. See Filter results (fq).

One value per field. Picking a second value in the same group replaces the first: a click on Computing sends ["Computing"], and a click on FLOSS next sends ["FLOSS"], not both. Selecting values in different fields does combine.

Rename the labels

Field names read like a schema. Replace them with words your readers know:

const LABELS = {
  tags: 'Topic',
  categories: 'Section',
  'custom_attributes.section': 'Desk',
};

workflow.useLayouts({
  facets: {
    templates: {
      title(layout, { field }) {
        const { escapeHtml } = layout.templates.helpers;
        return escapeHtml(LABELS[field] || field);
      },
      value(layout, { value }) {
        return layout.templates.helpers.escapeHtml(value);
      },
    },
  },
});

The heading now reads "Topic" rather than "tags". Use value the same way to tidy the values themselves, such as turning science-and-tech into "Science and technology".

Miso strips a custom_attributes. prefix before it draws the default heading, so custom_attributes.section already shows as section. Your LABELS lookup sees the full path, so key it either way:

title(layout, { field }) {
  const name = field.replace('custom_attributes.', '');
  return layout.templates.helpers.escapeHtml(LABELS[name] || name);
}

The templates

Each one replaces a smaller part of the block. Change the least you can.

Template Draws Arguments
title One group heading. (layout, { field })
value One option label. (layout, { field, value, count })
count The number beside a value. (layout, { count })
option One whole row. (layout, entry, state)
options The list of rows in a group. (layout, facet, state)
header The heading row of a group. (layout, facet, state)
facet One whole group. (layout, facet, state)
facets Every group. (layout, state)

Keep data-role="option" if you rewrite option. The SDK listens for clicks on that attribute. A row without it renders correctly and stops filtering, with no error.

Hide the counts, for example, by returning nothing from count:

workflow.useLayouts({
  facets: { templates: { count: () => '' } },
});

Hide some values

To drop values a reader has no use for, override options, filter the entries, and hand the rest to the original template:

workflow.useLayouts({
  facets: {
    templates: {
      options(layout, data, state) {
        const skip = ['Uncategorised'];
        const entries = data.entries.filter(([value]) => !skip.includes(value));
        const { options: defaultOptions } = layout.constructor.defaultTemplates;
        return defaultOptions(layout, { ...data, entries }, state);
      },
    },
  },
});

data.entries is an array of [value, count] pairs, so entries[0] is the value and entries[1] is the count. Filter, sort or slice it, then let Miso draw the rows.

Reach the original through layout.constructor.defaultTemplates. Inside an override, layout.templates.options is your own function, so calling it repeats forever. defaultTemplates holds the version you replaced. This is the way to add to a template rather than rewrite it.

The counts stay as Miso calculated them. Hiding a row removes the control, not the results behind it. To change what the query matches, filter with fq.

Order and empty groups

Values arrive most common first, and Miso returns the top 10 values per field. A field with no matching values is left out of the response, so the group does not render.

To show a different number of values, or to filter which ones appear, set the facet as an object on the endpoint. See Hybrid Search API for size, alias, include, exclude and ranges.

Can I use checkboxes, or a dropdown?

Not today. Facets render as clickable rows, and each field holds one value.

You can make the rows look like checkboxes with the option template, but do not. A checkbox tells a reader to tick several values in a group, and the second tick clears the first. The control then promises what the behaviour does not do.

The SDK has a checkbox layout and a select layout, and neither one fits here. The checkbox layout is a single on and off switch, used for the Answer Updates subscribe button. The select layout is the single-choice dropdown used for sorting.

If you need several values at once, filter with fq while you wait, and ask Miso for multi-select facets. See Filter results (fq).

Do not drive facets from your own code

Facet state belongs to the SDK, and there is no public call to select a value for the reader. That is the design, not a gap: one owner of the selection means the chips, the counts, the request and the results cannot disagree.

When your code needs to control what is eligible, that is a different job, and fq does it:

workflow.useApi({ fq: 'custom_attributes.premium:"false"' });

Set fq and the facet counts follow it automatically.

Next