Search code examples
elasticsearchnest

Searching Complex Nested JSON Objects in Elasticsearch


How can I efficiently search nested JSON objects in Elasticsearch and return only the relevant data without returning the entire document?

I have a JSON object representing a menu structure that will be ingested per user. Each menu item has nested fields such as Text, Url, Area, and Children. I would like to be able to search on any of these fields and return only the relevant data for the search query. For example, if I search for 'Workflow Roles', I would like to only return the relevant menu item and not all the extra data.

I have tried creating a nested mapping for the menu object, but my queries using inner hits return too much data. I have also experimented with path filters but have not found a satisfactory solution.

So my questions are:

  1. What is the best way to ingest a deeply nested JSON object in Elasticsearch for efficient search?
  2. How should the mapping be structured for such a deeply nested object?
  3. How can I write a query that will allow me to search on all nested fields and return only the relevant data for the search query?

Here is an example of the JSON object that needs to be searched:

[
  {
    "MenuStructure": 0,
    "NavigationLinkId": 0,
    "Children": [
      {
        "MenuStructure": 0,
        "NavigationLinkId": 111,
        "Text": "Bulk Actions",
        "Action": "Index",
        "Controller": "BulkUpload",
        "Area": "Company",
        "Url": "/some-url/bulk-upload",
      }
    ]
  },
  {
    "MenuStructure": 1,
    "NavigationLinkId": 1,
    "Children": [
      {
        "MenuStructure": 1,
        "NavigationLinkId": 2,
        "Text": "Company Config",
        "Area": "Company",
        "Url": "javascript:void(0);",
        "Children": [
          {
            "MenuStructure": 1,
            "NavigationLinkId": 0,
            "Text": "Basic Settings",
            "Url": "javascript:void(0);",
            "Children": [
              {
                "MenuStructure": 1,
                "NavigationLinkId": 60668,
                "Text": "Company Settings",
                "Icon": "fas fa-cogs",
                "Url": "javascript:void(0);",
                "Children": [
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 27,
                    "Text": "Basic Company Information",
                    "Icon": "fa fa-building",
                    "Action": "Index",
                    "Controller": "CompanyProfile",
                    "Area": "Company",
                    "Url": "/some-url/company-profile",
                  },
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 163,
                    "Text": "Reminder Contact Details",
                    "Icon": "fas fa-balance-scale-right",
                    "Url": "/some-url/classic/163",
                  }
                ]
              },
              {
                "MenuStructure": 1,
                "NavigationLinkId": 0,
                "Text": "Dropdown Management",
                "Icon": "fas fa-users-class",
                "Url": "javascript:void(0);",
                "Children": [
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 261,
                    "Text": "Workflow Roles",
                    "Action": "Index",
                    "Controller": "CompanyWorkflowRoles",
                    "Area": "Company",
                    "Url": "/some-url/workflow-roles"
                  },
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 50323,
                    "Text": "Other Dropdowns",
                    "Icon": "fas fa-balance-scale-right",
                    "Url": "/some-url/classic/50323"
                  }
                ]
              },
              {
                "MenuStructure": 1,
                "NavigationLinkId": 50327,
                "Text": "Legislative Configurations",
                "Icon": "fas fa-balance-scale-right",
                "Url": "javascript:void(0);",
                "Children": [
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 244,
                    "Text": "Statistics SA Config",
                    "Url": "/some-url/classic/244"
                  }
                ]
              }
            ]
          },
          
          {
            "MenuStructure": 1,
            "NavigationLinkId": 167,
            "Text": "Security",
            "Icon": "fa fa-lock",
            "Url": "javascript:void(0);",
            "Children": [
              {
                "MenuStructure": 1,
                "NavigationLinkId": 167,
                "Text": "Security",
                "Icon": "fa fa-lock",
                "Url": "javascript:void(0);",
                "Children": [
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 168,
                    "Text": "User Profiles",
                    "Url": "/some-url/classic/168",
                  },
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 169,
                    "Text": "Security Roles",
                    "Url": "/some-url/classic/169",
                  },
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 188,
                    "Text": "User Org. Unit Permissions",
                    "Url": "/some-url/classic/188",
                  },
                  {
                    "MenuStructure": 1,
                    "NavigationLinkId": 60610,
                    "Text": "Activate Users",
                    "Url": "/some-url/classic/60610",
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  }
]

To achieve this, I have tried creating a nested field type mapping for the menu object. Here is an example mapping:

