Spec-Zone.ru › Elasticsearch 8
›Руководство по Elasticsearch [8.17] ›Агрегации ›Агрегации по корзинам

Агрегация по дате-гистограмме

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

Как и в случае с гистограммой, значения округляются вниз до ближайшей корзины. Например, если интервал — календарный день, 2020-01-03T07:00:01Z округляется до 2020-01-03T00:00:00Z. Значения округляются следующим образом:

bucket_key = Math.floor(value / interval) * interval

Календарные и фиксированные интервалы

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

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

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

Календарные интервалы

Календарные интервалы настраиваются с помощью параметра calendar_interval. Вы можете указать календарные интервалы, используя наименование единицы измерения, например, month, или как количество единиц, например, 1M. Например, day и 1d эквивалентны. Не поддерживаются множественные количества, такие как 2d.

Допустимые календарные интервалы:

minute, 1m
Все минуты начинаются с 00 секунд. Одна минута — интервал между 00 секундами первой минуты и 00 секундами следующей минуты в указанной временной зоне, с учетом любых промежуточных високосных секунд, так что количество минут и секунд, прошедших с начала часа, одинаково в начале и в конце.
hour, 1h
Все часы начинаются с 00 минут и 00 секунд. Один час (1ч) — интервал между 00:00 минутами первого часа и 00:00 минутами следующего часа в указанной временной зоне, с учетом любых промежуточных високосных секунд, так что количество минут и секунд, прошедших с начала часа, одинаково в начале и в конце.
day, 1d
Все дни начинаются с самого раннего возможного времени, обычно 00:00:00 (полночь). Один день (1д) — интервал между началом дня и началом следующего дня в указанной временной зоне, с учетом любых промежуточных изменений времени.
week, 1w
Одна неделя — интервал между началом дня недели, часа, минуты и секунды и тем же днём недели и временем следующей недели в указанной временной зоне.
month, 1M
Один месяц — интервал между началом месяца и временем суток и тем же днём месяца и временем суток следующего месяца в указанной временной зоне, так что день месяца и время суток одинаковы в начале и в конце. Обратите внимание, что день может отличаться, если используется offset, который больше, чем один месяц.
quarter, 1q
Один квартал — интервал между началом месяца и временем суток и тем же днём месяца и временем суток через три месяца, так что день месяца и время суток одинаковы в начале и в конце.
year, 1y
Один год — интервал между началом месяца и временем суток и тем же днём месяца и временем суток следующего года в указанной временной зоне, так что дата и время одинаковы в начале и в конце.

Примеры календарных интервалов

В качестве примера, вот агрегация, запрашивающая интервалы корзин в один месяц в календарном времени:

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "month"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'month'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "month"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "month",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "month"
      }
    }
  }
}

Если вы попытаетесь использовать кратные календарные единицы, агрегация завершится неудачно, потому что поддерживаются только единичные календарные единицы:

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "2d"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: '2d'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "2d"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "2d",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "2d"
      }
    }
  }
}
{
  "error" : {
    "root_cause" : [...],
    "type" : "x_content_parse_exception",
    "reason" : "[1:82] [date_histogram] failed to parse field [calendar_interval]",
    "caused_by" : {
      "type" : "illegal_argument_exception",
      "reason" : "The supplied interval [2d] could not be parsed as a calendar interval.",
      "stack_trace" : "java.lang.IllegalArgumentException: The supplied interval [2d] could not be parsed as a calendar interval."
    }
  }
}

Фиксированные интервалы

Фиксированные интервалы настраиваются с помощью параметра fixed_interval.

В отличие от календарных интервалов, фиксированные интервалы представляют собой фиксированное количество единиц СИ и никогда не изменяются, независимо от того, где они попадают на календарь. Одна секунда всегда состоит из 1000ms. Это позволяет указывать фиксированные интервалы в любом кратном значении поддерживаемых единиц.

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

