Spec-Zone.ru › Elasticsearch 8
›Гид по Elasticsearch [8.17] ›REST API ›API поиска

API профилирования

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

Предоставляет подробную информацию о времени выполнения отдельных компонентов в запросе поиска.

Описание

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

Вывод API профилирования очень подробный, особенно для сложных запросов, выполняемых через множество фрагментов. Рекомендуется форматировать ответ, чтобы упростить понимание вывода.

Новая справка по API

Для получения самой актуальной информации об API обратитесь к API поиска.

Примеры

Любой _search запрос можно профилировать, добавив параметр верхнего уровня profile:

resp = client.search(
    index="my-index-000001",
    profile=True,
    query={
        "match": {
            "message": "GET /search"
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    profile: true,
    query: {
      match: {
        message: 'GET /search'
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  profile: true,
  query: {
    match: {
      message: "GET /search",
    },
  },
});
console.log(response);
GET /my-index-000001/_search
{
  "profile": true,
  "query" : {
    "match" : { "message" : "GET /search" }
  }
}

Установив параметр верхнего уровня profile в значение true, вы включите профилирование для поиска.

API возвращает следующий результат:

{
  "took": 25,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 5,
      "relation": "eq"
    },
    "max_score": 0.17402273,
    "hits": [...] 
  },
  "profile": {
    "shards": [
      {
        "id": "[q2aE02wS1R8qQFnYu6vDVQ][my-index-000001][0]",
        "node_id": "q2aE02wS1R8qQFnYu6vDVQ",
        "shard_id": 0,
        "index": "my-index-000001",
        "cluster": "(local)",
        "searches": [
          {
            "query": [
              {
                "type": "BooleanQuery",
                "description": "message:get message:search",
                "time_in_nanos" : 11972972,
                "breakdown" : {
                  "set_min_competitive_score_count": 0,
                  "match_count": 5,
                  "shallow_advance_count": 0,
                  "set_min_competitive_score": 0,
                  "next_doc": 39022,
                  "match": 4456,
                  "next_doc_count": 5,
                  "score_count": 5,
                  "compute_max_score_count": 0,
                  "compute_max_score": 0,
                  "advance": 84525,
                  "advance_count": 1,
                  "score": 37779,
                  "build_scorer_count": 2,
                  "create_weight": 4694895,
                  "shallow_advance": 0,
                  "create_weight_count": 1,
                  "build_scorer": 7112295,
                  "count_weight": 0,
                  "count_weight_count": 0
                },
                "children": [
                  {
                    "type": "TermQuery",
                    "description": "message:get",
                    "time_in_nanos": 3801935,
                    "breakdown": {
                      "set_min_competitive_score_count": 0,
                      "match_count": 0,
                      "shallow_advance_count": 3,
                      "set_min_competitive_score": 0,
                      "next_doc": 0,
                      "match": 0,
                      "next_doc_count": 0,
                      "score_count": 5,
                      "compute_max_score_count": 3,
                      "compute_max_score": 32487,
                      "advance": 5749,
                      "advance_count": 6,
                      "score": 16219,
                      "build_scorer_count": 3,
                      "create_weight": 2382719,
                      "shallow_advance": 9754,
                      "create_weight_count": 1,
                      "build_scorer": 1355007,
                      "count_weight": 0,
                      "count_weight_count": 0
                    }
                  },
                  {
                    "type": "TermQuery",
                    "description": "message:search",
                    "time_in_nanos": 205654,
                    "breakdown": {
                      "set_min_competitive_score_count": 0,
                      "match_count": 0,
                      "shallow_advance_count": 3,
                      "set_min_competitive_score": 0,
                      "next_doc": 0,
                      "match": 0,
                      "next_doc_count": 0,
                      "score_count": 5,
                      "compute_max_score_count": 3,
                      "compute_max_score": 6678,
                      "advance": 12733,
                      "advance_count": 6,
                      "score": 6627,
                      "build_scorer_count": 3,
                      "create_weight": 130951,
                      "shallow_advance": 2512,
                      "create_weight_count": 1,
                      "build_scorer": 46153,
                      "count_weight": 0,
                      "count_weight_count": 0
                    }
                  }
                ]
              }
            ],
            "rewrite_time": 451233,
            "collector": [
              {
                "name": "QueryPhaseCollector",
                "reason": "search_query_phase",
                "time_in_nanos": 775274,
                "children" : [
                  {
                    "name": "SimpleTopScoreDocCollector",
                    "reason": "search_top_hits",
                    "time_in_nanos": 775274
                  }
                ]
              }
            ]
          }
        ],
        "aggregations": [],
        "fetch": {
          "type": "fetch",
          "description": "",
          "time_in_nanos": 660555,
          "breakdown": {
            "next_reader": 7292,
            "next_reader_count": 1,
            "load_stored_fields": 299325,
            "load_stored_fields_count": 5,
            "load_source": 3863,
            "load_source_count": 5
          },
          "debug": {
            "stored_fields": ["_id", "_routing", "_source"]
          },
          "children": [
            {
              "type" : "FetchFieldsPhase",
              "description" : "",
              "time_in_nanos" : 238762,
              "breakdown" : {
                "process_count" : 5,
                "process" : 227914,
                "next_reader" : 10848,
                "next_reader_count" : 1
              }
            },
            {
              "type": "FetchSourcePhase",
              "description": "",
              "time_in_nanos": 20443,
              "breakdown": {
                "next_reader": 745,
                "next_reader_count": 1,
                "process": 19698,
                "process_count": 5
              },
              "debug": {
                "fast_path": 5
              }
            },
            {
              "type": "StoredFieldsPhase",
              "description": "",
              "time_in_nanos": 5310,
              "breakdown": {
                "next_reader": 745,
                "next_reader_count": 1,
                "process": 4445,
                "process_count": 5
              }
            }
          ]
        }
      }
    ]
  }
}

Результаты поиска возвращаются, но для краткости здесь опушены.

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

Общая структура ответа профилирования следующая:

{
   "profile": {
        "shards": [
           {
              "id": "[q2aE02wS1R8qQFnYu6vDVQ][my-index-000001][0]",  
              "node_id": "q2aE02wS1R8qQFnYu6vDVQ",
              "shard_id": 0,
              "index": "my-index-000001",
              "cluster": "(local)",             
              "searches": [
                 {
                    "query": [...],             
                    "rewrite_time": 51443,      
                    "collector": [...]          
                 }
              ],
              "aggregations": [...],            
              "fetch": {...}                    
           }
        ]
     }
}

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

Если запрос выполнялся в локальном кластере, имя кластера исключается из составного идентификатора и здесь отмечается как "(локальный)". Для профилирования, выполняемого в удаленном кластере с использованием межкластерного поиска, значение «id» будет чем-то вроде [q2aE02wS1R8qQFnYu6vDVQ][remote1:my-index-000001][0], а значение «cluster» будет remote1.

Время запроса и другая отладочная информация.

Накопленное время переписывания.

Имена и времена вызова каждого коллектора.

Время агрегаций, количество вызовов и отладочная информация.

Время извлечения и отладочная информация.

Поскольку запрос поиска может выполняться на одном или нескольких фрагментах в индексе, а поиск может охватывать один или несколько индексов, верхний уровень в ответе профилирования — это массив объектов shard. Каждый объект фрагмента перечисляет его id, который уникально идентифицирует фрагмент. Формат идентификаторов — [nodeID][clusterName:indexName][shardID]. Если поиск выполняется в локальном кластере, имя кластера не добавляется, и формат — [nodeID][indexName][shardID].

Сам профиль может состоять из одного или нескольких «поисков», где поиск — это запрос, выполняемый на базовом индексе Lucene. Большинство запросов поиска, отправляемых пользователем, выполняют только один search на индексе Lucene. Но иногда выполняются несколько поисков, например, при включении глобальной агрегации (для глобального контекста требуется выполнить вторичный запрос «match_all»).

Внутри каждого объекта search будут два массива профильной информации: массив query и массив collector. Наряду с объектом search находится объект aggregations, содержащий информацию о профилировании агрегаций. В будущем могут быть добавлены дополнительные разделы, такие как suggest, highlight и т. д.

Также будет показан метрика rewrite, отображающая общее время переписывания запроса (в наносекундах).

Как и другие API статистики, API профилирования поддерживает удобочитаемый вывод. Это можно включить, добавив ?human=true в строку запроса. В этом случае вывод содержит дополнительное поле time, содержащее округлённую, удобочитаемую информацию о времени выполнения (например, "time": "391,9ms", "time": "123.3micros").

Профилирование запросов

Подробности, предоставляемые API профилирования, напрямую отображают имена и концепции классов Lucene, что означает, что для полной интерпретации результатов требуется довольно продвинутое знание Lucene. Эта страница пытается дать краткий курс по тому, как Lucene выполняет запросы, чтобы вы могли успешно использовать API профилирования для диагностики и отладки запросов, но это лишь обзор. Для полного понимания, пожалуйста, обратитесь к документации Lucene и, в некоторых местах, к коду.

При этом, полное понимание зачастую не требуется для исправления медленного запроса. Обычно достаточно увидеть, что определённый компонент запроса медленный, а не обязательно понимать, почему именно фаза advance этого запроса является причиной, например.

query Раздел

Раздел query содержит подробное время выполнения дерева запросов, выполняемого Lucene на определенном фрагменте. Общая структура этого дерева запросов будет напоминать ваш исходный запрос Elasticsearch, но может быть немного (или иногда сильно) отличаться. Она также будет использовать похожие, но не всегда идентичные имена. Используя наш предыдущий пример запроса match, давайте проанализируем раздел query:

"query": [
    {
       "type": "BooleanQuery",
       "description": "message:get message:search",
       "time_in_nanos": "11972972",
       "breakdown": {...},               
       "children": [
          {
             "type": "TermQuery",
             "description": "message:get",
             "time_in_nanos": "3801935",
             "breakdown": {...}
          },
          {
             "type": "TermQuery",
             "description": "message:search",
             "time_in_nanos": "205654",
             "breakdown": {...}
          }
       ]
    }
]

В целях простоты, детализированное время выполнения опущено.

Основываясь на структуре профиля, мы видим, что наш запрос match был переписан Lucene в BooleanQuery с двумя операндами (оба содержат TermQuery). Поле type отображает имя класса Lucene и часто совпадает с эквивалентным именем в Elasticsearch. Поле description отображает текст объяснения Lucene для запроса и предназначено для помощи в различении частей вашего запроса (например, message:get и message:search являются TermQuery и в противном случае выглядели бы одинаково).

Поле time_in_nanos показывает, что для выполнения всего BooleanQuery этот запрос потребовал ~11,9 мс. Зафиксированное время включает время всех дочерних элементов.

Поле breakdown предоставит подробные данные о том, как было затрачено время, мы посмотрим на это чуть позже. Наконец, массив children перечисляет любые подзапросы, которые могут быть присутствуют. Поскольку мы искали два значения ("get search"), наш BooleanQuery содержит два дочерних TermQueries. Они имеют идентичную информацию (тип, время, детализация и т. д.). Дочерние элементы могут иметь свои собственные дочерние элементы.

Детализация времени

Компонент breakdown предоставляет подробную статистику времени о выполнении низкоуровневых операций Lucene:

"breakdown": {
  "set_min_competitive_score_count": 0,
  "match_count": 5,
  "shallow_advance_count": 0,
  "set_min_competitive_score": 0,
  "next_doc": 39022,
  "match": 4456,
  "next_doc_count": 5,
  "score_count": 5,
  "compute_max_score_count": 0,
  "compute_max_score": 0,
  "advance": 84525,
  "advance_count": 1,
  "score": 37779,
  "build_scorer_count": 2,
  "create_weight": 4694895,
  "shallow_advance": 0,
  "create_weight_count": 1,
  "build_scorer": 7112295,
  "count_weight": 0,
  "count_weight_count": 0
}

Время указано в наносекундах реального времени и не нормализовано. Все замечания по общему time_in_nanos применимы и здесь. Цель детализации - дать вам представление о том, А) какие механизмы в Lucene фактически потребляют время, и Б) величину различий во времени между различными компонентами. Как и общее время, детализация включает время всех дочерних элементов.

