- PVSM.RU - https://www.pvsm.ru -
Как сделать фасетный поиск в интернет-магазине? Как формируются значения в фильтрах фасетного поиска? Как выбор значения в фильтре влияет на значения в соседних фильтрах? В поиске ответов дошел до пятой страницы поисковой выдачи Google. Исчерпывающей информации не нашел, пришлось разобраться самому. Статья описывает:
Здесь нет готовых решений. Скопировать и вставить не получится. Для решения собственной задачи придется вникнуть.

Полнотекстовый поиск — поиск товаров по слову или фразе. Для пользователя — это поле для ввода текста с кнопкой «Найти», которое доступно на любой странице сайта.
Фасетный поиск — поиск товара по нескольким характеристикам: цвету, размеру, объему памяти, цене и т.п. Для пользователя — это набор фильтров. Каждый фильтр связан только с одной характеристикой и наоборот. Значения фильтра — все возможные значения характеристики. Пользователь видит фильтры на странице раздела, категории, на странице с результатами полнотекстового поиска. Когда пользователь выбрал значение, фильтр считается активным.
Коротко звучит так: фильтр фильтрует товары и фильтрует варианты выбора в других фильтрах.
С этим просто. Пользователь выбрал:
В терминах булевой алгебры: между фильтрами по действует логическое «И», между значениями в фильтре логическое «ИЛИ». Простая логика.
«Ну… какие варианты есть — отображается, чего нет — скрывается» — примерно так бизнес описывает поведение фильтров. Звучит логично. На практике это работает так:
Количество значений фильтра зависит от количества товаров: чем больше товаров с разным значением характеристики, тем больше значений в фильтре. Пользователь сократил количество товаров в выборке для остальных фильтров, когда выбрал бренд. Это привело к обновлению списков значений.
Отсюда вытекает универсальное правило: значения фильтра извлекаются из выборки товаров, которая сформирована остальными активными фильтрами.
Каждый активный фильтр имеет свою выборку товаров.
Если у нас N фильтров и:
В итоге, когда пользователь выбирает значение фасетного фильтра, происходит следующее:
Возникает вопрос — как это реализовать на практике?
Характеристики товара не универсальны, поэтому вы не найдете здесь готовую структуру индекса для хранения товаров или готовых запросов. Вместо этого будут ссылки на документацию с объяснениями, как самостоятельно построить «правильные» индексы и запросы. «Правильные» — на основе моего опыта и знаний.
В ES нас интересует 2 типа данных:
ES анализирует значения в поле с типом text [1] и формирует словарь для полнотекстового поиска. Значения в поле с типом keyword [2] индексируются в том виде, в котором получены. Агрегация и сортировка доступна только для полей с типом keyword.
Пользователь использует характеристики в обоих случаях: в полнотекстовом поиске и через фильтры. ES не позволяет назначить 2 типа одному полю, но предлагает другие решения:
fields [3]
PUT my_index
{
«mappings»: {
«properties»: {
«some_property»: {
«type»: «text», // 1
«fields»: { // 2
«raw»: {
«type»: «keyword»
}
}
}
}
}
}
Так для каждой характеристики.
В запросах для операций точного сравнения, сортировки и агрегации нужно использовать дочернее виртуальное поле типа keyword [2]. В примере это some_property.raw. Для поиска по тексту — родительское.
copy_to [4].
PUT my_index
{
«mappings»: {
«properties»: {
«all_properties»: { // 1
«type»: «text»
}, «some_property_1»: {
«type»: «keyword»,
«copy_to»: «all_properties» // 2
},
«some_property_2»: {
«type»: «keyword»,
«copy_to»: «all_properties»
}
}
}
Для операций точного сравнения, сортировки и агрегации нужно использовать поле характеристики, для поиска по тексту — поле со значениями всех характеристик.
Оба подхода создают в индексе дополнительные поля, которые отсутствуют в исходной структуре документа. Поэтому для создания запроса нужно знать структуру индекса.
Я предпочитаю вариант с copy_to [4]. Тогда для построения запроса полнотекстового поиска достаточно знать одно поле с копией значений всех характеристик.
Будем считать, что структура индекса как в варианте с copy_to [4]. Для полнотекстового поиска в ES используется конструкция match [5], для сравнения со значениями фасетных фильтров — terms query [6]. boolean query [7] объединяет конструкции в один запрос. Он будет примерно таким:
{
«query» : {
«bool»: {
«must»: {
«match»: {
«virtual_field_for_fulltext_searching»: {
«query»: «some text»
}
}
},
«filter»: {
«must»: [
{«property_1»: [ «value_1_1», …, «value_1_n»]},
…
{«property_n»: [ «value_n_1», …, «value_n_m»]}
]
}
}
}
}
query.bool.must.match основной запрос на полнотекстовый поиск
query.bool.filter фильтры для уточнения основного запроса. must внутри означает логическое «и» между фильтрами. Массив значений в каждом фильтре — логическое «или».
Конструкция terms aggregation [8] группирует товары по значениям характеристики и вычисляет количество в каждой группе. Такая операция называется агрегация. Сложность в том, что для каждого активного фильтра terms aggregation [8] должна выполнится на выборке товаров, сформированной другими активными фильтрами. Для не активных фильтров — на выборке совпадающей с поисковой выдачей. Конструкция filter aggregation [9] позволяет сформировать для каждой агрегации отдельную выборку и «упаковать» операции в один запрос.
Структура запроса будет такой:
{
«size»: 0,
«query» : {
«bool»: {
«must»: {
«match»: {
«field_for_fulltext_searching»: {
«fuzziness»: 2,
«query»: «some text»
}
}
},
«filter»: {
}
}
},
«aggs» : {
«inavtive_filter_agg» : {
«filter» : { …
},
«aggs»: {
«some_inavtive_filter_subagg»: {
«terms» : {
«field» : «some_property»
}
},
...
«some_other_inavtive_filter_subagg»: {
«terms» : {
«field» : «some_other_property»
}
}
}
},
«active_filter_1_agg» : {
«filter»: {
… },
«aggs»: {
«active_filter_1_subagg»: {
«terms» : {
«field»: «property_1»
}
}
}
},
…,
«active_filter_N_agg» : {
«filter»: {
…
},
«aggs»: {
«active_filter_N_subagg»: {
«terms» : {
«field»: «property_N»
}
}
}
}
}
}
query.bool — основной запрос, операции фильтрации выполняются в его контексте. Он состоит из:
aggs.inavtive_filter_agg — агрегация для неактивных фасетных фильтров состоит из:
aggs.active_filter_1_agg — агрегация получения значений первого из активных фасетных фильтров. Каждая конструкция связана с одним фасетным фильтром. Состоит из:
Важно указать «size»: 0, иначе получите список товаров соответствующих основному запросу без агрегаций.
Получили два запроса:
Каждый запрос самодостаточен, поэтому лучше выполнять их асинхронно.
P.S. Допускаю, существуют более «правильные» подходы и инструменты для решения задачи фасетного поиска. Буду благодарен за дополнительную информацию и примеры в комментариях.
Автор: abratko
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/356483
Ссылки в тексте:
[1] text: https://www.elastic.co/guide/en/elasticsearch/reference/current/text.html
[2] keyword: https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html
[3] fields: https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html#multi-fields
[4] copy_to: https://www.elastic.co/guide/en/elasticsearch/reference/current/copy-to.html
[5] match: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
[6] terms query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html#query-dsl-terms-query
[7] boolean query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
[8] terms aggregation: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation
[9] filter aggregation: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html#search-aggregations-bucket-filter-aggregation
[10] Источник: https://habr.com/ru/post/517074/?utm_source=habrahabr&utm_medium=rss&utm_campaign=517074
Нажмите здесь для печати.