Поиск в DC CMS

В этой статье вы узнаете об использовании сервиса поиска с использованием CMS Engine, а также о внедрении фасетного поиска и поисковых подсказок.

Чтобы выполнить запросы к контенту, используйте клиент, предоставляемый CMS Engine (имя бина (bean) searchClient), доступным из любого Groovy-скрипта.

Для создания запросов существует два подхода в зависимости от сложности запроса: Query DSL и Query Builders.

Query DSL

Следуя структуре, используемой ElasticSearch для REST API, этот метод идеален для простых или постоянных запросов, требующих минимальной конфигурации.

Поисковый запрос с использованием DSL:

// No imports are required for this method

// Execute the query using inline builders
def searchResponse = searchClient.search(r -> r
  .query(q -> q
    .bool(b -> b
      .should(s -> s
        .match(m -> m
          .field('content-type')
          .query(v -> v
            .stringValue('/component/article')
          )
        )
      )
      .should(s -> s
        .match(m -> m
          .field('author')
          .query(v -> v
            .stringValue('My User')
          )
        )
      )
    )
  )
, Map)

def itemsFound = searchResponse.hits().total().value()
def items = searchResponse.hits().hits()*.source()

return items

Copy-icon

Query Builders

Используйте все классы, доступные в официальном пакете клиента ElasticSearch, чтобы создавать запросы. Этот метод позволяет использовать объекты-строители (builder objects) для разработки сложной логики конструирования запросов.

Поисковый запрос с использованием объектов-строителей:

// Import the required classes
import org.elasticsearch.client.elasticsearch.core.SearchRequest

def queryStatement = 'content-type:"/component/article" AND author:"My User"'

// Use the appropriate builders according to your query
def builder = new SearchRequest.Builder()
    .query(q -> q
      .queryString(s -> s
        .query(queryStatement)
      )
    )

// Perform any additional changes to the builder, for example add pagination if required
if (pagination) {
  builder
    .from(pagination.offset)
    .size(pagination.limit)
}

// Execute the query
def searchResponse = searchClient.search(builder.build(), Map)

def itemsFound = searchResponse.hits().total().value()
def items = searchResponse.hits().hits()*.source()

return items

Copy-icon

Реализация фасетного поиска

Фасетный поиск можно осуществить при помощи агрегаций, чтобы предоставить пользователям возможность уточнить результаты поиска на основе одного или нескольких полей.

Функционал поиска предоставляет ряд агрегаций, настроенных под характеристики полей в вашей модели или требования интерфейса пользователя для отображения данных.

В этой статье мы будем использовать базовую агрегацию terms для создания фасетного поиска, основанного на категоризации статей блога.

Изображение статьи

Первый шаг включает в себя определение полей для агрегации. В данном случае модель страницы “Статья” включает в себя поле “Categories”, значения которого извлекаются из таксономии сайта через источник данных. Поэтому название поля в индексе - categories.item.value_smv.

Изображение статьи
Изображение статьи

Для создания фасетного поиска необходимо выполнить следующие шаги:

  1. Интегрировать соответствующие агрегации в запрос поиска.
  2. Обработать агрегации, полученные из ответа поиска.
  3. Отобразить фасеты на странице результатов поиска.

Добавление агрегаций в запрос поиска

Для включения агрегаций в запрос используйте ключ aggs. У каждой агрегации должно быть свое уникальное имя в качестве ключа и конфигурации, основанные на её типе.

Поисковый запрос с использованием агрегаций:

def result = searchClient.search(r -> r
  .query(q -> q
    .queryString(s -> s
      .query(q as String)
    )
  )
  .from(start)
  .size(rows)
  .aggregations('categories', a -> a
    .terms(t -> t
    .field(categories.item.value_smv)
    .minDocCount(1)
    )
  )
, Map)

Copy-icon

В пример выше включена агрегация типа terms с именем categories, которая вернет все найденные значения для поля categories.item.value_smv, которым присвоена хотя бы 1 статья.

Обработка агрегаций в ответе поиска

Ответ на поисковый запрос будет предоставлять агрегации в поле aggregations. Содержимое каждой агрегации изменяется в зависимости от её типа.

Поисковый ответ с использованием агрегаций:

def facets = [:]
if(result.aggregations()) {
  result.aggregations().each { name, agg ->
    facets[name] = agg.sterms().buckets().array().collect{ [ value: it.key(), count: it.docCount() ] }
  }
}

Copy-icon

В примере выше агрегации извлекаются из объекта ответа в простую карту. Этот пример предполагает, что все агрегации являются типа terms и извлекают ключ и docCount для каждого идентифицированного значения (называемого "buckets" в Search).

Запрос на все существующие статьи может выглядеть примерно так:

"facets":{
  "categories":[
    { "value":"Entertainment", "count":3 },
    { "value":"Health", "count":3 },
    { "value":"Style", "count":1 },
    { "value":"Technology", "count":1 }
  ]
}

Copy-icon

Если повторно запустить запрос с фильтром по категории "Развлечения", он вернет ровно три статьи. Последующие запросы будут генерировать новые фасеты на основе этих статей. Этот метод позволяет пользователям эффективно уточнять результаты и находить более релевантные данные с минимальными усилиями.

Отображение фасетов на страницах результатов поиска

Отображение фасетов зависит от используемой технологии представления информации. Это может быть реализовано с использованием Freemarker или Single Page Application (SPA) с такими фреймворками, как Angular, React или Vue. В качестве примера мы будем использовать шаблоны Handlebars, которые будут отображаться с помощью jQuery.

