Functionality

Simple search adds an ability to search different entities based on their data. You can search by the core fields set or add your own fields to be indexed, and then search by them. We have one index (we call it master index) where the data of the entities is indexed and there is an ability to search using it.

Supported entities

Using search you can find next entities among search results. Only published entities go to the search engine:

  • Page
  • Blog
  • Post
  • FAQ question
  • Product (Products\Entities\ProductDescription)

Core fields set

There are next fields you can search by:

  • entityClass, for example, Pages\Entities\Page, Blogs\Entities\Post
  • id -- ID of the entity
  • name
  • fullPath -- relative URL to the entity in Frontend
  • nativeText -- additional text assigned directly to the entity, not from its layout areas. Currently, it is not used.
  • layoutId -- ID of the layout entity uses
  • componentLayoutIds -- IDs of all layouts that entity components use
  • language.code -- 2 chars of the language code
  • author.displayName -- the name of the author of the entity
  • created -- the date when entity was created in Y-m-d\TH:i:sP format in UTC, e.g. 2019-08-26T21:07:38+00:00
  • published -- the date when the entity was published, in the same format as created. Present only for entities that carry a publish date (pages, posts, ...); an entity without one has no published field in its document.
  • indexed -- the date when entity is indexed to the search engine. Same format as created has. This value is changed on every reindex operation.
  • tags.name, tags.slug, categories.name, categories.slug -- tags/categories names/slugs assigned to the entity
  • relatedRecords -- IDs of the related records of the entity. It's filled from record(s) input types of the entity. Structure example is: [Forms\Entities\Form => [1], Common\Entities\Menu => [2, 10], ...]. There could be any entity in this array, not only from the supported ones for the searching.
  • fields -- array of additionally indexed fields from the entity areas where the key is the field name. You can search by value using fields.fieldName query.

The type of a core field decides how it can be filtered. id, layoutId and componentLayoutIds are numbers; created, published and indexed are dates. These six accept range filters (published=>=2026-01-01) as well as exact values. Every other core field is text and is filtered by exact value only.

Layout area fields

Editing a layout, it is possible to index some areas with a specific field. You can provide a field name the value of the area will be indexed under. You can do that for areas that use input types with text categories. Just add a new name or select among existing. For example, you have an area called ageRestriction in the layout. Use the switch to index this area and give it the "age" name. When you fill this area inside the entity, you can use fields.age to search by its values. You can index the area for the component and every entity that uses it will have its value indexed. When you change (add/remove/assign) search field for the layout area, it is automatically reindexed in the search engine. Use the "Reindex all" button if you want to force all data reindexed along with areas index fields immediately

The indexed field's type follows the area's input type editor:

Input type editor Indexed as
text, textarea, richtext, email, select, multiselect, math, code text (with an automatic .keyword copy — see below)
number number (double, because a number area may carry a decimal point)
date, datetime date
switch boolean

The type decides what you can do with the field when searching: a text field is full-text searched and filtered by exact value, a number or date field is filtered by exact value or by range, a boolean field is filtered by true / false.

Field types and the .keyword subfield

Indexed text fields serve double duty. Each is stored as text for the full-text query (q / search fields), which matches it case-insensitively — searching XL and xl finds the same record — and the framework automatically adds an exact-match .keyword copy next to it. That copy is what filtering and sorting use: fields.age is the full-text form, fields.age.keyword the exact-match form. Both ignore case and accents, so there is no per-field case toggle on the master index.

Number, date and boolean fields have no .keyword copy — they are exact by nature and are filtered under their own name (fields.weight, fields.event-date, created). Number and date fields additionally accept range filters. Boolean fields accept the usual spellings — 1 / true / yes / on and 0 / false / no / off, in any casing.

This automatic .keyword dual-use is the master index's convention. The facets index inverts it: there, fields are exact by default and you opt a field into full-text with a .text subfield — so master appends .keyword for filtering, while facets appends .text for full-text.

Indexing process

Every time you CRUD the record it is indexed in the search engine. You can also call reindexing all entities whenever you want using the "Reindex all" button.

Views

Everything described above is about indexing data to the search engine. View entity helps to specify how we search indexed data: what we search, how we sort data, how we filter it, etc. You can create a main view that returns all indexed data. At the same time you can create and set up products view that returns only records where entityClass is Products\Entities\ProductDescription.

You can set up the view in different ways: page size, full-text search settings, fields that are used for searching, filter fields, etc. Almost every setting can be set as (un)changeable -- the one you can (not) change creating a search query. When any parameter is unchangeable, the value you specified in the view will always be used.

You need to use q parameter to search by a specific phrase. Also, below there are other parameters that can be used.

Pagination

The amount of records that are returned on the page in search results can be set up in the view. If it is changeable, use limit to change it. Use page to specify a page number you want to access (started from 1).

Full-text search

There are next parameters you can use in your query:

All these fields can be (un)changeable.

Search fields

There is an ability to specify which fields are used for searching and what is the weight of them: which field match adds more scope points for the result, which means the result will be higher.