Допустимые единицы измерения для фиксированных интервалов:

миллисекунды (ms)
Одна миллисекунда. Это очень, очень маленький интервал.
секунды (s)
Определяются как 1000 миллисекунд каждая.
минуты (m)
Определяются как 60 секунд каждая (60 000 миллисекунд). Все минуты начинаются с 00 секунд.
часы (h)
Определяются как 60 минут каждая (3 600 000 миллисекунд). Все часы начинаются с 00 минут и 00 секунд.
дни (d)
Определяются как 24 часа (86 400 000 миллисекунд). Все дни начинаются с самого раннего возможного времени, обычно 00:00:00 (полночь).

Примеры фиксированных интервалов

Если мы попытаемся воссоздать «месяц» calendar_interval из предыдущего примера, мы можем приблизительно заменить его 30 фиксированными днями:

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date",
                "fixed_interval": "30d"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          fixed_interval: '30d'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "fixed_interval": "30d"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        fixed_interval: "30d",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "fixed_interval": "30d"
      }
    }
  }
}

Но если мы попытаемся использовать календарную единицу, которая не поддерживается, например, недели, мы получим исключение:

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date",
                "fixed_interval": "2w"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          fixed_interval: '2w'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "fixed_interval": "2w"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        fixed_interval: "2w",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "fixed_interval": "2w"
      }
    }
  }
}
{
  "error" : {
    "root_cause" : [...],
    "type" : "x_content_parse_exception",
    "reason" : "[1:82] [date_histogram] failed to parse field [fixed_interval]",
    "caused_by" : {
      "type" : "illegal_argument_exception",
      "reason" : "failed to parse setting [date_histogram.fixedInterval] with value [2w] as a time value: unit is missing or unrecognized",
      "stack_trace" : "java.lang.IllegalArgumentException: failed to parse setting [date_histogram.fixedInterval] with value [2w] as a time value: unit is missing or unrecognized"
    }
  }
}

Примечания к использованию агрегации по дате-гистограмме

Во всех случаях, если указанное конечное время не существует, фактическое конечное время — это ближайшее доступное время после указанного конечного.

Широко распределенные приложения также должны учитывать особенности, такие как страны, которые начинают и останавливают летнее время в 12:01, что приводит к одной минуте воскресенья, за которой следует ещё 59 минут субботы раз в год, и страны, которые решают переместиться через международную дату границу. Такие ситуации могут сделать нерегулярные смещения часовых поясов кажущимися простыми.

Как и всегда, тщательные тесты, особенно вокруг событий смены времени, гарантируют, что ваше указание временного интервала соответствует вашим намерениям.

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

Дробные значения времени не поддерживаются, но вы можете решить эту проблему, перейдя к другой временной единице (например, 1.5h можно указать как 90m).

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

Ключи

Внутри, дата представляется как 64-битное число, представляющее временную метку в миллисекундах с начала эпохи (01.01.1970 в полночь по UTC). Эти временные метки возвращаются как имя ведра.

key_as_string — это та же временная метка, преобразованная в отформатированную строку даты с использованием спецификации параметра format:

Если вы не укажете format, используется первый формат даты формата, указанный в сопоставлении поля.

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "1M",
                "format": "yyyy-MM-dd"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: '1M',
          format: 'yyyy-MM-dd'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "1M",
	        "format": "yyyy-MM-dd"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "1M",
        format: "yyyy-MM-dd",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "1M",
        "format": "yyyy-MM-dd" 
      }
    }
  }
}

Поддерживает выразительные шаблоны форматирования даты шаблон форматирования даты

Ответ:

{
  ...
  "aggregations": {
    "sales_over_time": {
      "buckets": [
        {
          "key_as_string": "2015-01-01",
          "key": 1420070400000,
          "doc_count": 3
        },
        {
          "key_as_string": "2015-02-01",
          "key": 1422748800000,
          "doc_count": 2
        },
        {
          "key_as_string": "2015-03-01",
          "key": 1425168000000,
          "doc_count": 2
        }
      ]
    }
  }
}

