Published on May 2, 2019
in category programming
In this post we are going to manage nested objects of a document indexed with Elasticsearch.
The nested type is a specialised version of the object datatype that allows arrays of objects to be indexed in a way that they can be queried independently of each other. – Nested datatype - Official Elasticsearch reference
To follow this post you need:
The document of our index will represent a human and its nested objects will be cats (no surprises).
Open your Kibana dev console and type the following to create the index.
# Create the index
PUT iridakos_nested_objects
{
"mappings": {
"human": {
"properties": {
"name": {
"type": "text"
},
"cats": {
"type": "nested",
"properties": {
"colors": {
"type": "integer"
},
"name": {
"type": "text"
},
"breed": {
"type": "text"
}
}
}
}
}
}
}
Human has:
name
property of type text
cats
property of type nested
Each cat has:
colors
property of type integer
name
property of type text
breed
property of type text
In the Kibana console, execute the following to add a human with three cats.
# Index a human
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"
}
]
}
Confirm the insertion with:
GET iridakos_nested_objects/human/1
You should see something like this:
{
"_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"
}
]
}
}
Done, moving on.
Suppose that iridakos
got a new Persian cat named Leon
. To add it in iridakos’ collection of cats we will use the Update API.
In Kibana:
# Add a new cat
POST iridakos_nested_objects/human/1/_update
{
"script": {
"source": "ctx._source.cats.add(params.cat)",
"params": {
"cat": {
"colors": 4,
"name": "Leon",
"breed": "Persian"
}
}
}
}
Notes:
ctx._source.cats
. This gave us a collectionparams.cat
) were passed as parameters in the params
attribute of the request under the attribute cat
.Confirm the addition with:
GET iridakos_nested_objects/human/1
Cat added:
{
"_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"
}
]
}
}
Suppose we want to remove Nino
from the human’s cat collection.
In Kibana:
# Remove Nino
POST iridakos_nested_objects/human/1/_update
{
"script": {
"source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
"params": {
"cat_name": "Nino"
}
}
}
Notes:
ctx._source.cats
. This gave us a collectionremoveIf
method in which we specify which items we want to remove. This predicate will be executed on each item of the collection and resolves to a Boolean value. If the resolution is true
then the item will be removed. In our case, the condition is a simple equality check on the cat
’s name attribute.cat_name
was passed as a parameter (params.cat_name
) instead of fixing it to the script source.Confirm the addition with:
GET iridakos_nested_objects/human/1
Cat removed:
{
"_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"
}
]
}
}
Suppose we want to change all cat breeds from European
to European Shorthair
(Phoebe is the only one in our case).
# Update breed
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"
}
}
}
Notes:
ctx._source.cats
. This gave us a collectionfindAll
method in which we specify which items we want to select. This predicate will be executed on each item of the collection and resolves to a Boolean value. If the resolution is true
then the item will be selected. In our case, the condition is a simple equality check on the cat
’s breed attribute.current_breed
was passed as a parameter (params.current_breed
) instead of fixing it to the script source.breed
attribute has value European
) and change their breed to the new value which we passed by another parameter breed
.Confirm the change:
GET iridakos_nested_objects/human/1
Cat updated:
{
"_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"
}
]
}
}
Now, in a more advanced example, we are going to use a more flexible script to:
Suppose we want to change the breed of cats who have 4 colors
and their breed is Persian
to Aegean
and their colors to 3
.
The script we will use is the following:
# Update multiple attributes with multiple conditions
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
}
}
}
}
For convenience, here’s the script source with proper indentation.
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()
}
}
Notes:
params.conditions
parameter.params.changes
parameter.Confirm:
GET iridakos_nested_objects/human/1
Cat updated.
{
"_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"
}
]
}
}
That’s all! Cat photo.