All of them are (un)changeable -- you can forbid them to change all together. If they are changeable, you can specify something like this q=coffee&searchFields=tags.name,categories.name^2. In this case, the coffee phrase will be searched only inside tag and categories names, and results found in categories will be higher in the response, because the bigger weight is specified.

You can use array-based syntax: q=coffee&searchFields[tags.name]=&searchFields[categories.name]=2, which is equal to the mentioned above. When you don't specify a weight, it is set to 1. It should be 0 and higher.

It is possible to use mixed syntax: q=coffee&searchFields[tags.name]=&searchFields[categories.name]=2&searchFields[]=name&searchFields[]=fullPath^2,entityClass^0.5.

If you don't specify such fields, default behavior is applied. Usually, all fields are searchable and affect the result.

Filter fields

There is an ability to filter results assigning to their fields specific values. You don't need to use a specific parameter to specify them, just specify a filter name as a query parameter.

You can filter by multiple values sending them as an array or just separate by comma in string (or use 2 variants together). Unspecified fields are ignored.

You can make EVERY filter field (un)changeable and make it ignore (or not) empty values. By default, every field is changeable and ignored when empty.

For example, in the query q=coffee&tags.name[]=coffee,roasting&tags.name[tea]=tea&tags.name[]=water&componentLayoutIds=, the results that contain "coffee" inside their fields will be returned

Moreover, only those which have at least one of the following tags: coffee, roasting, tea, water. componentLayoutIds is ignored when the "ignore empty" switch is enabled. When you disable it, the empty filter is kept and matches no documents at all, so the search returns an empty result. It does not mean "records that have no components" — that cannot currently be expressed; leave "ignore empty" on and do not send the parameter.

Range filters

A field whose type is a number or a date can be filtered by a range instead of an exact value. On the master index that is id, layoutId, componentLayoutIds, created, published, indexed, and every fields.* field indexed from a number, date or datetime area. Put a comparison operator in front of the value:

Operator Meaning
> greater than
>= greater than or equal to
< less than
<= less than or equal to
/search/blog?published=>=2026-01-01                  published on or after 1 January 2026
/search/blog?published=>=2026-01-01,<2026-02-01      published in January 2026
/search/blog?published=>=now-1M                      published in the last month (Elasticsearch date math)
/search/main?fields.age=>=18                         "age" area of 18 or more (a number input type)
/search/main?fields.age=>=18,<65                     18 ≤ age < 65

A lower and an upper bound on the same field are joined with a comma and describe one band — on a text field the same comma separates alternative values. The operator may also be written as the array key: published[gte]=2026-01-01&published[lt]=2026-02-01 is the same request as the second example. An exact value and a bound on the same field intersect: fields.age=>=18,21 matches only records whose age is exactly 21.

Range filters need the field to be a number or a date. An age area indexed from a text or select input type is a text field, and fields.age=>=18 would then look for the literal value >=18 — re-index it from a number input type to filter it by range.

A date without a time zone is read in the site's time zone — a bare 2026-01-01 is midnight at the site — and compared with the stored UTC value correctly. A value that carries its own offset (2026-01-01T10:00:00+02:00) keeps it. Elasticsearch date math (now, now-7d, now-1M/d) is passed through untouched.

On a text field an operator has no meaning: ?tags.name=>coffee filters by the literal value >coffee.

The complete rules — mixing exact values with bounds, repeating a bound, what an unreadable value does, and the pitfalls of mixing the two spellings — are on the Query Parameter Syntax page.

Sort fields

You can provide fields the result will be sorted by. When you provide nothing, the default sorting is applied. Usually, results are sorted relatively (_score DESC).

Sort can be (un)changeable. If it is, you can set up sort fields manually. Use orderBy parameter for that. Every filter field is sortable — text fields through their .keyword copy, numbers and dates directly — plus _score. Unsupportable fields are ignored.

You can separate fields by comma, or use array syntax, or combine them. The direction follows the field name after a colon (id:desc); in the array-key form the key is the field and the value is the direction (orderBy[id]=desc).

Specifying q=coffee&orderBy[name]=&orderBy[]=entityClass,id:desc&orderBy[]=_score you will have results sorted by name ASC, entity ASC, id DESC, _score DESC. Notice when you don't specify an ordering, it is ASC by default. Only for _score the DESC is the default value. Note _score is used for relative sorting.

Dates and numbers sort as such — orderBy=published:desc lists the most recently published records first.

Highlight

You can select which fields will be returned with highlighted search results. You can use highlightFields parameter for that. Separate values by comma, pass them as an array or use combined syntax. It can be (un)changeable.

If you have q=coffee&highlightFields[]=name, and there is a result with "Coffee Roaster" in the name, its highlighted value will be "<em>Coffee</em> Roaster".

Use q=coffee&highlightFields=tags.name,tags.slug to have tag names and slugs highlighted. Combined variant is q=coffee&highlightFields[]=tags.name,tags.slug&highlightFields[]=name, which additionally highlights name as well.

Searching

To search, use different ways to get search results:

  • /search/{ViewSlug}?q=phrase in frontend
  • Same in API
  • Use $_search->search({ViewSlug}, $parameters) call within the template.