Часовой пояс

Elasticsearch хранит даты и время в формате Coordinated Universal Time (UTC). По умолчанию все группировка и округление также выполняются в UTC. Используйте параметр time_zone, чтобы указать, что группировка должна использовать другой часовой пояс.

Когда вы указываете часовой пояс, используется следующий алгоритм для определения ведра, к которому принадлежит документ:

bucket_key = localToUtc(Math.floor(utcToLocal(value) / interval) * interval))

Например, если интервал — это календарный день, а часовой пояс — America/New_York, то значение даты 2020-01-03T01:00:01Z обрабатывается следующим образом:

  1. Преобразуется в EST: 2020-01-02T20:00:01
  2. Округляется вниз до ближайшего интервала: 2020-01-02T00:00:00
  3. Преобразуется обратно в UTC: 2020-01-02T05:00:00:00Z

Когда для ведра генерируется key_as_string, значение ключа хранится в формате America/New_York, поэтому оно будет отображаться как "2020-01-02T00:00:00".

Вы можете указать часовые пояса как смещение UTC в формате ISO 8601, например, +01:00 или -08:00, или как идентификатор часового пояса IANA, например, America/Los_Angeles.

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

resp = client.index(
    index="my-index-000001",
    id="1",
    refresh=True,
    document={
        "date": "2015-10-01T00:30:00Z"
    },
)
print(resp)

resp1 = client.index(
    index="my-index-000001",
    id="2",
    refresh=True,
    document={
        "date": "2015-10-01T01:30:00Z"
    },
)
print(resp1)

resp2 = client.search(
    index="my-index-000001",
    size="0",
    aggs={
        "by_day": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "day"
            }
        }
    },
)
print(resp2)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  refresh: true,
  body: {
    date: '2015-10-01T00:30:00Z'
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 2,
  refresh: true,
  body: {
    date: '2015-10-01T01:30:00Z'
  }
)
puts response

