在Elasticsearch的文档中如何管理嵌套对象(nested objects)?

本文翻译自《How to manage nested objects in Elasticsearch documents》。

如何使用Update API和painless脚本在Elasticsearch文档中添加、更新和删除嵌套对象。

2019/05 /02

在这篇文章中,我们将管理使用Elasticsearch索引的文档的嵌套对象(nested objects)

嵌套类型(nested)是对象类型(object)的一个特殊版本,它对对象数组进行索引,以使数组的元素可以被相互独立地查询。

先决条件

要跟随此帖子继续下去,你需要:

  • 一个正在运行的Elasticsearch实例。我在这里用6.7版本
  • 一个正在运行的Kibana实例,用于与Elasticsearch交互

准备

我们创建一个索引iridakos_nested_objects,它有一个名为human的类型(type)(Elasticsearch 8开始没有type这个概念了),其中有一个嵌套对象cats。

创建这个索引

打开Kibana开发控制台并键入以下内容以创建这个索引。

PUT iridakos_nested_objects
{
  "mappings": {
    "human": {
      "properties": {
        "name": {
          "type": "text"
        },
        "cats": {
          "type": "nested",
          "properties": {
            "colors": {
              "type": "integer"
            },
            "name": {
              "type": "text"
            },
            "breed": {
              "type": "text"
            }
          }
        }
      }
    }
  }
}

// ES 8版本的语句
PUT iridakos_nested_objects
{
  "mappings": {
      "properties": {
        "name": {
          "type": "text"
        },
        "cats": {
          "type": "nested",
          "properties": {
            "colors": {
              "type": "integer"
            },
            "name": {
              "type": "text"
            },
            "breed": {
              "type": "text"
            }
          }
        }
      }
    }
}

返回响应信息:

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "iridakos_nested_objects"
}

human具有:

  • text类型的name属性
  • 嵌套类型(nested)的cats属性

每只cat都有:

  • integer类型的colors属性
  • text类型的name属性
  • text类型的一个品种特性

添加一个human

在Kibana控制台中,执行以下操作以添加一个human和三只cat。

POST iridakos_nested_objects/human/1
{
  "name": "iridakos",
  "cats": [
    {
      "colors": 1,
      "name": "Irida",
      "breed": "European Shorthair"
    },
    {
      "colors": 2,
      "name": "Phoebe",
      "breed": "European"
    },
    {
      "colors": 3,
      "name": "Nino",
      "breed": "Aegean"
    }
  ]
}

// ES 8版本的语句
PUT iridakos_nested_objects/_doc/1
{
  "name": "iridakos",
  "cats": [
    {
      "colors": 1,
      "name": "Irida",
      "breed": "European Shorthair"
    },
    {
      "colors": 2,
      "name": "Phoebe",
      "breed": "European"
    },
    {
      "colors": 3,
      "name": "Nino",
      "breed": "Aegean"
    }
  ]
}

查询看看是否真的插入成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

你应该能看到类似以下的响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 3,
        "name": "Nino",
        "breed": "Aegean"
      }
    ]
  }
}

管理嵌套对象

添加一个新的嵌套对象

假设iridakos得到了一只新的波斯猫,名叫Leon。要将其添加到iridakos索引的cats集合中,我们将使用更新API。 在Kibana:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "ctx._source.cats.add(params.cat)",
    "params": {
      "cat": {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "ctx._source.cats.add(params.cat)",
    "params": {
      "cat": {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了human的嵌套对象cats。这给了我们一个集合(collection)
  • 我们对集合执行了add方法以添加一只新的cat
  • 新cat(params.cat)作为参数传递给add方法,其属性在params中给出。

查询看看是否真的插入成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 3,
        "name": "Nino",
        "breed": "Aegean"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见,已插入Leon到cats集合。

删除一个嵌套对象

假设我们想把Nino从cats集合中移除。在Kibana:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
    "params": {
      "cat_name": "Nino"
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
    "params": {
      "cat_name": "Nino"
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了嵌套对象cat。这给了我们一个集合(collection)
  • 我们对集合执行removeIf方法,以有条件地移除里面的条目项
  • 我们为removeIf方法提供了一个谓词(Predicate),在该方法中指定要删除的条目项。此谓词将在集合的每个条目项上执行,并返回为布尔值。如果返回true,则该条目项将被删除。在我们的例子中,条件是对cat的name属性进行简单的相等性检查。
  • cat_name是作为参数(params.cat_name)传递的,而不是将其固定到source脚本里。

执行以下语句查看一下是否删除Nino成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见name为Nino的cat信息已在cats集合中删除。

更新嵌套对象

假设我们想把所有的猫品种从欧洲(European)改为欧洲短毛猫(European Shorthair)(在我们的案例中目前只有Phoebe的品种是European)。

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> cat.breed == params.current_breed); for(cat in targets) { cat.breed = params.breed }",
    "params": {
      "current_breed": "European",
      "breed": "European Shorthair"
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> cat.breed == params.current_breed); for(cat in targets) { cat.breed = params.breed }",
    "params": {
      "current_breed": "European",
      "breed": "European Shorthair"
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了嵌套对象cat。这给了我们一个集合(collection)
  • 我们对cats集合执行了findAll方法来选择特定的条目项
  • 我们为findAll方法提供了一个谓词(Predicate),在其中我们指定要选择的条目项。此谓词将在集合的每个条目项上执行,并返回布尔值。如果返回true,就会选择该条目项。在我们的例子中,条件是对猫的品种(breed)属性进行简单的相等性检查。
  • current_breed作为参数(params.current_breed)传递,而不是将其固定到source脚本里。
  • 然后,我们循环遍历选中的猫(其品种属性值为European),并将它们的品种更改为我们通过另一个参数params.breed传递的新值。

查看是否更新成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European Shorthair"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见更新成功。

更新满足多个条件的嵌套对象的多个属性

现在,在一个更高级的示例中,我们将使用一个更灵活的脚本来:

  • 基于多种条件来匹配目标对象(此处为color和breed)
  • 更新多个属性(此处为color和breed)

假设我们想改变有4种颜色的猫的品种,它们的品种从Persian到Aegean,它们的颜色改为3种。使用下面的脚本:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> { for (condition in params.conditions.entrySet()) { if (cat[condition.getKey()] != condition.getValue()) { return false; } } return true; }); for (cat in targets) { for (change in params.changes.entrySet()) { cat[change.getKey()] = change.getValue() } }",
    "params": {
      "conditions": {
        "breed": "Persian",
        "colors": 4
      },
      "changes": {
        "breed": "Aegean",
        "colors": 3
      }
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> { for (condition in params.conditions.entrySet()) { if (cat[condition.getKey()] != condition.getValue()) { return false; } } return true; }); for (cat in targets) { for (change in params.changes.entrySet()) { cat[change.getKey()] = change.getValue() } }",
    "params": {
      "conditions": {
        "breed": "Persian",
        "colors": 4
      },
      "changes": {
        "breed": "Aegean",
        "colors": 3
      }
    }
  }
}

为了方便起见,下面是带有适当缩进的脚本的源代码。

注意事项:

  • 我们通过检查cat的属性是否具有params.conditions中指定的值来选择要更新的cat。
  • 对于每个选定的cat,我们按照params.changes中给出的值更改它对应的属性值。

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 5,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 4,
  "_primary_term": 1
}

确认一下是否更新成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European Shorthair"
      },
      {
        "name": "Leon",
        "colors": 3,
        "breed": "Aegean"
      }
    ]
  }
}

可见,更新成功。

参考

https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html

https://www.elastic.co/guide/en/elasticsearch/painless/6.7/painless-api-reference.html

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注