PUT my-index
{
  "mappings": {
    "properties": {
      "user_id": {
        "type": "keyword"
      },
      "menu": {
        "type": "nested",
        "properties": {
          "MenuStructure": {
            "type": "long"
          },
          "NavigationLinkId": {
            "type": "long"
          },
          "Text": {
            "type": "text"
          },
          "Action": {
            "type": "keyword"
          },
          "Controller": {
            "type": "keyword"
          },
          "Area": {
            "type": "keyword"
          },
          "Url": {
            "type": "keyword"
          },
          "Icon": {
            "type": "keyword"
          },
          "Children": {
            "type": "nested",
            "properties": {
              "MenuStructure": {
                "type": "long"
              },
              "NavigationLinkId": {
                "type": "long"
              },
              "Text": {
                "type": "text"
              },
              "Action": {
                "type": "keyword"
              },
              "Controller": {
                "type": "keyword"
              },
              "Area": {
                "type": "keyword"
              },
              "Url": {
                "type": "keyword"
              },
              "Icon": {
                "type": "keyword"
              },
              "Children": {
                "type": "nested",
                "properties": {
                  "MenuStructure": {
                    "type": "long"
                  },
                  "NavigationLinkId": {
                    "type": "long"
                  },
                  "Text": {
                    "type": "text"
                  },
                  "Action": {
                    "type": "keyword"
                  },
                  "Controller": {
                    "type": "keyword"
                  },
                  "Area": {
                    "type": "keyword"
                  },
                  "Url": {
                    "type": "keyword"
                  },
                  "Icon": {
                    "type": "keyword"
                  },
                  "Children": {
                    "type": "nested",
                    "properties": {
                      "MenuStructure": {
                        "type": "long"
                      },
                      "NavigationLinkId": {
                        "type": "long"
                      },
                      "Text": {
                        "type": "text"
                      },
                      "Action": {
                        "type": "keyword"
                      },
                      "Controller": {
                        "type": "keyword"
                      },
                      "Area": {
                        "type": "keyword"
                      },
                      "Url": {
                        "type": "keyword"
                      },
                      "Icon": {
                        "type": "keyword"
                      },
                      "Children": {
                        "type": "nested",
                        "properties": {
                          "MenuStructure": {
                            "type": "long"
                          },
                          "NavigationLinkId": {
                            "type": "long"
                          },
                          "Text": {
                            "type": "text"
                          },
                          "Action": {
                            "type": "keyword"
                          },
                          "Controller": {
                            "type": "keyword"
                          },
                          "Area": {
                            "type": "keyword"
                          },
                          "Url": {
                            "type": "keyword"
                          },
                          "Icon": {
                            "type": "keyword"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

How can I search for any of the children's nested properties, I tried the below and some other similar types of queries:

GET my-index/_search
{
  "query": {   
    "nested": {
      "path": "menu",
      "query": {
        "bool": {
          "must": [
            {"match": 
            {
              "menu.Children.Area": "Company"
            }
            }
          ]
        }
      },
      "inner_hits": {"highlight": {"fields": {"menu.Children.Area": {}}}}
    }
  }
}

the above query does not really work as its basically returning so much data. I only want to return the inner hits, for instance if you search for 'Workflow Roles' only return

{
    "MenuStructure": 1,
    "NavigationLinkId": 261,
    "Text": "Workflow Roles",
    "Action": "Index",
    "Controller": "CompanyWorkflowRoles",
    "Area": "Company",
    "Url": "/some-url/workflow-roles"
}

not all the extra data. I have tried quite a few combination with path_filters etc. But I think I am going down the path.


Solution

  • You need multiple levels of nested fields (menu + all Children sub-fields) and design your query accordingly, then it will work as you expect.

    The query will need to be designed in a way that each nested field needs its own nested query, so you'd probably have to do something like this:

    {
      "query": {
        "bool": {
          "minimum_should_match": 1, 
          "should": [
            {
              "nested": {
                "path": "menu",
                "query": {
                  "match": {
                    "menu.Text": "Company"
                  }
                }
              }
            },
            {
              "nested": {
                "path": "menu.Children",
                "query": {
                  "match": {
                    "menu.Children.Text": "Company"
                  }
                }
              }
            },
            {
              "nested": {
                "path": "menu.Children.Children",
                "query": {
                  "match": {
                    "menu.Children.Children.Text": "Company"
                  }
                }
              }
            },
            ...
          ]
        }
      }
    }