response = client.search(
  index: 'my-index-000001',
  size: 0,
  body: {
    aggregations: {
      by_day: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'day'
        }
      }
    }
  }
)
puts response
{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T00:30:00Z"
	}`),
		es.Index.WithDocumentID("1"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T01:30:00Z"
	}`),
		es.Index.WithDocumentID("2"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Search(
		es.Search.WithIndex("my-index-000001"),
		es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "by_day": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "day"
	      }
	    }
	  }
	}`)),
		es.Search.WithSize(0),
		es.Search.WithPretty(),
	)
	fmt.Println(res, err)
}
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  refresh: "true",
  document: {
    date: "2015-10-01T00:30:00Z",
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 2,
  refresh: "true",
  document: {
    date: "2015-10-01T01:30:00Z",
  },
});
console.log(response1);

const response2 = await client.search({
  index: "my-index-000001",
  size: 0,
  aggs: {
    by_day: {
      date_histogram: {
        field: "date",
        calendar_interval: "day",
      },
    },
  },
});
console.log(response2);
PUT my-index-000001/_doc/1?refresh
{
  "date": "2015-10-01T00:30:00Z"
}

PUT my-index-000001/_doc/2?refresh
{
  "date": "2015-10-01T01:30:00Z"
}

GET my-index-000001/_search?size=0
{
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field":     "date",
        "calendar_interval":  "day"
      }
    }
  }
}

Если вы не укажете часовой пояс, будет использоваться UTC. В результате оба документа будут помещены в одно и то же ведро дня, которое начинается в полночь UTC 1 октября 2015 года:

{
  ...
  "aggregations": {
    "by_day": {
      "buckets": [
        {
          "key_as_string": "2015-10-01T00:00:00.000Z",
          "key":           1443657600000,
          "doc_count":     2
        }
      ]
    }
  }
}

Если вы укажете часовой пояс time_zone -01:00, полночь в этом часовом поясе будет на один час раньше полуночи UTC:

resp = client.search(
    index="my-index-000001",
    size="0",
    aggs={
        "by_day": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "day",
                "time_zone": "-01:00"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  size: 0,
  body: {
    aggregations: {
      by_day: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'day',
          time_zone: '-01:00'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("my-index-000001"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "by_day": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "day",
	        "time_zone": "-01:00"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "my-index-000001",
  size: 0,
  aggs: {
    by_day: {
      date_histogram: {
        field: "date",
        calendar_interval: "day",
        time_zone: "-01:00",
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search?size=0
{
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field":     "date",
        "calendar_interval":  "day",
        "time_zone": "-01:00"
      }
    }
  }
}

Теперь первый документ попадает в ведро для 30 сентября 2015 года, а второй — в ведро для 1 октября 2015 года:

{
  ...
  "aggregations": {
    "by_day": {
      "buckets": [
        {
          "key_as_string": "2015-09-30T00:00:00.000-01:00", 
          "key": 1443574800000,
          "doc_count": 1
        },
        {
          "key_as_string": "2015-10-01T00:00:00.000-01:00", 
          "key": 1443661200000,
          "doc_count": 1
        }
      ]
    }
  }
}

Значение key_as_string представляет полночь каждого дня в указанном часовом поясе.

Многие часовые пояса сдвигают свои часы на летнее время. Ведра, близкие к моменту этих изменений, могут иметь немного другие размеры, чем ожидалось по calendar_interval или fixed_interval. Например, рассмотрим начало DST в часовом поясе CET: 27 марта 2016 года в 2 часа по местному времени часы перевели на 1 час вперёд, до 3 часов местного времени. Если вы используете day в качестве calendar_interval, ведро, охватывающее этот день, будет содержать данные только в течение 23 часов, а не в течение обычных 24 часов для других ведер. То же самое относится к более коротким интервалам, таким как fixed_interval 12h, где у вас будет только 11-часовое ведро утром 27 марта при смене DST.

Смещение

Используйте параметр offset, чтобы изменить начальное значение каждого ведра на указанное положительное (+) или отрицательное смещение (-) продолжительности, например, 1h для часа или 1d для дня. См. Единицы времени для получения дополнительных возможных вариантов продолжительности времени.

Например, при использовании интервала day каждое ведро работает с полуночи до полуночи. Установка параметра offset в значение +6h изменяет каждое ведро, чтобы оно работало с 6:00 до 6:00:

resp = client.index(
    index="my-index-000001",
    id="1",
    refresh=True,
    document={
        "date": "2015-10-01T05:30:00Z"
    },
)
print(resp)

resp1 = client.index(
    index="my-index-000001",
    id="2",
    refresh=True,
    document={
        "date": "2015-10-01T06:30:00Z"
    },
)
print(resp1)

resp2 = client.search(
    index="my-index-000001",
    size="0",
    aggs={
        "by_day": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "day",
                "offset": "+6h"
            }
        }
    },
)
print(resp2)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  refresh: true,
  body: {
    date: '2015-10-01T05:30:00Z'
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 2,
  refresh: true,
  body: {
    date: '2015-10-01T06:30:00Z'
  }
)
puts response

response = client.search(
  index: 'my-index-000001',
  size: 0,
  body: {
    aggregations: {
      by_day: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'day',
          offset: '+6h'
        }
      }
    }
  }
)
puts response
{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T05:30:00Z"
	}`),
		es.Index.WithDocumentID("1"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T06:30:00Z"
	}`),
		es.Index.WithDocumentID("2"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Search(
		es.Search.WithIndex("my-index-000001"),
		es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "by_day": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "day",
	        "offset": "+6h"
	      }
	    }
	  }
	}`)),
		es.Search.WithSize(0),
		es.Search.WithPretty(),
	)
	fmt.Println(res, err)
}
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  refresh: "true",
  document: {
    date: "2015-10-01T05:30:00Z",
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 2,
  refresh: "true",
  document: {
    date: "2015-10-01T06:30:00Z",
  },
});
console.log(response1);