The search response

Every search — frontend, API, or $_search->search() from a template — returns a SearchResponse, serialised as:

{
  "totalCount": 42,
  "resultItems": [ ... ],
  "facets": []
}
  • totalCount — total number of documents matching the query across all pages, not just the current page.
  • resultItems — the matched records for the requested page, each in the shape described below.
  • facets — always empty for the master index; facets are produced only by the facets index. It is present so the master and facets responses share one shape.

(The requested offset and page size are not part of the serialised response.)

ResultItem shape

Each entry in resultItems is the raw Elasticsearch hit: three wrapper keys plus the indexed document (_source) spread onto the same level.

{
  "_id": "pages.entities.page.123",
  "_score": 3.14,
  "highlight": {
    "name": ["<em>Coffee</em> Roaster"]
  },
  "entityClass": "Pages\\Entities\\Page",
  "id": 123,
  "name": "Coffee Roaster",
  "fullPath": "/coffee/coffee-roaster",
  "language": { "code": "en" },
  "created": "2026-05-02T08:14:31+00:00",
  "published": "2026-05-03T00:00:00+00:00",
  "fields": { "age": "18+" }
}
  • _id — the Elasticsearch document id: the lower-cased, dot-joined entity class plus the record id (e.g. pages.entities.page.123, blogs.entities.post.456). This is not the entity id — the entity id is the id field.
  • _score — the relevance score for this hit (higher = more relevant); null when the query doesn't score (e.g. no q phrase).
  • highlight — highlighted snippets for the fields requested via highlightFields; each field maps to an array of HTML fragments with <em>…</em> around the match. null when nothing was highlighted.
  • everything else — the indexed document (_source): every field from Core fields set the record has, plus the fields object of your indexed layout-area values. language.code, author.displayName, tags, categories, and relatedRecords appear as nested objects/arrays exactly as indexed.

Reading a result item in a template

In a template you don't touch the raw array — each result item is an object with typed accessor methods. Field arguments use dot notation for nested values (language.code, author.displayName, tags.name, fields.age) and must point at a leaf value — use getTexts('tags.name'), not getTexts('tags'). Every reader returns null (or an empty result) when the field is missing, so they're safe to call unconditionally.

Method Returns Description
getId() string The Elasticsearch document _id (e.g. pages.entities.page.123). Not the entity id — use getInteger('id') for that.
getScore() float \| null The relevance _score of the hit; null when the query doesn't score.
getRecord() Model \| null Loads the full framework model behind the hit (Page, Post, ProductDescription, …), resolved from entityClass + id in the current abstraction. null when the underlying record no longer exists or can't be loaded. Consider hiding items whose record is null.
getRelatedRecords() ModelsCollection The records referenced by the relatedRecords field, as one collection. Empty when there are none or they can't be loaded.
getText($field) string \| null Value at $field as a string; a multi-valued field is joined with ", ".
getTexts($field) string[] \| null Value at $field as an array of strings (use for multi-valued fields such as tags.name).
getInteger($field) int \| null Value at $field as an integer (first element if the field is multi-valued).
getIntegers($field) int[] \| null Value at $field as an array of integers.
getFloat($field) float \| null Value at $field as a float (first element if multi-valued).
getFloats($field) float[] \| null Value at $field as an array of floats.
getDate($field) \DateTime \| null Value at $field as a \DateTime in the site timezone (first element if multi-valued); null if absent or unparseable.
getHighlightText($field) string \| null The highlighted (<em>…</em>) string for $field, falling back to getText($field) when the field wasn't highlighted.
getHighlightTexts($field) string[] \| null The highlighted strings for $field, falling back to getTexts($field).
getHighlight() array \| null The whole highlight map (each highlighted field → its array of snippets), or null.
{foreach $response->getResultItems() as $item}
    {$record = $item->getRecord()}
    {$published = $item->getDate('published')}
    {if $record}
        <a href="{$item->getText('fullPath')}">{$item->getHighlightText('name')}</a>
        {if $published}<time>{$published->format('j F Y')}</time>{/if}
    {/if}
{/foreach}

Example usage

Suppose you want to search for a product with the name "Coffee Beans", which has a tag "organic" and is created by the author with the name "John". You can use the following query:

/search/products?q=Coffee Beans&tags.name=organic&author.displayName=John

This will return all products that match the search criteria.

Suppose you want to search for a blog post that contains the phrase "coffee recipe" and was published in the last month. Use a range filter on the published date:

/search/blog?q=coffee recipe&published=>=now-1M

This will return all blog posts that match the phrase and were published within the last month. Without an operator a date filters by exact value — created=2026-12-12 matches the records created on that day (in UTC) — so a "since" or "between" question is always a range.

Conclusion

In this markup, we have discussed the functionality of the search engine, the entities it supports, the core fields set that can be searched, layout area fields that can be indexed, the indexing process, views, pagination, full-text search, search fields, filter fields including range filters on numbers and dates, sort fields, highlight, and searching. By understanding these concepts, you can use the search engine to its full potential and get the search results that you need.