Шаблоны страниц результатов поиска:

<script id="search-facets-template" type="text/x-handlebars-template">
  {{#if facets}}
    <div class="row uniform">
      {{#each facets}}
        <div class="3u 6u(medium) 12u$(small)">
          <input type="checkbox" id="{{value}}" name="{{value}}" value="{{value}}">
          <label for="{{value}}">{{value}} ({{count}})</label>
        </div>
      {{/each}}
    </div>
  {{/if}}
</script>

<script id="search-results-template" type="text/x-handlebars-template">
{{#each articles}}
  <div>
    <h4><a href="{{url}}">{{title}}</a></h4>
    {{#if highlight}}
      <p>{{{highlight}}}</p>
    {{/if}}
  </div>
  {{else}}
  <p>No results found</p>
{{/each}}
</script>

Copy-icon

Мы используем шаблоны для отображения результатов после выполнения поиска:

$.get("/api/search.json", params).done(function(data) {
  if (data == null) {
    data = {};
  }
  $('#search-facets').html(facetsTemplate({ facets: data.facets.categories }));
  $('#search-results').html(articlesTemplate(data));
});

Copy-icon

Последний шаг включает в себя запуск нового поиска, когда пользователь выбирает одно из значений в фасетах:

$('#search-facets').on('click', 'input', function() {
var categories = [];
$('#search-facets input:checked').each(function() {
categories.push($(this).val());
});

doSearch(queryParam, categories);
});

Copy-icon

Мультииндексный запрос

DC CMS поддерживает запрос нескольких поисковых индексов в одном запросе.

Для поиска как на вашем сайте, так и в дополнительных индексах отправьте запрос на поиск, включив в него список индексов или алиасов (указателей на индекс), разделенных запятой. Это действие позволит исследовать ваш сайт вместе с указанными индексами.

Важно отметить, что все остальные индексы или алиасы, в которых осуществляется поиск, должны быть предварительно добавлены к имени сайта в формате SITENAME_{external-index-name}. Однако при отправке запроса префикс SITENAME_ следует исключить из других индексов или алиасов.

DC CMS поддерживает различные параметры запросов поиска, включая:

  • indices_boost
  • search_type
  • allow_no_indices
  • expand_wildcards
  • ignore_throttled
  • ignore_unavailable

Внедрение поисковых подсказок (Type-ahead service)

В этом разделе мы рассмотрим, как использовать запрос для предоставления предложений-подсказок по мере ввода данных пользователем.

Создание сервиса

Разработайте REST-сервис, который возвращает предложения-подсказки, основанные на контенте вашего сайта.

Требования:

  • Сервис использует текущий поисковый запрос пользователя и находит похожий контент
  • Сервис возвращает результаты в виде списка строк

Чтобы создать REST-эндпоинт, разместите указанный файл Groovy в вашей папке со скриптами:

import ru.dc.cms.sites.editorial.SuggestionHelper

// Obtain the text from the request parameters
def term = params.term

def helper = new SuggestionHelper(searchClient)

// Execute the query and process the results
return helper.getSuggestions(term)

Copy-icon

Вам также нужно создать вспомогательный класс в папке /scripts:

package ru.dc.cms.sites.editorial

import org.elasticsearch.client.elasticsearch.core.SearchRequest
import ru.dc.cms.search.elasticsearch.client.ElasticSearchClientWrapper

class SuggestionHelper {

    static final String DEFAULT_CONTENT_TYPE_QUERY = "content-type:\"/page/article\""
    static final String DEFAULT_SEARCH_FIELD = "subject_t"

    ElasticSearchClientWrapper searchClient

    String contentTypeQuery = DEFAULT_CONTENT_TYPE_QUERY
    String searchField = DEFAULT_SEARCH_FIELD

    SuggestionHelper(searchClient) {
        this.searchClient = searchClient
    }

    def getSuggestions(String term) {
        def queryStr = "${contentTypeQuery} AND ${searchField}:*${term}*"
        def result = searchClient.search(SearchRequest.of(r -> r
            .query(q -> q
                .queryString(s -> s
                    .query(queryStr)
                )
            )
        ), Map)

              return process(result)
        }

    def process(result) {
            def processed = result.hits.hits*.getSourceAsMap().collect { doc ->
                    doc[searchField]
            }
            return processed
    }
}

Copy-icon

После создания этих файлов и перезагрузки контекста сайта вы можете протестировать REST-эндпоинт из браузера.

Создание пользовательский интерфейса

Создайте интерфейс с использованием HTML, JavaScript и AJAX.

Требования:

  • отправка запроса на сервер для мгновенных результатов по мере ввода пользователем
  • отображение результатов и предложение вариантов на основе потенциального запроса пользователя
  • для оптимизации производительности не запускать запрос при каждом нажатии клавиши; вместо этого накапливать нажатия клавиш пользователем и отправлять запрос, когда размер пакета (batch size) достигнут или когда пользователь прекращает ввод

Вы также можете интегрировать существующую библиотеку или фреймворк, предоставляющий компонент автозаполнения. Например, для использования компонента автозаполнения jQuery UI Autocomplete необходимо указать соответствующий REST-эндпоинт в конфигурации.

$('#search').autocomplete({
  // Wait for at least this many characters to send the request
  minLength: 2,
  source: '/api/1/services/suggestions.json',
  // Once the user selects a suggestion from the list, redirect to the results page
  select: function(evt, ui) {
    window.location.replace("/search-results?q=" + ui.item.value);
  }
});

Copy-icon