const response2 = await client.search({
  index: "my-index-000001",
  size: 0,
  aggs: {
    by_day: {
      date_histogram: {
        field: "date",
        calendar_interval: "day",
        offset: "+6h",
      },
    },
  },
});
console.log(response2);
PUT my-index-000001/_doc/1?refresh
{
  "date": "2015-10-01T05:30:00Z"
}

PUT my-index-000001/_doc/2?refresh
{
  "date": "2015-10-01T06:30:00Z"
}

GET my-index-000001/_search?size=0
{
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field":     "date",
        "calendar_interval":  "day",
        "offset":    "+6h"
      }
    }
  }
}

Вместо одного ведра, начинающегося в полночь, вышеуказанный запрос группирует документы в ведра, начинающиеся в 6:00:

{
  ...
  "aggregations": {
    "by_day": {
      "buckets": [
        {
          "key_as_string": "2015-09-30T06:00:00.000Z",
          "key": 1443592800000,
          "doc_count": 1
        },
        {
          "key_as_string": "2015-10-01T06:00:00.000Z",
          "key": 1443679200000,
          "doc_count": 1
        }
      ]
    }
  }
}

Начальное offset каждого ведра рассчитывается после time_zone корректировок.

Длинные смещения по календарным интервалам

Обычно используют смещения в единицах, меньших, чем calendar_interval. Например, использование смещений в часах при интервале в днях или смещение в днях при интервале в месяцах. Если календарный интервал всегда имеет стандартную длину, или offset меньше одной единицы календарного интервала (например, меньше +24h для days или меньше +28d для месяцев), тогда каждое ведро будет иметь повторяющееся начало. Например, +6h для days приведет к тому, что все ведра будут начинаться в 6:00 каждый день. Однако +30h также приведет к началу ведер в 6:00, за исключением случаев перехода с стандартного времени на летнее время или наоборот.

Эта ситуация намного более выражена для месяцев, где каждый месяц имеет разную длину по крайней мере по одному из соседних месяцев. Чтобы продемонстрировать это, рассмотрим восемь документов, каждый с полем даты на 20-й день каждого из восьми месяцев с января по август 2022 года.

При запросе гистограммы дат по календарному интервалу месяцев ответ вернёт по одному ведру на месяц, каждое с одним документом. Каждое ведро будет иметь имя, указывающее на первый день месяца, плюс любое смещение. Например, смещение +19d приведет к ведрам с именами, такими как 2022-01-20.

"buckets": [
  { "key_as_string": "2022-01-20", "key": 1642636800000, "doc_count": 1 },
  { "key_as_string": "2022-02-20", "key": 1645315200000, "doc_count": 1 },
  { "key_as_string": "2022-03-20", "key": 1647734400000, "doc_count": 1 },
  { "key_as_string": "2022-04-20", "key": 1650412800000, "doc_count": 1 },
  { "key_as_string": "2022-05-20", "key": 1653004800000, "doc_count": 1 },
  { "key_as_string": "2022-06-20", "key": 1655683200000, "doc_count": 1 },
  { "key_as_string": "2022-07-20", "key": 1658275200000, "doc_count": 1 },
  { "key_as_string": "2022-08-20", "key": 1660953600000, "doc_count": 1 }
]

Увеличение смещения до +20d, каждый документ появится в ведре предыдущего месяца, при этом все ключи ведер будут заканчиваться на тот же день месяца, как обычно. Однако при дальнейшем увеличении до +28d, то, что раньше было ведром февраля, теперь стало "2022-03-01".

