Guide 11 · Developer docs

Search, filtering and faceted navigation

Site search on Core dna is an Elasticsearch index that the platform fills and queries, and a page you build. What reaches the index, search views, the search page, product facets, filtered listings that need no index, and when to go beyond the built-in index.

L2
Unit 01 · Indexing and search views

Search views

A search view is the admin record that defines one search: its index, the fields it matches and their weights, fixed filters, sorting, page size and highlighting, and which of them a visitor may change from the URL. How to configure one, and the three ways to run it.

Who this is for. The developer setting up search for a site, and the reviewer checking that a view's scope cannot be widened from the address bar. Afterwards you can configure a view, lock what must not change, and run it from a page, a template or the API.

What a view controls

Views are created under Search › Views, belong to one site, and are addressed by their slug. A site needs at least one before its search page can show anything. Each setting has a value saved on the view and, for most, a URL parameter that overrides it while the setting's Changeable switch is on.

SettingWhat it doesURL parameterChangeable on a new view
IndexSite Search (pages, posts, FAQ questions, product descriptions and the rest) or Faceted Search (product descriptions, with facet counts). Chosen when the view is created; it cannot be changed afterwardsNoneNot applicable
Page sizeResults per page: 15, the admin's Table entries setting, unless you enter another number. Saved with the viewlimitNo
Full text query typeMatch words finds records with any of the words, scored by the best-matching field. Match phrase needs the words in order, the last one as the start of a word, and needs Fuzzy search offqueryType: best_fields or phrase_prefixNo
Minimum should matchHow many of the query's words a result must contain: 2, 75%minimumShouldMatchYes
Tie breaker0 to 1: how much matches in fields other than the best one add to the scoretieBreakerYes
Fuzzy searchAccepts one or two wrong letters in a word, by its length. On by defaultfuzzy=0 or fuzzy=1Yes
Full text search fieldsThe text fields a query is matched against, each with a weight. With none listed, every field is searchedsearchFields, as name^3,fields.summaryYes
Filter fieldsFixed values per field, each field with Changeable, Ignore empty and, on a Faceted Search view only, RenderableThe field's name: tags.name=NewsYes, each field
Sort fieldsFields and directions, in order. With none, results come in relevance order when there is a queryorderBy, as published:descYes
HighlightText fields whose matching words come back wrapped in <em>highlightFieldsYes

The query is always q, and the page number page. How a query is matched and ranked is on Queries, ranking and highlighting. Filter values work like this:

  • A value is compared with the whole stored value, ignoring case and accents. Several values for one field, as a comma-separated list or by repeating the parameter with [], mean any of them; different fields must all match.
  • A value that contains a comma cannot be filtered on: the comma splits it into two values.
  • A URL value replaces the view's own values for that field; it does not add to them.
  • There are no ranges. A date or a number is matched exactly, so "under $50" is a band computed when indexing (Price ranges become bands).
Match phrase and fuzzy search cannot be combined

Elasticsearch refuses a phrase query with fuzziness, so keep Fuzzy search off on every Match phrase view, and its Changeable off too, so ?fuzzy=1 cannot turn it back on.

What a visitor can change

On a new view every setting except page size and query type is changeable, and so is every filter field. Anyone can add ?entityClass=… to a blog search and turn it into a search of pages, sort by any filterable field, or choose which fields are searched. Only published records that a signed-out visitor may read are in the index, so this exposes nothing private, but a view's scope is a suggestion until you lock it:

  • Switch Changeable off on every filter that defines the view's scope: the record type of a blog search, the category of a guides search, the language of a regional site.
  • Leave changeable only what the page offers: the filters in its form and the sort orders in its menu.
  • Keep Ignore empty on for any filter a form can send blank. With it off, an empty ?tags.name= makes the view return nothing; so does a filter switched to Use with no values.
  • searchFields and highlightFields are not checked against the view's lists: any field name from the URL is used. orderBy is limited to the filterable fields and _score; another name is dropped.

Three ways to run a view

  • The search page. GET /search/<slug> runs the view with every query parameter of the request and renders your search/search.html; requested with Accept: application/json, it answers JSON instead (The search route and template).
  • The $_search helper, in any front-end template: $_search->search('<slug>', [parameters]) runs the view with the parameters you give it, as the URL would carry them, and ignores the request's own. It returns the same response object as the page. Use it for a related-content block, a list of the newest guides, or results inside another page.
  • The API. GET /cdna-api/search/<slug>, with an OAuth access token or, the older option, an API key. It answers data.response only, and answers 500 when the search fails, so an integration can tell an outage from an empty result (API reference).

The page and the helper find the view by its slug on the current site; a view belonging to another site on the account is not found, and the page answers 404. Parameters given to the helper obey the same Changeable switches as the URL's, so a filter whose Changeable is off cannot be set from a template either:

Template code
{$guides = $_search->search('latest-guides', ['categories.slug' => 'guides'])}
{if $guides->getTotalCount() > 0}
  <ul class="latest-guides">
    {foreach $guides->getResultItems() as $item}
      <li><a href="{$item->getText('fullPath')|escape}">{$item->getText('name')|escape}</a></li>
    {/foreach}
  </ul>
{/if}
{% set guides = _search.search('latest-guides', {'categories.slug': 'guides'}) %}
{% if guides.getTotalCount() > 0 %}
  <ul class="latest-guides">
    {% for item in guides.getResultItems() %}
      <li><a href="{{ item.getText('fullPath')|e }}">{{ item.getText('name')|e }}</a></li>
    {% endfor %}
  </ul>
{% endif %}

A call with no q lists everything the view and its filters cover, so a view with a page size of 5 and a sort on published:desc is a "latest" list. If the call fails for any reason, the helper returns an empty response and logs the error, marked so that administrators see it in the admin as well as in the platform's log.

When a site needs more than one view

One view per distinct search the site offers, because a view fixes scope and behaviour together:

  • Site search: Site Search index, every type, Match words, fuzzy on, highlighting on the body field.
  • A section's search, such as the blog or the help centre: the same index with the record type or a category fixed and not changeable.
  • Search as you type in the header: Match phrase with fuzzy off, a page size of 5, and name weighted well above everything else.
  • A product finder: a Faceted Search view, with its facets marked Renderable (Showing the facets).
  • Lists inside other pages, run through $_search with no query and a fixed sort.

Give each view a slug that says what it is for. The slug is part of the public URL and of every template that calls the view, so renaming it later means finding every reference.

Go deeper

Was this page helpful?

Your answer goes to the docs team.