Запрос денормализованных данных дерева в Elasticsearch

#elasticsearch

#elasticsearch

Вопрос:

У меня есть данные дерева, хранящиеся в Elasticsearch 7.9 со структурой данных, описанной ниже. Я пытаюсь написать запрос, который может предоставить 10 лучших дочерних элементов, у которых под ними больше всего дочерних элементов.


Установочные данные

Учитывая этот пример дерева:

пример дерева

описывается следующими данными в ES:

 { "id": "A", "name": "User A" }
{ "id": "B", "name": "User B", "parents": ["A"], "parent1": "A" }
{ "id": "C", "name": "User C", "parents": ["A"], "parent1": "A" }
{ "id": "D", "name": "User D", "parents": ["A", "B"], "parent1": "B", "parent2": "A" }
{ "id": "E", "name": "User E", "parents": ["A", "B", "D"], "parent1": "D", "parent2": "B", "parent2": "A" }
 

каждое поле соответствует типу сопоставления keyword

Поля документа:

  • «id» — идентификатор документа, такой же, как _id,
  • «родители» — все родители документа или пустые, если это корневой узел
  • «parent1» — родительский элемент документа
  • «parent2» — общий родительский элемент документа
  • «parent N» — N-й прародитель до 5

Желаемые результаты

Я хотел бы найти всех «родителей» от пользователя A и общее count количество дочерних элементов. Итак, в этом примере результаты будут

 User B - 2
User C - 0
 

Проверьте это самостоятельно

 PUT test_index
PUT test_index/_mapping
{
  "properties": {
    "id": { "type": "keyword" },
    "name": { "type": "keyword" },
    "referred_by_sub": { "type": "keyword" },
    "parents": { "type": "keyword" },
    "parent1": { "type": "keyword" },
    "parent2": { "type": "keyword" },
    "parent3": { "type": "keyword" },
    "parent4": { "type": "keyword" },
    "parent5": { "type": "keyword" }
  }
}

POST _bulk
{ "index" : { "_index" : "test_index", "_id" : "A" } }
{ "id": "A", "name": "User A" }
{ "index" : { "_index" : "test_index", "_id" : "B" } }
{ "id": "B", "name": "User B", "parents": ["A"], "parent1": "A" }
{ "index" : { "_index" : "test_index", "_id" : "C" } }
{ "id": "C", "name": "User C", "parents": ["A"], "parent1": "A" }
{ "index" : { "_index" : "test_index", "_id" : "D" } }
{ "id": "D", "name": "User D", "parents": ["A", "B"], "parent1": "B", "parent2": "A" }
{ "index" : { "_index" : "test_index", "_id" : "E" } }
{ "id": "E", "name": "User E", "parents": ["A", "B", "D"], "parent1": "D", "parent2": "B", "parent2": "A" }
 

Окончательный результат расширен из ответа Джо

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

Возможно, это поможет кому-то в будущем.

Запрос

 GET test_index/_search
{
  "size": 0,
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "id": "A"
          }
        },
        {
          "term": {
            "parents": "A"
          }
        }
      ]
    }
  },
  "aggs": {
    "children_counter": {
      "scripted_metric": {
        "init_script": "state.ids_vs_children = [:]; state.root_children = [:]",
        "map_script": """
          def current_id = doc['id'].value;
          if (!state.ids_vs_children.containsKey(current_id)) {
            state.ids_vs_children[current_id] = new ArrayList();
          }
          
          if(doc['parent1'].contains(params.id)) {
            state.root_children[current_id] = params._source;
          }
          
          def parents = doc['parents'];
          if (parents.size() > 0) {
            for (def p : parents) {
              if (!state.ids_vs_children[current_id].contains(p)) {
                if (!state.ids_vs_children.containsKey(p)) {
                  state.ids_vs_children[p] = new ArrayList();
                }
                state.ids_vs_children[p].add(current_id);
              }
            }
          }
        """,
        "combine_script": """
          def results = [];
          for (def pair : state.ids_vs_children.entrySet()) {
            def uid = pair.getKey();
            if (!state.root_children.containsKey(uid)) {
              continue;
            }
            
            def doc_map = [:];
            doc_map["doc"] = state.root_children[uid];
            doc_map["num_children"] = pair.getValue().size();
            results.add(doc_map);
          }
      
          def final_result = [:];
          final_result['count'] = results.length;
          final_result['results'] = results;
          return final_resu<
        """,
        "reduce_script": "return states",
        "params": {
          "id": "A"
        }
        
      }
    }
  }
}
 

Вывод

 {
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "children_counter" : {
      "value" : [
        {
          "count" : 2,
          "results" : [
            {
              "num_children" : 1,
              "doc" : {
                "parent1" : "A",
                "name" : "User B",
                "id" : "B",
                "parents" : [
                  "A"
                ]
              }
            },
            {
              "num_children" : 0,
              "doc" : {
                "parent1" : "A",
                "name" : "User C",
                "id" : "C",
                "parents" : [
                  "A"
                ]
              }
            }
          ]
        }
      ]
    }
  }
}
 

Ответ №1:

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

 GET test_index/_search
{
  "size": 0,
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "id": "A"
          }
        },
        {
          "term": {
            "parents": "A"
          }
        }
      ]
    }
  },
  "aggs": {
    "children_counter": {
      "scripted_metric": {
        "init_script": "state.ids_vs_children = [:];",
        "map_script": """
          def current_id = doc['id'].value;
          if (!state.ids_vs_children.containsKey(current_id)) {
            state.ids_vs_children[current_id] = new ArrayList();
          }
          
          def parents = doc['parents'];
          if (parents.size() > 0) {
            for (def p : parents) {
              if (!state.ids_vs_children[current_id].contains(p)) {
                state.ids_vs_children[p].add(current_id);
              }
            }
          }
        """,
        "combine_script": """
          def final_map = [:];
          for (def pair : state.ids_vs_children.entrySet()) {
            def uid = pair.getKey();
            if (params.exclude_users != null amp;amp; params.exclude_users.contains(uid)) {
              continue;
            }
            
            final_map[uid] = pair.getValue().size();
          }
      
          return final_map;
        """,
        "reduce_script": "return states",
        "params": {
          "exclude_users": ["A"]
        }
      }
    }
  }
}
 

выдача

 ...
"aggregations" : {
  "children_counter" : {
    "value" : [
      {
        "B" : 2,    <--
        "C" : 0,    <--
        "D" : 1,
        "E" : 0
      }
    ]
  }
}
 

Настоятельно рекомендуется использовать запрос верхнего уровня, чтобы не перегружать процессор. Подобные сценарии b / c, как известно, требуют больших ресурсов.
Требуется запрос верхнего уровня, чтобы ограничить это только дочерними элементами A .

Совет: если вы не слишком часто обновляете этих пользователей, я бы посоветовал выполнить это дочернее вычисление перед индексацией — вам придется где-то выполнять итерации, так почему бы не за пределами ES?

Комментарии:

1. Потрясающе, да, это отлично работает. Я согласен, что нам, вероятно, следует обновлять эти данные при приеме, потому что это не сильно меняется, но ваш код здесь должен пройти пока. Большое спасибо!

2. Звучит неплохо. Никаких проблем, рад помочь! Эй, бесстыдный плагин: я пишу руководство по Elasticsearch. О чем еще вы хотели бы узнать?