Освоение Elasticsearch Query DSL: Основные команды для извлечения данных
Раскройте возможности извлечения данных Elasticsearch, освоив Query DSL. Это руководство разбирает основные структуры JSON-запросов, уделяя внимание практическому использованию запросов `match`, `term` и range. Узнайте критическую разницу между условиями `must` (с оценкой релевантности) и `filter` (с кэшированием) в базовом запросе `bool`, что позволит эффективно строить сложные высокопроизводительные поиски данных.
Освоение Elasticsearch Query DSL: Основные команды для извлечения данных
Elasticsearch Query DSL — это язык JSON, который используется, когда простого поискового поля недостаточно. Он позволяет объединить полнотекстовый поиск, точные фильтры, диапазоны дат, сортировку, пагинацию и агрегации в одном запросе. Такая гибкость полезна, но также легко написать запрос, который вернет неправильные документы или будет работать нормально в тестировании, но замедлится в продакшене.
Лучший способ изучить Query DSL — держать в уме два вопроса: «Ищу ли я текст по релевантности?» и «Фильтрую ли я точные значения?» Большинство решений по выбору запроса вытекают из этого разделения.
Анатомия поискового запроса Elasticsearch
Все поиски в Elasticsearch выполняются через конечную точку _search конкретного индекса (или индексов). Базовый поисковый запрос — это POST-запрос, содержащий JSON-тело, определяющее параметры запроса. Наиболее важная часть этого тела — объект query.
Базовая структура:
POST /your_index_name/_search
{
"query": { ... Определите структуру запроса здесь ... },
"size": 10,
"from": 0
}
Основные типы запросов: Точность и релевантность
Query DSL предлагает широкий спектр запросов, адаптированных для разных типов данных и потребностей сопоставления. Выбор запроса существенно влияет как на оценку релевантности, так и на производительность.
1. Полнотекстовый поиск: Запрос match
Запрос match является стандартным для полнотекстового поиска по анализируемым полям. Он токенизирует поисковый термин и проверяет совпадение токенов в указанном поле (полях).
Сценарий использования: Поиск текста на естественном языке, где важна оценка релевантности.
Пример: Поиск документов, где поле 'description' содержит слово 'cloud' или 'computing'.
GET /products/_search
{
"query": {
"match": {
"description": "cloud computing"
}
}
}
2. Сопоставление точных значений: Запрос term
Запрос term ищет документы, содержащие точный указанный термин. В отличие от match, он не выполняет анализ поисковой строки, что делает его идеальным для точного сопоставления по ключевым словам, ID или числовым полям.
Сценарий использования: Фильтрация по точным значениям в неанализируемых полях (например, поля типа keyword или числа).
Пример: Получение продукта с точным ID SKU10021.
GET /products/_search
{
"query": {
"term": {
"product_id": "SKU10021"
}
}
}
3. Диапазонные запросы
Диапазонные запросы позволяют фильтровать документы, где значение поля попадает в указанный диапазон (числовой, дата или строка).
Синтаксис: Использует gt (больше), gte (больше или равно), lt (меньше) и lte (меньше или равно).
Пример: Поиск заказов, размещенных после 1 января 2024 года.
GET /orders/_search
{
"query": {
"range": {
"order_date": {
"gte": "2024-01-01",
"lt": "2025-01-01"
}
}
}
}
4. Фильтрация по наличию: Запрос exists
Запрос exists определяет документы, в которых присутствует определенное поле (т.е. не null и не отсутствует).
Пример: Поиск всех пользователей, указавших адрес электронной почты.
GET /users/_search
{
"query": {
"exists": {
"field": "email_address"
}
}
}
Построение сложной логики с помощью запроса bool
Практически для всех реальных поисковых приложений необходимо комбинировать несколько критериев. Запрос bool является основным инструментом для этого, позволяя объединять другие условия запроса с помощью булевой логики.
Условия внутри bool
Запрос bool принимает четыре основных условия:
must: Все условия в этом массиве должны совпадать. Условия вmustвлияют на оценку релевантности.filter: Все условия в этом массиве должны совпадать, но они выполняются в контексте без оценки релевантности. Это делает их намного быстрее для строгих критериев включения/исключения.should: По крайней мере одно условие в этом массиве должно совпадать. Эти условия влияют на оценку релевантности, но не обязательны для совпадения.must_not: Ни одно из условий в этом массиве не должно совпадать (эквивалент логического НЕ).
Практический пример запроса bool
Давайте объединим несколько концепций, чтобы найти документы с высоким приоритетом, которые упоминают 'security', но исключают черновики и доступны в регионе 'US'.
GET /logs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "security breach"
}
}
],
"filter": [
{
"term": {
"region.keyword": "US"
}
}
],
"should": [
{
"term": {
"priority": 5
}
}
],
"must_not": [
{
"term": {
"status.keyword": "DRAFT"
}
}
]
}
}
}
Объяснение примера:
- Must: Документ должен содержать фразу "security breach" в анализируемом поле content.
- Filter: Документ должен быть помечен для региона 'US' (быстрое точное совпадение).
- Should: Документы, соответствующие
priority: 5, получат повышение оценки релевантности, но документы с более низким приоритетом, удовлетворяющие условиямmustиfilter, все равно будут возвращены. - Must Not: Документы, помеченные как 'DRAFT', строго исключаются.
Лучшие практики построения запросов
Чтобы ваши поиски были точными и производительными, придерживайтесь следующих рекомендаций:
- Предпочитайте
filterвместоmustдля критериев без оценки релевантности. Если вы только проверяете включение/исключение (например, фильтрация по ID, точной дате или статусу), всегда используйте условиеfilterвнутри запросаbool. Это использует кэширование и избегает дорогостоящих вычислений оценки. - Используйте точные запросы с умом: Для полей, отображаемых как
text(анализируемые), используйтеmatch. Для полей, отображаемых какkeyword(не анализируемые), используйтеtermили диапазонные запросы. - Избегайте глубокой вложенности: Хотя это возможно, глубоко вложенные запросы
boolмогут стать трудными для чтения и отладки, а иногда могут привести к снижению производительности. - Используйте
minimum_should_match: Для условийshouldустановкаminimum_should_match(например, на1или2) заставляет выполнить определенное количество этих необязательных критериев, фактически превращая их в обязательные, но при этом позволяя им влиять на оценку.
Маппинг определяет, какой запрос имеет смысл
Большинство ошибок Query DSL начинаются с маппинга. Запрос может выглядеть правильным, но все равно возвращать запутанные результаты, если поле отображено иначе, чем вы думаете.
Распространенный шаблон — текстовое поле с подполем keyword:
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"status": { "type": "keyword" },
"created_at": { "type": "date" },
"price": { "type": "double" }
}
}
}
Используйте match для title, когда нужно анализируемое полнотекстовое поведение. Используйте term для title.keyword, когда нужно точное значение заголовка. Используйте term для status, потому что это уже keyword. Используйте range для created_at или price, потому что эти поля являются датой и числовыми значениями.
Если запрос term к текстовому полю работает не так, как ожидалось, проблема часто в анализе. Сохраненные токены могут быть приведены к нижнему регистру, разделены, стеммированы или иным образом изменены. Проверьте маппинг, прежде чем менять запрос.
GET /products/_mapping
Для проблем с анализом текста полезен _analyze:
GET /products/_analyze
{
"field": "description",
"text": "Cloud Computing"
}
Это покажет, какие токены Elasticsearch будет искать.
match, match_phrase и multi_match
match — это повседневный полнотекстовый запрос, но он не единственный, который вы будете использовать.
Используйте match_phrase, когда важен порядок слов:
GET /products/_search
{
"query": {
"match_phrase": {
"description": "wireless charging stand"
}
}
}
Это полезно для названий продуктов, сообщений логов, заголовков документов и фраз, где точная последовательность имеет значение. Он строже, чем match, поэтому может возвращать меньше документов.
Используйте multi_match, когда один и тот же пользовательский ввод должен искать по нескольким полям:
GET /products/_search
{
"query": {
"multi_match": {
"query": "noise cancelling headphones",
"fields": ["title^3", "description", "brand^2"]
}
}
}
Повышения ^3 и ^2 говорят Elasticsearch, что совпадения в title и brand должны весить больше, чем совпадения в description. Повышение — это не гарантия, что документ будет ранжироваться первым; это подсказка для оценки. Тестируйте с реальными запросами, прежде чем настраивать повышения слишком агрессивно.
Пагинация без вреда для кластера
Базовые параметры from и size подходят для неглубокой пагинации:
GET /products/_search
{
"from": 20,
"size": 10,
"query": {
"match": {
"description": "laptop sleeve"
}
}
}
Глубокая пагинация — это другое дело. Запрос страницы 1 000 заставляет Elasticsearch сортировать и пропускать много результатов. Для пользовательского поиска избегайте неограниченной глубокой пагинации. Для экспорта или фонового сканирования используйте search_after со стабильной сортировкой:
GET /products/_search
{
"size": 100,
"sort": [
{ "created_at": "asc" },
{ "_id": "asc" }
],
"search_after": ["2025-01-10T12:00:00Z", "abc123"],
"query": {
"term": {
"status": "active"
}
}
}
Значения в search_after берутся из массива sort последнего результата в предыдущем ответе. Этот подход более стабилен для прохода по большим наборам результатов.
Фильтрация источника делает ответы полезными
Производительность поиска — это не только выполнение запроса. Возврат огромных документов может замедлить клиент, сеть и координирующий узел. Если интерфейсу нужно всего несколько полей, запрашивайте только эти поля:
GET /orders/_search
{
"_source": ["order_id", "customer_id", "total", "created_at", "status"],
"query": {
"bool": {
"filter": [
{ "term": { "status": "paid" } },
{ "range": { "created_at": { "gte": "now-7d/d" } } }
]
}
}
}
Это делает ответ более читаемым и может уменьшить размер полезной нагрузки. Это не заменяет хороший дизайн индекса, но помогает, когда документы содержат большие описания, блоки метаданных или вложенные массивы, которые не нужны на текущей странице.
Сортировка и агрегации требуют правильных полей
Сортировка по анализируемому тексту обычно является ошибкой. Сортируйте по полям keyword, числовым или датам:
GET /products/_search
{
"sort": [
{ "price": "asc" },
{ "title.keyword": "asc" }
],
"query": {
"term": {
"status": "active"
}
}
}
То же самое относится ко многим агрегациям. Если нужны подсчеты по статусу, агрегируйте по полю keyword:
GET /orders/_search
{
"size": 0,
"aggs": {
"orders_by_status": {
"terms": {
"field": "status"
}
}
},
"query": {
"range": {
"created_at": {
"gte": "now-30d/d"
}
}
}
}
size: 0 говорит Elasticsearch, что вам нужны только результаты агрегации, а не соответствующие документы. Это небольшая привычка, которая делает ответы чище.
Отладка запросов с помощью explain и profile
Когда результат ранжируется странно, используйте explain для одного документа:
GET /products/_explain/SKU10021
{
"query": {
"match": {
"description": "cloud computing"
}
}
}
Когда запрос медленный, используйте profile в непродуктивной или тщательно контролируемой продуктивной среде:
GET /products/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "description": "cloud computing" } }
],
"filter": [
{ "term": { "status": "active" } }
]
}
}
}
Вывод профиля многословен, но может показать, тратится ли время на текстовый запрос, фильтр, скрипт или другую часть запроса. Не оставляйте профилирование включенным в коде приложения; используйте его как инструмент отладки.
Разумная привычка построения запросов
Для большинства поисков в приложениях стройте запрос в следующем порядке:
- Поместите точные ограничения в
filter: ID арендатора, статус, регион, окно дат, разрешения. - Поместите введенный пользователем текст в
mustсmatch,match_phraseилиmulti_match. - Используйте
shouldдля предпочтений ранжирования, а не жестких требований, если только вы не установилиminimum_should_match. - Ограничьте
_sourceполями, которые нужны вызывающей стороне. - Добавьте стабильную сортировку, если важна пагинация или экспорт.
- Проверьте маппинг, прежде чем винить Elasticsearch.
Query DSL мощен, потому что разделяет фильтрацию, оценку, сортировку и формирование ответа. Как только вы будете держать эти задачи раздельно, запросы станут легче читать, легче настраивать и менее неожиданными в продакшене.
Небольшой пример устранения неполадок
Предположим, пользователь ищет ACME-1000 и не получает результатов, хотя продукт существует. Не добавляйте сразу подстановочные знаки. Сначала проверьте маппинг. Если sku — это keyword, это должно работать:
GET /products/_search
{
"query": {
"term": {
"sku": "ACME-1000"
}
}
}
Если sku был случайно отображен как text, анализ мог разделить или изменить значение. В некоторых случаях вы все равно можете запросить его, но лучшее исправление обычно — изменение маппинга для будущих индексов. Точные идентификаторы, статусы, регионы и ID арендаторов должны быть полями типа keyword. Описания и заголовки, написанные человеком, должны быть текстовыми полями. Query DSL становится намного проще, когда маппинг соответствует тому, как люди на самом деле извлекают данные.