Значения статистики таковы:

Все параметры:

create_weight

Запрос в Lucene должен иметь возможность повторного использования через несколько IndexSearchers (подумайте об этом как о движке, который выполняет поиск по определенному индексу Lucene). Это ставит Lucene в трудную ситуацию, поскольку многие запросы должны накапливать временное состояние/статистику, связанные с индексом, к которому они применяются, но контракт Query предписывает, что он должен быть неизменным.

Чтобы обойти это, Lucene просит каждый запрос сгенерировать объект Weight, который выступает в качестве временного объекта контекста для хранения состояния, связанного с этой конкретной парой (IndexSearcher, Query). Метрика weight показывает, сколько времени занимает этот процесс.

build_scorer

Этот параметр показывает, сколько времени требуется для построения Scorer для запроса. Scorer - это механизм, который итерируется по совпадающим документам и генерирует оценку по каждому документу (например, насколько хорошо "foo" соответствует документу?). Обратите внимание, это фиксирует время, необходимое для создания объекта Scorer, а не фактическое ранжирование документов. У некоторых запросов более быстрое или медленное инициализирование Scorer, в зависимости от оптимизаций, сложности и т. д.

Это также может показать время, связанное с кешированием, если оно включено и/или применимо для данного запроса.

next_doc

Метод Lucene next_doc возвращает ID документа следующего документа, соответствующего запросу. Эта статистика показывает время, которое требуется для определения документа, который является следующим совпадением, процесс, который значительно варьируется в зависимости от характера запроса. Next_doc - это специализированная форма advance(), которая более удобна для многих запросов в Lucene. Она эквивалентна advance(docId() + 1)

advance

advance - это "низкоуровневая" версия next_doc: она выполняет ту же функцию поиска следующего совпадающего документа, но требует, чтобы вызывающий запрос выполнял дополнительные задачи, такие как идентификация и пропуск пропусков и т. д. Однако не все запросы могут использовать next_doc, поэтому advance также измеряется для этих запросов.

Соединения (например, must операнды в Boolean) являются типичными потребителями advance

match

Некоторые запросы, такие как запросы по фразе, сопоставляют документы с помощью «двухфазного» процесса. Сначала документ «приблизительно» сопоставляется, и если он соответствует приблизительно, он проверяется второй раз более строгим (и более дорогим) процессом. Проверка второй фазы - это то, что измеряет статистика match.

Например, запрос по фразе сначала приблизительно проверяет документ, гарантируя, что все термины в фразе присутствуют в документе. Если все термины присутствуют, он выполняет проверку второй фазы, чтобы убедиться, что термины находятся в порядке, чтобы образовать фразу, что относительно дороже, чем просто проверка на наличие терминов.

Поскольку этот двухфазный процесс используется только небольшим числом запросов, статистика match часто равна нулю.

score

Это записывает время, затраченное на оценку конкретного документа с помощью его Scorer.

*_count

Записывает количество вызовов конкретного метода. Например, "next_doc_count": 2, означает, что метод nextDoc() был вызван на двух разных документах. Это можно использовать для оценки избирательности запросов, сравнивая счетчики между различными компонентами запроса.

collectors Раздел

Раздел Collectors ответа показывает подробные сведения об исполнении на высоком уровне. Lucene работает, определяя «Collector», который отвечает за координацию обхода, подсчёта очков и сбора совпадающих документов. Collectors также позволяют одному запросу записывать результаты агрегации, выполнять нескомпонованные «глобальные» запросы, выполнять фильтры после запроса и т. д.

Рассмотрим предыдущий пример:

"collector": [
  {
    "name": "QueryPhaseCollector",
    "reason": "search_query_phase",
    "time_in_nanos": 775274,
    "children" : [
      {
        "name": "SimpleTopScoreDocCollector",
        "reason": "search_top_hits",
        "time_in_nanos": 775274
      }
    ]
  }
]

Мы видим коллектор верхнего уровня, названный QueryPhaseCollector, который содержит дочерний элемент SimpleTopScoreDocCollector. SimpleTopScoreDocCollector — это стандартный «подсчёт очков и сортировка» Collector, используемый Elasticsearch. Поле reason пытается дать простое словесное описание имени класса. Поле time_in_nanos аналогично времени в дереве запросов: время, измеряемое от начала до конца, включая все дочерние элементы. Аналогично, поле children перечисляет все подколлекторы. Когда запрашиваются агрегации, QueryPhaseCollector будет содержать дополнительный коллектор с причиной aggregation, который выполняет агрегации.

Следует отметить, что время коллекторов независимо от времени запросов. Они вычисляются, объединяются и нормализуются независимо! Из-за характера выполнения Lucene невозможно «слить» время коллекторов в раздел запросов, поэтому они отображаются в отдельных разделах.

Для справки, различные причины коллекторов:

search_top_hits

Коллектор, подсчитывающий и сортирующий документы. Это наиболее распространённый коллектор и будет отображаться в большинстве простых поисков

search_count

Коллектор, подсчитывающий только количество документов, соответствующих запросу, но не извлекающий исходные данные. Это наблюдается, когда указано size: 0

search_query_phase

Коллектор, который включает в себя сбор лучших совпадений, а также агрегации в рамках фазы запроса. Он поддерживает прекращение выполнения поиска после нахождения n совпадающих документов (когда указано terminate_after), а также возвращает только совпадающие документы с оценкой, большей, чем n (когда указано min_score). Кроме того, он может отфильтровать совпадающие лучшие совпадения на основе предоставленного post_filter.

search_timeout

Коллектор, который прерывает выполнение после истечения определённого времени. Это наблюдается, когда был указан параметр timeout верхнего уровня.

