Skip to content

Instantly share code, notes, and snippets.

@etki
Last active September 20, 2023 00:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save etki/bd585835c269f9b8211c9c9edf6592e8 to your computer and use it in GitHub Desktop.
Save etki/bd585835c269f9b8211c9c9edf6592e8 to your computer and use it in GitHub Desktop.
Elasticsearch + экзистенциальные запросы

Перекладывание SQL-запроса на эластик без изменения формата данных (трансформирования перед индексацией)

Вводные:

  • Документы с коллекциями nested.
  • Коллекция представляет собой историю одной и той же сущности. Одна версия является актуальной и отличается отсутствием флага archived.
  • Актуальная версия может как присутствовать, так и нет. В случае отсутствия поиск производится по архивированным версиям.
  • Nested ожидаемо содержит прочие данные, которые участвуют в запросе.

Решение состоит из композиции логических запросов:

or:
  - and:
    # проверка что актуальная версия вообще существует
    - nested:
        not:
          exists: archived # alt. archived = false
    - nested:
        and:
          # проверка что конкретная версия (из всех доступных) 
          # является актуальной
          - not:
              exists: archived
          - <стандартный запрос>
  - and: 
    # проверка несуществования nested без archived,
    # i.e. что актуальной версии попросту нет
    # два not нельзя схлопнуть из-за наличия nested
    # (выступающего экзистенциальным квантификатором)
    # между ними. Если бы в эластике был универсальный 
    # for-all, тогда бы это свелось к for all ... exists: archived
    - not:
        nested:
          not:
            exists: archived
    # and на archived = true не требуется, т.к. он подразумевается
    # выполнением предыдущего подзапроса
    - nested:
        <стандартный запрос>

см. query.json

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

aggs:
  immerse:
    nested:
      path: items
      aggs:
        counter:
          reverse_nested: {}

Результат:

{
  "aggregations" : {
    "immerse" : {
      "doc_count" : 5,
      "counter" : {
        "doc_count" : 3 // << искомое значение
      }
    }
  }
}

Setup & Query

ADDRESS=localhost:9200

curl -XPUT -H 'Content-Type: application/json' "$ADDRESS/playground?pretty" -d @index.json
for document in $(find . -name "document.*"); do
  local id=$(basename "$document" .json)
  curl -XPUT -H 'Content-Type: application/json' "$ADDRESS/playground/_doc/$id?pretty" -d "@$document"
done

curl -XPOST -H 'Content-Type: application/json' "$ADDRESS/playground/_refresh?pretty"
curl -XPOST -H 'Content-Type: application/json' "$ADDRESS/playground/_search?pretty" -d @query.json
{
"id": "matching.actual",
"items": [
{
"category": 1,
"timestamp": "2022-02-02T00:00:00.000Z"
}
]
}
{
"id": "matching.archived",
"items": [
{
"archived": true,
"category": 1,
"timestamp": "2022-02-02T00:00:00.000Z"
}
]
}
{
"id": "matching.combined",
"items": [
{
"category": 1,
"timestamp": "2022-02-02T02:00:00.000Z"
},
{
"archived": true,
"category": 3,
"timestamp": "2022-02-02T01:00:00.000Z"
},
{
"archived": true,
"category": 2,
"timestamp": "2022-02-02T00:00:00.000Z"
}
]
}
{
"id": "mismatching.actual",
"items": [
{
"category": 2,
"timestamp": "2022-02-02T00:00:00.000Z"
}
]
}
{
"id": "mismatching.archived",
"items": [
{
"archived": true,
"category": 2,
"timestamp": "2022-02-02T00:00:00.000Z"
}
]
}
{
"id": "mismatching.combined",
"items": [
{
"category": 2,
"timestamp": "2022-02-02T01:00:00.000Z"
},
{
"archived": true,
"category": 1,
"timestamp": "2022-02-02T00:00:00.000Z"
}
]
}
{
"settings": {
"number_of_replicas": 0
},
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"items": {
"type": "nested",
"properties": {
"archived": {
"type": "boolean"
},
"category": {
"type": "integer"
},
"@counter": {
"type": "integer"
}
}
}
}
}
}
{
"query": {
"bool": {
"should": [
{
"bool": {
"must": [
{
"nested": {
"path": "items",
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "items.archived"
}
}
]
}
}
}
},
{
"nested": {
"path": "items",
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "items.archived"
}
}
],
"must": [
{
"terms": {
"items.category": [0, 1, 987]
}
}
]
}
}
}
}
]
}
},
{
"bool": {
"must_not": [
{
"nested": {
"path": "items",
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "items.archived"
}
}
]
}
}
}
}
],
"must": [
{
"nested": {
"path": "items",
"query": {
"terms": {
"items.category": [0, 1, 987]
}
}
}
}
]
}
}
]
}
},
"aggs": {
"immerse": {
"nested": {
"path": "items"
},
"aggs": {
"counter": {
"reverse_nested": {}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment