Организация поиска по сайту: запросы
Feb 23, 2018 12:08 · 1736 words · 9 minute read
В предыдущей статье мы разобрали основы Elasticsearch: индексы, маппинги и анализаторы. В этой же я хочу углубиться в тему построения запросов. У Elasticsearch свой собственный DSL, который может показаться непривычным, но по своей гибкости не уступает всем знакомому SQL. Набор данных, над которым я буду приводить примеры, можно взять здесь:
Набор данных
POST /twitter/tweet/_bulk
{"index": {"_id": 1}}
{"subject": "Первая запись в твиттер", "geotag": "Краснодар", "hashtags": ["твит", "новости", "Краснодар"], "published": "2014-09-12T20:44:42+00:00"}
{"index": {"_id": 2}}
{"subject": "Первая запись в Геленджике", "geotag": "Геленджик", "hashtags": ["твит", "море", "Геленджик"], "published": "2014-09-13T20:44:42+00:00"}
{"index": {"_id": 3}}
{"subject": "Солнечный Краснодар", "geotag": "Краснодар", "hashtags": ["Краснодар"], "published": "2014-09-11T20:44:42+00:00"}
{"index": {"_id": 4}}
{"subject": "Солнечный Геленджик", "geotag": "Геленджик", "hashtags": ["Геленджик"], "published": "2014-09-11T10:44:42+00:00"}
PUT /twitter/retweet/1?pretty
{"source": "https://twitter.com/3564123", "subject": "Солнечный Геленджик", "geotag": "Геленджик", "hashtags": ["Геленджик"], "published": "2014-09-11T10:44:42+00:00"}
Поисковые запросы к Elasticsearch могут быть представлены в 2х видах:
- упрощённый ‘lite’ -
GET /twitter/_search?q=hashtags:sea
- полный с телом запроса -
GET /twitter/_search {"query": {<куча условий>}}
Строка поиска
Рассмотрим первый вариант. На самом деле это наследие от поискового движка Lucene, на котором построен Elasticsearch. Сразу обращаю внимание, что он поддерживает только unicode encoded строку, например такую:
GET /twitter/_search?q=\u0437\u0430\u043F\u0438\u0441\u044C
Ответ
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0.6650044,
"hits": [
{
"_index": "twitter",
"_type": "tweet",
"_id": "1",
"_score": 0.6650044,
"_source": {
"subject": "Первая запись в твиттер",
"geotag": "Краснодар",
"hashtags": [
"твит",
"новости",
"Краснодар"
],
"published": "2014-09-12T20:44:42+00:00"
}
},
{
"_index": "twitter",
"_type": "tweet",
"_id": "2",
"_score": 0.6333549,
"_source": {
"subject": "Первая запись в Геленджике",
"geotag": "Геленджик",
"hashtags": [
"твит",
"море",
"Геленджик"
],
"published": "2014-09-13T20:44:42+00:00"
}
}
]
}
}
Заполнение данными
POST /games/strategy/_bulk
{"index": {"_id": 1}}
{"name": "Civilization V", "release": "2010-09-21T00:00:00+00:00"}
{"index": {"_id": 2}}
{"name": "XCOM 2", "release": "2016-02-05T00:00:00+00:00"}
{"index": {"_id": 3}}
{"name": "Civilization VI", "release": "2016-10-21T00:00:00+00:00"}
GET /games/_search?q=name:Civilization
Ответ
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0.25811607,
"hits": [
{
"_index": "games",
"_type": "strategy",
"_id": "1",
"_score": 0.25811607,
"_source": {
"name": "Civilization V",
"release": "2010-09-21T00:00:00+00:00"
}
},
{
"_index": "games",
"_type": "strategy",
"_id": "3",
"_score": 0.25811607,
"_source": {
"name": "Civilization VI",
"release": "2016-10-21T00:00:00+00:00"
}
}
]
}
}
+
для объединения условий поAND
-
для исключения""
для точного совпадения?
или*
для нечёткого поиска~
для поиска близких по расстоянию Дамеру-Левенштайна (например,name:XROM~1
всё равно найдёт “XCOM”)
Например, GET /games/_search?q=+name:(Civilization XCOM)
выберет все записи, у которых в поле name есть слова “Civilization” или “XCOM”. Для исключения слишком древних игр добавим фильтр по дате: GET /games/_search?q=+name:(Civilization XCOM) -release:<2016-01-01
.
Напомню, что запросы должны быть закодированы, так что последний превратится в мало читаемый GET /games/_search?q=+name%3A%28Civilization%20XCOM%29%20+date%3A%3C2016-01-01
. Пожалуй, это достаточная причина, чтобы познакомиться ближе с полной версией.
Полный формат запросов
Все поисковые запросы начинаются одинаково:
GET /twitter/_search
{
"query": {
<тип>: {
<параметр1>: <значение1>,
...
<параметрN>: <значениеN>,
}
}
}
Тип запроса определяет как именно разбирать запрос: по каким полям искать, как именно, а также как ранжировать результаты (ведь Elasticsearch это не только про поиск результатов, но и про показ наиболее релевантных!). Помимо текстового поиска существуют также специальные запросы на сравнение дат, геопозиций, регулярных выражений и пр. Большая часть из них будет рассмотрена ниже.
match и multi_match
Эта пара типов, пожалуй, самая частоиспользуемая для поиска текста. В самом простом случае при поиске слова в определённом поле запрос будет следующим:
GET /twitter/_search
{
"query": {
"match": {
"hashtags": "море"
}
}
}
Ответ
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.49191087,
"hits": [
{
"_index": "twitter",
"_type": "tweet",
"_id": "2",
"_score": 0.49191087,
"_source": {
"subject": "Первая запись в Геленджике",
"geotag": "Геленджик",
"hashtags": [
"твит",
"море",
"Геленджик"
],
"published": "2014-09-13T20:44:42+00:00"
}
}
]
}
}
GET /twitter/_search
{
"query": {
"multi_match" : {
"query": "Геленджик",
"fields": ["hashtags^3", "subject"],
"type": "best_fields"
}
}
}
Обратите внимание на ^3
в запросе в перечислении полей - так указываются веса (boost
) для ранжирования результатов поиска (поле _score
в ответе). К этому мы ещё вернёмся в следующей статье, когда будем разбирать такие понятия как dis_max
и tie_breaker
.
По умолчанию multi_match
использует тип best_fields
- строка запроса будет искаться в каждом поле отдельно, и результат _score
будет максимумом по всем полям. Если же поиск идёт по одному и тому же полю, но с использованием разных анализаторов, то возможно имеет смысл применить тип most_fields
- в таком случае _score
будет средним арифметическим между полями. То есть запрос
GET /twitter/_search
{
"query": {
"multi_match" : {
"query": "Геленджик",
"fields": ["subject", "subject.ru", "subject.autocomplete"],
"type": "most_fields"
}
}
}
будет выполняться как
GET /twitter/_search
{
"query": {
"bool": {
"should": [
{"match": {"subject": "Геленджик"}},
{"match": {"subject.ru": "Геленджик"}},
{"match": {"subject.autocomplete": "Геленджик"}}
]
}
}
}
И, наконец, поиск каждого слова в запросе по каждому полю - cross_fields
. Причём, для включения в результат достаточно, чтобы каждая часть запроса была найдена хотя бы в одном поле.
GET /twitter/_search
{
"query": {
"multi_match" : {
"query": "море в Геленджике",
"fields": ["subject", "hashtags"],
"type": "cross_fields"
}
}
}
Ответ
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.7118499,
"hits": [
{
"_index": "twitter",
"_type": "tweet",
"_id": "2",
"_score": 1.7118499,
"_source": {
"subject": "Первая запись в Геленджике",
"geotag": "Геленджик",
"hashtags": [
"твит",
"море",
"Геленджик"
],
"published": "2014-09-13T20:44:42+00:00"
}
},
{
"_index": "twitter",
"_type": "tweet",
"_id": "1",
"_score": 0.6099695,
"_source": {
"subject": "Первая запись в твиттер",
"geotag": "Краснодар",
"hashtags": [
"твит",
"новости",
"Краснодар"
],
"published": "2014-09-12T20:44:42+00:00"
}
}
]
}
}
_score
в этом случае является суммой всех _score
в отдельности. То есть для tweet с id 2 это будет ~1.7117:
- ~0.6099 для вхождения “в” в “subject”
- ~0.4919 для вхождения “море” в “hashtags”
- ~0.6099 для вхождения “Геленджике” в “subject”
Подводя итоги:
best_fields
- учитывается самый лучший результат по каждому полюmost_fields
-_score
является средним арифметическим по_score
каждого поляcross_fields
-_score
вычисляется как сумма_score
по каждому полю для каждого слова в запросе
term и match_phrase
Наряду с match
есть ещё условие term
, которое предотвращает какой-либо анализ содержимого запроса. Это полезно, когда ищется точное соответствие. В случае поиска по hashtags это никак не повлияет на результат. Сходным образом работает и match_phrase
, который ищет последовательность слов, входящих в точно таком же порядке. Ниже пример для строки строки “запись в”, а вот поиск “в запись” уже ни к чему не приведёт, т.к. слова поменяны местами.
GET /twitter/_search
{
"query": {
"match_phrase": {
"subject": "запись в"
}
}
}
Ответ
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.219939,
"hits": [
{
"_index": "twitter",
"_type": "tweet",
"_id": "2",
"_score": 1.219939,
"_source": {
"subject": "Первая запись в Геленджике",
"geotag": "Геленджик",
"hashtags": [
"твит",
"море",
"Геленджик"
],
"published": "2014-09-13T20:44:42+00:00"
}
},
{
"_index": "twitter",
"_type": "tweet",
"_id": "1",
"_score": 1.219939,
"_source": {
"subject": "Первая запись в твиттер",
"geotag": "Краснодар",
"hashtags": [
"твит",
"новости",
"Краснодар"
],
"published": "2014-09-12T20:44:42+00:00"
}
}
]
}
}
simple_query_string и query_string
GET /twitter/_search
{
"query": {
"simple_query_string" : {
"query": "+(твит | Геленджик) -море",
"fields": ["hashtags^3", "subject"],
"default_operator": "and"
}
}
}
Ответ
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 4.3515453,
"hits": [
{
"_index": "twitter",
"_type": "retweet",
"_id": "1",
"_score": 4.3515453,
"_source": {
"source": "https://twitter.com/3564123",
"subject": "Солнечный Геленджик",
"geotag": "Геленджик",
"hashtags": [
"Геленджик"
],
"published": "2014-09-11T10:44:42+00:00"
}
},
{
"_index": "twitter",
"_type": "tweet",
"_id": "1",
"_score": 2.4757328,
"_source": {
"subject": "Первая запись в твиттер",
"geotag": "Краснодар",
"hashtags": [
"твит",
"новости",
"Краснодар"
],
"published": "2014-09-12T20:44:42+00:00"
}
},
{
"_index": "twitter",
"_type": "tweet",
"_id": "4",
"_score": 2.4250035,
"_source": {
"subject": "Солнечный Геленджик",
"geotag": "Геленджик",
"hashtags": [
"Геленджик"
],
"published": "2014-09-11T10:44:42+00:00"
}
}
]
}
}```
simple_query_string
и query_string
- они схожи по синтаксису с lite версией запроса, но имеют большее число настроек. Нашлись все записи, у которых в полях “hashtags” или “subject” есть упоминание слов “твит” или “Геленджик”, но не должно быть упоминания слова “море”.
ids, range, terms, exists, etc…
Наряду с рассмотренным выше term есть ещё некоторое количество запросов, которые не анализируют (морфологически) искомое выражение. Все они описаны в документации, а здесь рассмотрены лишь самые распространённые. Например, поиск по id документов:
GET /twitter/tweet/_search
{
"query": {
"ids" : {
"values" : ["1", "4"]
}
}
}
Покажет соответственно всего 2 объекта. Поиск по дате:
GET /twitter/tweet/_search
{
"query": {
"range": {
"published": {
"gte": "13.09.2014",
"lt": "now-1d/d",
"format": "dd.MM.yyyy"
}
}
}
}
выберет все твиты, которые были опубликованы с 13 сентября 2014 и по вчерашний день. Параметер “format” не обязателен, по умолчанию ISO 8601.
bool
Куда ж без объединения условий? Для этого существует специальный запрос, в котором собраны все булевые операции:
GET /twitter/_search
{
"query": {
"bool": {
"must": {
"match": {"subject": "запись"}
},
"filter": {
"match": {"geotag": "Краснодар"}
},
"must_not": {
"range": {"published": {"lte": "2010-01-01"}}
},
"should": [
{"term": {"hashtags": "море"}},
{"term": {"hashtags": "твит"}}
],
"minimum_should_match" : 1
}
}
}
Ответ
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1.1018803,
"hits": [
{
"_index": "twitter",
"_type": "tweet",
"_id": "1",
"_score": 1.1018803,
"_source": {
"subject": "Первая запись в твиттер",
"geotag": "Краснодар",
"hashtags": [
"твит",
"новости",
"Краснодар"
],
"published": "2014-09-12T20:44:42+00:00"
}
}
]
}
}
must
- объединение по AND - должно выполняться во всех найденных объектах (и будет участвовать в подсчёте_score
)filter
- аналогичноmust
, но не будет участвовать в подсчёте_score
must_not
- отрицаниеshould
- объеддинение по OR, причём с помощью параметраminimum_should_match
можно указать какое минимальное количество условий должно выполняться. Если в запросе выше поставить 2, то результатов уже не будет.
Заключение
Данная статья не претендует на полноту, за этим лучше обратиться к документации. В частности, не описаны запросы по геопозиции, со вложенными условиями, параметризованные, а также написанные на встроенном языке painless. Они настолько специфичны, что могут никогда вам и не понадобиться, так что я решил опустить их перечисление