Агрегация по нескольким дочерним полям

Добрый день.
Есть элементы с двумя дочерними элементами (тип join).
Необходимо сделать агрегацию по минимальному значению child_value для answer2. И, если answer2 отсутствует для родителя, то вместо него использовать answer1.
Пример:

Индекс

    PUT my_index
    {
      "mappings": {
        "_doc": {
          "properties": {
            "my_join_field": { 
              "type": "join",
              "relations": {
                "question": ["answer1", "answer2"]
              }
            },
            "child_value": { 
              "type": "float"
            }
          }
        }
      }
    }

Основные данные

    PUT my_index/_doc/1?refresh
    {
      "text": "This is a question",
      "my_join_field": {
        "name": "question" 
      }
    }
    PUT my_index/_doc/2?refresh
    {
      "text": "This is a question 2",
      "my_join_field": {
        "name": "question" 
      }
    }

Дочерние данные

    PUT my_index/_doc/3?routing=1&refresh 
    {
      "my_join_field": {
        "name": "answer1", 
        "parent": "1" 
      },
      "child_value": 10
    }
    PUT my_index/_doc/4?routing=1&refresh 
    {
      "my_join_field": {
        "name": "answer2", 
        "parent": "1" 
      },
      "child_value": 15
    }
    PUT my_index/_doc/5?routing=1&refresh
    {
      "my_join_field": {
        "name": "answer1",
        "parent": "2"
      },
      "child_value": 13
    }

Ожидаемый результат: 13

Можно так. Результат - это меньшее значение между min_answer_1 и min_answer_2

POST my_index/_search
{
  "size": 0,
  "aggs": {
    "without_answer_2": {
      "filter": {
        "bool": {
          "must_not": [
            {
              "has_child": {
                "type": "answer2",
                "query": {
                  "match_all": {}
                }
              }
            }
          ]
        }
      },
      "aggs": {
        "answer_1": {
          "children": {
            "type": "answer1"
          },
          "aggs": {
            "min_answer_1": {
              "min": {
                "field": "child_value"
              }
            }
          }
        }
      }
    },
    "answer_2": {
      "children": {
        "type": "answer2"
      },
      "aggs": {
        "min_answer_2": {
          "min": {
            "field": "child_value"
          }
        }
      }
    }
  }
}

Если надо все в одном флаконе, то вот такое безобразие должно работать. Ответ в final_answer. fake_top_level добавлен из-за https://github.com/elastic/elasticsearch/issues/14600 без верхнего уровня bucket_script работать не будет.

POST my_index/_search
{
  "size": 0, 
  "aggs": {
    "fale_top_level": {
      "filters": {
        "filters": {"all": {"match_all": {}}}
      },
      "aggs": {
        "without_answer_2": {
          "filter": {
            "bool": {
              "must_not": [
                {
                  "has_child": {
                    "type": "answer2",
                    "query": {
                      "match_all": {}
                    }
                  }
                }
              ]
            }
          },
          "aggs": {
            "answer_1": {
              "children": {
                "type": "answer1"
              },
              "aggs": {
                "min_answer_1": {
                  "min": {
                    "field": "child_value"
                  }
                }
              }
            }
          }
        },
        "answer_2": {
          "children": {
            "type": "answer2"
          },
          "aggs": {
            "min_answer_2": {
              "min": {
                "field": "child_value"
              }
            }
          }
        },
        "final_answer": {
          "bucket_script": {
            "buckets_path": {
              "min_without": "without_answer_2>answer_1>min_answer_1",
              "min_with": "answer_2>min_answer_2"
            },
            "script": "params.min_without"
          }
        }
      }
    }
  }
}
1 Like

К сожалению первый вариант возвращает меньшее из всех, а мне требуется меньше из answer2 (а если для родителя нет answer2, то для этого родителя брать answer1). Если переводить в SQL, то примерно вот такое:

SELECT MIN( IF(answer2.child_value IS NOT NULL, answer2.child_value, answer1.child_value) )
FROM doc
LEFT JOIN answer2 ON answer2.parent = doc.id
LEFT JOIN answer1 ON answer1.parent = doc.id

Оба варианта возваршают одно и то же. Только в первом варианте вам надо взять меньшее значение из двух в ручную.

За это отвечает эта часть:

"filter": {
            "bool": {
              "must_not": [
                {
                  "has_child": {
                    "type": "answer2",
                    "query": {
                      "match_all": {}
                    }
                  }
                }
              ]
            }

Если что-то не работает, пришлите, пожалуйста данные, на которых пример не работает вместе с результатом, который вы ожидаете.

В данный момент в обоих случаях ответ - 13, это то, что вы ожидали.

Да, все заработало, спасибо большое! С первого раза не срабатывало из-за ошибки данных.
Может быть еще подскажите, отфильтровать родителей по таким же "пустым" дочерним элементам возможно?

Что значит пустым?

Для приведенной выше структуры, если родителя отсутствует дочерний answer2, то фильтрацию применять по значению answer1

Получил вот такое решение, может быть кому-то пригодится

{
    "bool": {
        "should": {
            {
                "has_child": {
                    "type": "answer2",
                    "query": {...Filter...}
                }
            },
            {
                "bool": {
                    "must": {
                        "has_child": {
                            "type": "answer1"
                            "query": {...Filter...}
                        }
                    },
                    "must_not": {
                        "has_child": {
                            "type": "answer2",
                            "query": {
                                "match_all": {}
                            }
                        }
                    }
                }
            }
        }
    }
}