"buckets": [
  { "key_as_string": "2021-12-29", "key": 1640736000000, "doc_count": 1 },
  { "key_as_string": "2022-01-29", "key": 1643414400000, "doc_count": 1 },
  { "key_as_string": "2022-03-01", "key": 1646092800000, "doc_count": 1 },
  { "key_as_string": "2022-03-29", "key": 1648512000000, "doc_count": 1 },
  { "key_as_string": "2022-04-29", "key": 1651190400000, "doc_count": 1 },
  { "key_as_string": "2022-05-29", "key": 1653782400000, "doc_count": 1 },
  { "key_as_string": "2022-06-29", "key": 1656460800000, "doc_count": 1 },
  { "key_as_string": "2022-07-29", "key": 1659052800000, "doc_count": 1 }
]

Если мы будем продолжать увеличивать смещение, месяцы с 30 днями также сместятся в следующий месяц, так что 3 из 8 ведер будут иметь разные дни, чем другие пять. Фактически, если мы продолжим, мы найдём случаи, когда два документа появятся в одном месяце. Документы, которые изначально были разделены на 30 дней, могут быть смещены в одно и то же ведро месяца с 31 днём.

Например, для +50d мы видим:

"buckets": [
  { "key_as_string": "2022-01-20", "key": 1642636800000, "doc_count": 1 },
  { "key_as_string": "2022-02-20", "key": 1645315200000, "doc_count": 2 },
  { "key_as_string": "2022-04-20", "key": 1650412800000, "doc_count": 2 },
  { "key_as_string": "2022-06-20", "key": 1655683200000, "doc_count": 2 },
  { "key_as_string": "2022-08-20", "key": 1660953600000, "doc_count": 1 }
]

Поэтому всегда важно, когда используете offset с calendar_interval размерами ведёр, понимать последствия использования смещений, больших, чем размер интервала.

Дополнительные примеры:

  • Если цель, например, состоит в том, чтобы иметь ежегодную гистограмму, где каждый год начинается 5 февраля, вы можете использовать смещение calendar_interval year и интервал offset +33d, и каждый год будет смещён идентично, потому что смещение включает только январь, длина которого одинакова каждый год. Однако, если цель состоит в том, чтобы год начинался 5 марта, этот метод не сработает, потому что смещение включает февраль, длина которого изменяется каждые четыре года.
  • Если вы хотите квартальную гистограмму, начинающуюся в дату в первом месяце года, она будет работать, но как только вы переместите начальную дату во второй месяц, имея смещение больше месяца, кварталы начнут на разные даты.

Ключевой ответ

Установив флаг keyed в значение true, каждой корзине присваивается уникальный строковый ключ, а диапазоны возвращаются в виде словаря, а не массива:

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "1M",
                "format": "yyyy-MM-dd",
                "keyed": True
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: '1M',
          format: 'yyyy-MM-dd',
          keyed: true
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "1M",
	        "format": "yyyy-MM-dd",
	        "keyed": true
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "1M",
        format: "yyyy-MM-dd",
        keyed: true,
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "1M",
        "format": "yyyy-MM-dd",
        "keyed": true
      }
    }
  }
}

Ответ:

{
  ...
  "aggregations": {
    "sales_over_time": {
      "buckets": {
        "2015-01-01": {
          "key_as_string": "2015-01-01",
          "key": 1420070400000,
          "doc_count": 3
        },
        "2015-02-01": {
          "key_as_string": "2015-02-01",
          "key": 1422748800000,
          "doc_count": 2
        },
        "2015-03-01": {
          "key_as_string": "2015-03-01",
          "key": 1425168000000,
          "doc_count": 2
        }
      }
    }
  }
}

Скрипты

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