aggregation

Коллектор, который Elasticsearch использует для выполнения агрегаций в рамках области видимости запроса. Один коллектор aggregation используется для сбора документов для всех агрегаций, поэтому в имени будет отображаться список агрегаций.

global_aggregation

Коллектор, который выполняет агрегацию в отношении глобальной области видимости запроса, а не указанного запроса. Поскольку глобальная область видимости обязательно отличается от выполняемого запроса, он должен выполнить свой собственный запрос match_all (который вы увидите в разделе Query), чтобы собрать все ваши данные

rewrite Раздел

Все запросы в Lucene проходят процесс «переписывания». Запрос (и его подзапросы) могут быть переписаны один или несколько раз, и процесс продолжается до тех пор, пока запрос не перестанет изменяться. Этот процесс позволяет Lucene выполнять оптимизации, такие как удаление избыточных условий, замена одного запроса на более эффективный путь выполнения и т. д. Например, Boolean → Boolean → TermQuery можно переписать в TermQuery, потому что в этом случае все Booleanы не нужны.

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

Более сложный пример

Для демонстрации немного более сложного запроса и соответствующих результатов мы можем проанализировать следующий запрос:

resp = client.search(
    index="my-index-000001",
    profile=True,
    query={
        "term": {
            "user.id": {
                "value": "elkbee"
            }
        }
    },
    aggs={
        "my_scoped_agg": {
            "terms": {
                "field": "http.response.status_code"
            }
        },
        "my_global_agg": {
            "global": {},
            "aggs": {
                "my_level_agg": {
                    "terms": {
                        "field": "http.response.status_code"
                    }
                }
            }
        }
    },
    post_filter={
        "match": {
            "message": "search"
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    profile: true,
    query: {
      term: {
        'user.id' => {
          value: 'elkbee'
        }
      }
    },
    aggregations: {
      my_scoped_agg: {
        terms: {
          field: 'http.response.status_code'
        }
      },
      my_global_agg: {
        global: {},
        aggregations: {
          my_level_agg: {
            terms: {
              field: 'http.response.status_code'
            }
          }
        }
      }
    },
    post_filter: {
      match: {
        message: 'search'
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  profile: true,
  query: {
    term: {
      "user.id": {
        value: "elkbee",
      },
    },
  },
  aggs: {
    my_scoped_agg: {
      terms: {
        field: "http.response.status_code",
      },
    },
    my_global_agg: {
      global: {},
      aggs: {
        my_level_agg: {
          terms: {
            field: "http.response.status_code",
          },
        },
      },
    },
  },
  post_filter: {
    match: {
      message: "search",
    },
  },
});
console.log(response);
GET /my-index-000001/_search
{
  "profile": true,
  "query": {
    "term": {
      "user.id": {
        "value": "elkbee"
      }
    }
  },
  "aggs": {
    "my_scoped_agg": {
      "terms": {
        "field": "http.response.status_code"
      }
    },
    "my_global_agg": {
      "global": {},
      "aggs": {
        "my_level_agg": {
          "terms": {
            "field": "http.response.status_code"
          }
        }
      }
    }
  },
  "post_filter": {
    "match": {
      "message": "search"
    }
  }
}

Этот пример содержит:

  • Запрос
  • Агрегация в области видимости
  • Глобальная агрегация
  • Фильтр после запроса

API возвращает следующий результат:

{
  ...
  "profile": {
    "shards": [
      {
        "id": "[P6xvulHtQRWuD4YnubWb7A][my-index-000001][0]",
        "node_id": "P6xvulHtQRWuD4YnubWb7A",
        "shard_id": 0,
        "index": "my-index-000001",
        "cluster": "(local)",
        "searches": [
          {
            "query": [
              {
                "type": "TermQuery",
                "description": "message:search",
                "time_in_nanos": 141618,
                "breakdown": {
                  "set_min_competitive_score_count": 0,
                  "match_count": 0,
                  "shallow_advance_count": 0,
                  "set_min_competitive_score": 0,
                  "next_doc": 0,
                  "match": 0,
                  "next_doc_count": 0,
                  "score_count": 0,
                  "compute_max_score_count": 0,
                  "compute_max_score": 0,
                  "advance": 3942,
                  "advance_count": 4,
                  "count_weight_count": 0,
                  "score": 0,
                  "build_scorer_count": 2,
                  "create_weight": 38380,
                  "shallow_advance": 0,
                  "count_weight": 0,
                  "create_weight_count": 1,
                  "build_scorer": 99296
                }
              },
              {
                "type": "TermQuery",
                "description": "user.id:elkbee",
                "time_in_nanos": 163081,
                "breakdown": {
                  "set_min_competitive_score_count": 0,
                  "match_count": 0,
                  "shallow_advance_count": 0,
                  "set_min_competitive_score": 0,
                  "next_doc": 2447,
                  "match": 0,
                  "next_doc_count": 4,
                  "score_count": 4,
                  "compute_max_score_count": 0,
                  "compute_max_score": 0,
                  "advance": 3552,
                  "advance_count": 1,
                  "score": 5027,
                  "count_weight_count": 0,
                  "build_scorer_count": 2,
                  "create_weight": 107840,
                  "shallow_advance": 0,
                  "count_weight": 0,
                  "create_weight_count": 1,
                  "build_scorer": 44215
                }
              }
            ],
            "rewrite_time": 4769,
            "collector": [
              {
                "name": "QueryPhaseCollector",
                "reason": "search_query_phase",
                "time_in_nanos": 1945072,
                "children": [
                  {
                    "name": "SimpleTopScoreDocCollector",
                    "reason": "search_top_hits",
                    "time_in_nanos": 22577
                  },
                  {
                    "name": "AggregatorCollector: [my_scoped_agg, my_global_agg]",
                    "reason": "aggregation",
                    "time_in_nanos": 867617
                  }
                ]
              }
            ]
          }
        ],
        "aggregations": [...], 
        "fetch": {...}
      }
    ]
  }
}

Часть "aggregations" опущена, потому что она будет рассмотрена в следующем разделе.

Как видите, вывод значительно более подробный, чем раньше. Все основные части запроса представлены:

  1. Первый TermQuery (user.id:elkbee) представляет основной запрос term.
  2. Второй TermQuery (message:search) представляет запрос post_filter.

Дерево коллекторов достаточно простое, демонстрируя, как один QueryPhaseCollector, содержащий обычный коллектор очков SimpleTopScoreDocCollector для сбора лучших совпадений, а также BucketCollectorWrapper для выполнения всех агрегаций в области видимости.

Понимание вывода MultiTermQuery

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

По существу, эти запросы переписывают себя для каждого сегмента. Если вы представите запрос с подстановкой b*, он технически может соответствовать любому токену, начинающемуся с буквы «b». Перечислить все возможные комбинации было бы невозможно, поэтому Lucene переписывает запрос в контексте оцениваемого сегмента, например, один сегмент может содержать токены [bar, baz], поэтому запрос переписывается в комбинацию BooleanQuery «bar» и «baz». Другой сегмент может содержать только токен [bakery], поэтому запрос переписывается в один TermQuery для «bakery».

Из-за этой динамической переписывки для каждого сегмента чистая структура дерева искажается и больше не следует чистому «родословью», показывающему, как один запрос переписывается в другой. На данный момент мы можем только извиниться и порекомендовать свернуть детали дочерних элементов запроса, если он слишком сложен. К счастью, все статистические данные о времени верны, просто не верен физический макет в ответе, поэтому достаточно проанализировать MultiTermQuery верхнего уровня и проигнорировать его дочерние элементы, если вы найдёте подробности слишком сложными для интерпретации.

Надеемся, что это будет исправлено в будущих итерациях, но это сложная проблема, которая ещё в процессе решения. :)

Профилирование агрегаций

aggregations Раздел

Раздел aggregations содержит подробные сведения о времени выполнения дерева агрегаций, выполняемого конкретным фрагментом. Общая структура этого дерева агрегаций будет похожа на ваш исходный запрос Elasticsearch. Давайте снова выполним предыдущий запрос и посмотрим на профиль агрегации:

resp = client.search(
    index="my-index-000001",
    profile=True,
    query={
        "term": {
            "user.id": {
                "value": "elkbee"
            }
        }
    },
    aggs={
        "my_scoped_agg": {
            "terms": {
                "field": "http.response.status_code"
            }
        },
        "my_global_agg": {
            "global": {},
            "aggs": {
                "my_level_agg": {
                    "terms": {
                        "field": "http.response.status_code"
                    }
                }
            }
        }
    },
    post_filter={
        "match": {
            "message": "search"
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    profile: true,
    query: {
      term: {
        'user.id' => {
          value: 'elkbee'
        }
      }
    },
    aggregations: {
      my_scoped_agg: {
        terms: {
          field: 'http.response.status_code'
        }
      },
      my_global_agg: {
        global: {},
        aggregations: {
          my_level_agg: {
            terms: {
              field: 'http.response.status_code'
            }
          }
        }
      }
    },
    post_filter: {
      match: {
        message: 'search'
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  profile: true,
  query: {
    term: {
      "user.id": {
        value: "elkbee",
      },
    },
  },
  aggs: {
    my_scoped_agg: {
      terms: {
        field: "http.response.status_code",
      },
    },
    my_global_agg: {
      global: {},
      aggs: {
        my_level_agg: {
          terms: {
            field: "http.response.status_code",
          },
        },
      },
    },
  },
  post_filter: {
    match: {
      message: "search",
    },
  },
});
console.log(response);
GET /my-index-000001/_search
{
  "profile": true,
  "query": {
    "term": {
      "user.id": {
        "value": "elkbee"
      }
    }
  },
  "aggs": {
    "my_scoped_agg": {
      "terms": {
        "field": "http.response.status_code"
      }
    },
    "my_global_agg": {
      "global": {},
      "aggs": {
        "my_level_agg": {
          "terms": {
            "field": "http.response.status_code"
          }
        }
      }
    }
  },
  "post_filter": {
    "match": {
      "message": "search"
    }
  }
}

Это даёт следующий результат профиля агрегаций:

{
  "profile": {
    "shards": [
      {
        "aggregations": [
          {
            "type": "NumericTermsAggregator",
            "description": "my_scoped_agg",
            "time_in_nanos": 79294,
            "breakdown": {
              "reduce": 0,
              "build_aggregation": 30885,
              "build_aggregation_count": 1,
              "initialize": 2623,
              "initialize_count": 1,
              "reduce_count": 0,
              "collect": 45786,
              "collect_count": 4,
              "build_leaf_collector": 18211,
              "build_leaf_collector_count": 1,
              "post_collection": 929,
              "post_collection_count": 1
            },
            "debug": {
              "total_buckets": 1,
              "result_strategy": "long_terms",
              "built_buckets": 1
            }
          },
          {
            "type": "GlobalAggregator",
            "description": "my_global_agg",
            "time_in_nanos": 104325,
            "breakdown": {
              "reduce": 0,
              "build_aggregation": 22470,
              "build_aggregation_count": 1,
              "initialize": 12454,
              "initialize_count": 1,
              "reduce_count": 0,
              "collect": 69401,
              "collect_count": 4,
              "build_leaf_collector": 8150,
              "build_leaf_collector_count": 1,
              "post_collection": 1584,
              "post_collection_count": 1
            },
            "debug": {
              "built_buckets": 1
            },
            "children": [
              {
                "type": "NumericTermsAggregator",
                "description": "my_level_agg",
                "time_in_nanos": 76876,
                "breakdown": {
                  "reduce": 0,
                  "build_aggregation": 13824,
                  "build_aggregation_count": 1,
                  "initialize": 1441,
                  "initialize_count": 1,
                  "reduce_count": 0,
                  "collect": 61611,
                  "collect_count": 4,
                  "build_leaf_collector": 5564,
                  "build_leaf_collector_count": 1,
                  "post_collection": 471,
                  "post_collection_count": 1
                },
                "debug": {
                  "total_buckets": 1,
                  "result_strategy": "long_terms",
                  "built_buckets": 1
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

Из структуры профиля видно, что my_scoped_agg выполняется как NumericTermsAggregator (потому что поле, которое он агрегирует, http.response.status_code, является числовым полем). На том же уровне мы видим GlobalAggregator, который происходит от my_global_agg. Эта агрегация затем имеет дочерний элемент NumericTermsAggregator, который происходит от агрегации второго термина по полю http.response.status_code.

Поле time_in_nanos показывает время, затраченное каждой агрегацией, и включает время всех дочерних элементов. Хотя общее время полезно, поле breakdown предоставит подробную статистику о том, как было потрачено время.

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

Детализация по времени

Компонент breakdown предоставляет подробную статистику о выполнении на низком уровне:

"breakdown": {
  "reduce": 0,
  "build_aggregation": 30885,
  "build_aggregation_count": 1,
  "initialize": 2623,
  "initialize_count": 1,
  "reduce_count": 0,
  "collect": 45786,
  "collect_count": 4,
  "build_leaf_collector": 18211,
  "build_leaf_collector_count": 1
}

Каждое свойство в компоненте breakdown соответствует внутреннему методу агрегирования. Например, свойство build_leaf_collector измеряет наносекунды, потраченные на выполнение метода агрегирования getLeafCollector(). Свойства, заканчивающиеся на _count, записывают количество вызовов соответствующего метода. Например, "collect_count": 2 означает, что агрегация вызвала метод collect() для двух различных документов. Свойство reduce зарезервировано для будущих целей и всегда возвращает значение 0.

Временные характеристики указаны в наносекундах реального времени и не нормализованы. Здесь применяются все замечания по поводу общего time. Цель детализации – показать, какие компоненты Elasticsearch потребляют больше времени, и оценить разницу во времени между различными компонентами. Как и общее время, детализация включает время всех дочерних элементов.

Профилирование извлечения

Все фрагменты, которые извлекли документы, будут иметь раздел fetch в профиле. Давайте выполним небольшой поиск и посмотрим на профиль извлечения:

resp = client.search(
    index="my-index-000001",
    filter_path="profile.shards.fetch",
    profile=True,
    query={
        "term": {
            "user.id": {
                "value": "elkbee"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  filter_path: 'profile.shards.fetch',
  body: {
    profile: true,
    query: {
      term: {
        'user.id' => {
          value: 'elkbee'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  filter_path: "profile.shards.fetch",
  profile: true,
  query: {
    term: {
      "user.id": {
        value: "elkbee",
      },
    },
  },
});
console.log(response);
GET /my-index-000001/_search?filter_path=profile.shards.fetch
{
  "profile": true,
  "query": {
    "term": {
      "user.id": {
        "value": "elkbee"
      }
    }
  }
}

И вот профиль извлечения:

{
  "profile": {
    "shards": [
      {
        "fetch": {
          "type": "fetch",
          "description": "",
          "time_in_nanos": 660555,
          "breakdown": {
            "next_reader": 7292,
            "next_reader_count": 1,
            "load_stored_fields": 299325,
            "load_stored_fields_count": 5,
            "load_source": 3863,
            "load_source_count": 5
          },
          "debug": {
            "stored_fields": ["_id", "_routing", "_source"]
          },
          "children": [
            {
              "type" : "FetchFieldsPhase",
              "description" : "",
              "time_in_nanos" : 238762,
              "breakdown" : {
                "process_count" : 5,
                "process" : 227914,
                "next_reader" : 10848,
                "next_reader_count" : 1
              }
            },
            {
              "type": "FetchSourcePhase",
              "description": "",
              "time_in_nanos": 20443,
              "breakdown": {
                "next_reader": 745,
                "next_reader_count": 1,
                "process": 19698,
                "process_count": 5
              },
              "debug": {
                "fast_path": 4
              }
            },
            {
              "type": "StoredFieldsPhase",
              "description": "",
              "time_in_nanos": 5310,
              "breakdown": {
                "next_reader": 745,
                "next_reader_count": 1,
                "process": 4445,
                "process_count": 5
              }
            }
          ]
        }
      }
    ]
  }
}

Поскольку это отладочная информация о том, как Elasticsearch выполняет извлечение, она может меняться от запроса к запросу и от версии к версии. Даже исправления могут изменить вывод здесь. Именно эта несогласованность делает его полезным для отладки.

В любом случае! time_in_nanos измеряет общее время фазы извлечения. breakdown подсчитывает и измеряет время подготовки на основе каждого сегмента в next_reader и время загрузки сохранённых полей в load_stored_fields. Раздел отладки содержит различную невременную информацию, в частности, stored_fields перечисляет сохранённые поля, которые нужно загрузить при извлечении. Если это пустой список, извлечение полностью пропустит загрузку сохранённых полей.

Раздел children содержит подфазы, выполняющие фактическую работу по извлечению, а breakdown содержит подсчеты и временные характеристики подготовки на основе каждого сегмента в next_reader и извлечение по документу в process.

Мы стараемся загрузить все необходимые сохранённые поля для извлечения заранее. Это обычно приводит к тому, что фаза _source занимает несколько микросекунд на каждый результат. В этом случае истинная стоимость фазы _source скрыта в компоненте load_stored_fields детализации. Можно полностью пропустить загрузку сохранённых полей, установив "_source": false, "stored_fields": ["_none_"].

Профилирование фазы DFS

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

  1. Когда значение search_type установлено в dfs_query_then_fetch, и индекс имеет несколько фрагментов.
  2. Когда запрос поиска содержит раздел kNN.

В обоих этих случаях можно профилировать, установив profile в true в качестве части запроса поиска.

Профилирование статистики DFS

Когда значение search_type установлено в dfs_query_then_fetch, и индекс имеет несколько фрагментов, фаза DFS собирает статистику по терминам, чтобы улучшить релевантность результатов поиска.

Ниже приведён пример установки profile в true для запроса, использующего dfs_query_then_fetch:

Сначала создадим индекс с несколькими фрагментами и добавим пару документов с различными значениями в поле keyword.

resp = client.indices.create(
    index="my-dfs-index",
    settings={
        "number_of_shards": 2,
        "number_of_replicas": 1
    },
    mappings={
        "properties": {
            "my-keyword": {
                "type": "keyword"
            }
        }
    },
)
print(resp)

resp1 = client.bulk(
    index="my-dfs-index",
    refresh=True,
    operations=[
        {
            "index": {
                "_id": "1"
            }
        },
        {
            "my-keyword": "a"
        },
        {
            "index": {
                "_id": "2"
            }
        },
        {
            "my-keyword": "b"
        }
    ],
)
print(resp1)
response = client.indices.create(
  index: 'my-dfs-index',
  body: {
    settings: {
      number_of_shards: 2,
      number_of_replicas: 1
    },
    mappings: {
      properties: {
        "my-keyword": {
          type: 'keyword'
        }
      }
    }
  }
)
puts response

response = client.bulk(
  index: 'my-dfs-index',
  refresh: true,
  body: [
    {
      index: {
        _id: '1'
      }
    },
    {
      "my-keyword": 'a'
    },
    {
      index: {
        _id: '2'
      }
    },
    {
      "my-keyword": 'b'
    }
  ]
)
puts response
const response = await client.indices.create({
  index: "my-dfs-index",
  settings: {
    number_of_shards: 2,
    number_of_replicas: 1,
  },
  mappings: {
    properties: {
      "my-keyword": {
        type: "keyword",
      },
    },
  },
});
console.log(response);

const response1 = await client.bulk({
  index: "my-dfs-index",
  refresh: "true",
  operations: [
    {
      index: {
        _id: "1",
      },
    },
    {
      "my-keyword": "a",
    },
    {
      index: {
        _id: "2",
      },
    },
    {
      "my-keyword": "b",
    },
  ],
});
console.log(response1);
PUT my-dfs-index
{
  "settings": {
    "number_of_shards": 2, 
    "number_of_replicas": 1
  },
  "mappings": {
      "properties": {
        "my-keyword": { "type": "keyword" }
      }
    }
}

POST my-dfs-index/_bulk?refresh=true
{ "index" : { "_id" : "1" } }
{ "my-keyword" : "a" }
{ "index" : { "_id" : "2" } }
{ "my-keyword" : "b" }

Индекс создаётся с несколькими фрагментами.

После настройки индекса мы можем профилировать фазу DFS запроса поиска. В этом примере мы используем запрос по терминам.

resp = client.search(
    index="my-dfs-index",
    search_type="dfs_query_then_fetch",
    pretty=True,
    size="0",
    profile=True,
    query={
        "term": {
            "my-keyword": {
                "value": "a"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-dfs-index',
  search_type: 'dfs_query_then_fetch',
  pretty: true,
  size: 0,
  body: {
    profile: true,
    query: {
      term: {
        "my-keyword": {
          value: 'a'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-dfs-index",
  search_type: "dfs_query_then_fetch",
  pretty: "true",
  size: 0,
  profile: true,
  query: {
    term: {
      "my-keyword": {
        value: "a",
      },
    },
  },
});
console.log(response);
GET /my-dfs-index/_search?search_type=dfs_query_then_fetch&pretty&size=0 
{
  "profile": true, 
  "query": {
    "term": {
      "my-keyword": {
        "value": "a"
      }
    }
  }
}

Параметр URL search_type установлен на dfs_query_then_fetch, чтобы гарантировать выполнение фазы DFS.

Параметр profile установлен на true.

В ответе мы видим профиль, который включает раздел dfs для каждого фрагмента, а также вывод профиля для остальных фаз поиска. Один из разделов dfs для фрагмента выглядит следующим образом:

"dfs" : {
    "statistics" : {
        "type" : "statistics",
        "description" : "collect term statistics",
        "time_in_nanos" : 236955,
        "breakdown" : {
            "term_statistics" : 4815,
            "collection_statistics" : 27081,
            "collection_statistics_count" : 1,
            "create_weight" : 153278,
            "term_statistics_count" : 1,
            "rewrite_count" : 0,
            "create_weight_count" : 1,
            "rewrite" : 0
        }
    }
}

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

Профилирование поиска kNN

Поиск k-ближайших соседей (kNN) выполняется во время фазы DFS.

Следующий пример демонстрирует установку profile в true для запроса, содержащего раздел knn:

Сначала создадим индекс с несколькими плотно упакованными векторами.

resp = client.indices.create(
    index="my-knn-index",
    mappings={
        "properties": {
            "my-vector": {
                "type": "dense_vector",
                "dims": 3,
                "index": True,
                "similarity": "l2_norm"
            }
        }
    },
)
print(resp)

resp1 = client.bulk(
    index="my-knn-index",
    refresh=True,
    operations=[
        {
            "index": {
                "_id": "1"
            }
        },
        {
            "my-vector": [
                1,
                5,
                -20
            ]
        },
        {
            "index": {
                "_id": "2"
            }
        },
        {
            "my-vector": [
                42,
                8,
                -15
            ]
        },
        {
            "index": {
                "_id": "3"
            }
        },
        {
            "my-vector": [
                15,
                11,
                23
            ]
        }
    ],
)
print(resp1)
response = client.indices.create(
  index: 'my-knn-index',
  body: {
    mappings: {
      properties: {
        "my-vector": {
          type: 'dense_vector',
          dims: 3,
          index: true,
          similarity: 'l2_norm'
        }
      }
    }
  }
)
puts response

response = client.bulk(
  index: 'my-knn-index',
  refresh: true,
  body: [
    {
      index: {
        _id: '1'
      }
    },
    {
      "my-vector": [
        1,
        5,
        -20
      ]
    },
    {
      index: {
        _id: '2'
      }
    },
    {
      "my-vector": [
        42,
        8,
        -15
      ]
    },
    {
      index: {
        _id: '3'
      }
    },
    {
      "my-vector": [
        15,
        11,
        23
      ]
    }
  ]
)
puts response
const response = await client.indices.create({
  index: "my-knn-index",
  mappings: {
    properties: {
      "my-vector": {
        type: "dense_vector",
        dims: 3,
        index: true,
        similarity: "l2_norm",
      },
    },
  },
});
console.log(response);

const response1 = await client.bulk({
  index: "my-knn-index",
  refresh: "true",
  operations: [
    {
      index: {
        _id: "1",
      },
    },
    {
      "my-vector": [1, 5, -20],
    },
    {
      index: {
        _id: "2",
      },
    },
    {
      "my-vector": [42, 8, -15],
    },
    {
      index: {
        _id: "3",
      },
    },
    {
      "my-vector": [15, 11, 23],
    },
  ],
});
console.log(response1);
PUT my-knn-index
{
  "mappings": {
    "properties": {
      "my-vector": {
        "type": "dense_vector",
        "dims": 3,
        "index": true,
        "similarity": "l2_norm"
      }
    }
  }
}

POST my-knn-index/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "my-vector": [1, 5, -20] }
{ "index": { "_id": "2" } }
{ "my-vector": [42, 8, -15] }
{ "index": { "_id": "3" } }
{ "my-vector": [15, 11, 23] }

После настройки индекса мы можем профилировать запрос поиска kNN.

resp = client.search(
    index="my-knn-index",
    profile=True,
    knn={
        "field": "my-vector",
        "query_vector": [
            -5,
            9,
            -12
        ],
        "k": 3,
        "num_candidates": 100
    },
)
print(resp)
response = client.search(
  index: 'my-knn-index',
  body: {
    profile: true,
    knn: {
      field: 'my-vector',
      query_vector: [
        -5,
        9,
        -12
      ],
      k: 3,
      num_candidates: 100
    }
  }
)
puts response
const response = await client.search({
  index: "my-knn-index",
  profile: true,
  knn: {
    field: "my-vector",
    query_vector: [-5, 9, -12],
    k: 3,
    num_candidates: 100,
  },
});
console.log(response);
POST my-knn-index/_search
{
  "profile": true, 
  "knn": {
    "field": "my-vector",
    "query_vector": [-5, 9, -12],
    "k": 3,
    "num_candidates": 100
  }
}

Параметр profile установлен на true.

В ответе мы видим профиль, включающий раздел knn в качестве части раздела dfs для каждого фрагмента, а также вывод профиля для остальных фаз поиска.

Один из разделов dfs.knn для фрагмента выглядит следующим образом:

"dfs" : {
    "knn" : [
        {
        "vector_operations_count" : 4,
        "query" : [
            {
                "type" : "DocAndScoreQuery",
                "description" : "DocAndScore[100]",
                "time_in_nanos" : 444414,
                "breakdown" : {
                  "set_min_competitive_score_count" : 0,
                  "match_count" : 0,
                  "shallow_advance_count" : 0,
                  "set_min_competitive_score" : 0,
                  "next_doc" : 1688,
                  "match" : 0,
                  "next_doc_count" : 3,
                  "score_count" : 3,
                  "compute_max_score_count" : 0,
                  "compute_max_score" : 0,
                  "advance" : 4153,
                  "advance_count" : 1,
                  "score" : 2099,
                  "build_scorer_count" : 2,
                  "create_weight" : 128879,
                  "shallow_advance" : 0,
                  "create_weight_count" : 1,
                  "build_scorer" : 307595,
                  "count_weight": 0,
                  "count_weight_count": 0
                }
            }
        ],
        "rewrite_time" : 1275732,
        "collector" : [
            {
                "name" : "SimpleTopScoreDocCollector",
                "reason" : "search_top_hits",
                "time_in_nanos" : 17163
            }
        ]
    }   ]
}

В части dfs.knn ответа мы видим вывод временных характеристик для запроса, переопределения и сборщиков. В отличие от многих других запросов, kNN-поиск выполняет большую часть работы во время переопределения запроса. Это означает, что rewrite_time представляет время, затраченное на kNN-поиск. Атрибут vector_operations_count представляет общее количество операций над векторами, выполненных во время kNN-поиска.

Учитываемые моменты при профилировании

Как и любой другой профилировщик, API профилирования вносит значительную нагрузку на выполнение поиска. Действие инструментирования вызовов методов низкого уровня, таких как collect, advance и next_doc, может быть довольно дорогим, так как эти методы вызываются в тесных циклах. Поэтому профилирование не должно по умолчанию включаться в производственных средах и не должно сравниваться с временами запросов без профилирования. Профилирование – это просто диагностический инструмент.

Также существуют случаи, когда специальные оптимизации Lucene отключены, так как они не подходят для профилирования. Это может привести к тому, что некоторые запросы будут показывать более высокие относительные времена, чем их не-профилированные аналоги, но в целом это не должно иметь существенного влияния по сравнению с другими компонентами в профилируемом запросе.

Ограничения

  • В настоящее время профилирование не измеряет сетевую нагрузку.
  • Профилирование также не учитывает время, затраченное в очереди, слияние ответов фрагментов на координационном узле или дополнительные задачи, такие как построение глобальных порядковых номеров (внутренняя структура данных, используемая для ускорения поиска).
  • В настоящее время статистика профилирования недоступна для предложений.
  • В настоящее время профилирование фазы сокращения агрегации недоступно.
  • Профилировщик инструментирует внутренние компоненты, которые могут изменяться от версии к версии. Результирующий json следует считать в основном нестабильным, особенно элементы в разделе debug.

© 2023-2025 Elasticsearch
As of September 2024, Elasticsearch is available under a choice of three licenses: the Server Side Public License (SSPL), the Elastic License, or the AGPLv3 (OSI approved).
Elasticsearch and the Elasticsearch logo are trademarks of Elasticsearch B.V., registered in the U.S. and in other countries.
https://www.elastic.co/guide/en/elasticsearch/reference/8.17/search-profile.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API