У меня есть данные дерева, хранящиеся в Elasticsearch 7.9, со структурой данных, описанной ниже. Я пытаюсь написать запрос, который может дать 10 лучших детей, у которых больше всего детей под ними.
Учитывая этот пример дерева:
описывается следующими данными в ЭС:
{ "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
Поля документа:
Я хотел бы найти всех «родителей» от пользователя А и всего 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_result;
""",
"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"
]
}
}
]
}
]
}
}
}
Ваше денормализованное дерево уже содержит все, что вам нужно для этого расчета, но нам потребуется доступ к родительским документам других документов, поскольку мы просматриваем дочерние элементы и отслеживаем ссылки, так что это идеальный вариант использования для агрегации метрик по сценарию.
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 && 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?
Звучит отлично. Не за что, рад помочь! Эй, бессовестный плагин: я пишу руководство по Elasticsearch. 🔁 О чем бы вы еще хотели узнать?
Потрясающе, да, это отлично работает. Я согласен, что нам, вероятно, следует обновить эти данные при приеме, потому что они не сильно меняются, но ваш код здесь должен пока работать. Большое спасибо!