resp = client.search(
    index="sales",
    size="0",
    runtime_mappings={
        "date.promoted_is_tomorrow": {
            "type": "date",
            "script": "\n        long date = doc['date'].value.toInstant().toEpochMilli();\n        if (doc['promoted'].value) {\n          date += 86400;\n        }\n        emit(date);\n      "
        }
    },
    aggs={
        "sales_over_time": {
            "date_histogram": {
                "field": "date.promoted_is_tomorrow",
                "calendar_interval": "1M"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    runtime_mappings: {
      'date.promoted_is_tomorrow' => {
        type: 'date',
        script: "\n        long date = doc['date'].value.toInstant().toEpochMilli();\n        if (doc['promoted'].value) {\n          date += 86400;\n        }\n        emit(date);\n      "
      }
    },
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date.promoted_is_tomorrow',
          calendar_interval: '1M'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "sales",
  size: 0,
  runtime_mappings: {
    "date.promoted_is_tomorrow": {
      type: "date",
      script:
        "\n        long date = doc['date'].value.toInstant().toEpochMilli();\n        if (doc['promoted'].value) {\n          date += 86400;\n        }\n        emit(date);\n      ",
    },
  },
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date.promoted_is_tomorrow",
        calendar_interval: "1M",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "runtime_mappings": {
    "date.promoted_is_tomorrow": {
      "type": "date",
      "script": """
        long date = doc['date'].value.toInstant().toEpochMilli();
        if (doc['promoted'].value) {
          date += 86400;
        }
        emit(date);
      """
    }
  },
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date.promoted_is_tomorrow",
        "calendar_interval": "1M"
      }
    }
  }
}

Параметры

Вы можете контролировать порядок возвращаемых корзин с помощью настроек order и фильтровать возвращаемые корзины на основе настроек min_doc_count (по умолчанию возвращаются все корзины между первой корзиной, соответствующей документам, и последней). Эта гистограмма также поддерживает настройку extended_bounds, которая позволяет расширить границы гистограммы за пределы самих данных, и настройку hard_bounds, которая ограничивает гистограмму заданными границами. Дополнительную информацию см. в Extended Bounds и Hard Bounds.

Отсутствующее значение

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

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "sale_date": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "year",
                "missing": "2000/01/01"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sale_date: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'year',
          missing: '2000/01/01'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sale_date": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "year",
	        "missing": "2000/01/01"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sale_date: {
      date_histogram: {
        field: "date",
        calendar_interval: "year",
        missing: "2000/01/01",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sale_date": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "year",
        "missing": "2000/01/01" 
      }
    }
  }
}

Документы без значения в поле date попадут в ту же корзину, что и документы со значением 2000-01-01.

Порядок

По умолчанию возвращаемые корзины упорядочиваются по их значению key в порядке возрастания, но вы можете управлять порядком с помощью настройки order. Эта настройка поддерживает ту же функциональность order, что и Terms Aggregation.

Использование скрипта для агрегации по дням недели

Когда вам нужно агрегировать результаты по дням недели, выполните агрегацию terms по полю runtime, которое возвращает день недели:

resp = client.search(
    index="sales",
    size="0",
    runtime_mappings={
        "date.day_of_week": {
            "type": "keyword",
            "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
        }
    },
    aggs={
        "day_of_week": {
            "terms": {
                "field": "date.day_of_week"
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "sales",
  size: 0,
  runtime_mappings: {
    "date.day_of_week": {
      type: "keyword",
      script:
        "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))",
    },
  },
  aggs: {
    day_of_week: {
      terms: {
        field: "date.day_of_week",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "runtime_mappings": {
    "date.day_of_week": {
      "type": "keyword",
      "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
    }
  },
  "aggs": {
    "day_of_week": {
      "terms": { "field": "date.day_of_week" }
    }
  }
}

Ответ:

{
  ...
  "aggregations": {
    "day_of_week": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Sunday",
          "doc_count": 4
        },
        {
          "key": "Thursday",
          "doc_count": 3
        }
      ]
    }
  }
}

Ответ будет содержать все корзины, имеющие соответствующий день недели в качестве ключа: 1 для понедельника, 2 для вторника… 7 для воскресенья.

© 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-aggregations-bucket-datehistogram-aggregation.html

Spec-Zone.ru

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