Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Finding duplicates in Elasticsearch

I'm trying to find entries in my data which are equal in more than one aspect. I currently do this using a complex query which nests aggregations:

{
  "size": 0, 
  "aggs": { 
    "duplicateFIELD1": { 
      "terms": { 
        "field": "FIELD1", 
        "min_doc_count": 2 },
      "aggs": { 
        "duplicateFIELD2": { 
          "terms": { 
            "field": "FIELD2", 
            "min_doc_count": 2 },
          "aggs": {
            "duplicateFIELD3": {
              "terms": {
                "field": "FIELD3",
                "min_doc_count": 2 },
              "aggs": {
                "duplicateFIELD4": {
                  "terms": {
                    "field": "FIELD4",
                    "min_doc_count": 2 },
                  "aggs": {
                    "duplicate_documents": { 
                      "top_hits": {} } } } } } } } } } } }

This works to an extent as the result I get when no duplicates are found look something like this:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 27524067,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "duplicateFIELD1" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 27524027,
      "buckets" : [
        {
          "key" : <valueFromField1>,
          "doc_count" : 4,
          "duplicateFIELD2" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : <valueFromField2>,
                "doc_count" : 2,
                "duplicateFIELD3" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : <valueFromField3>,
                      "doc_count" : 2,
                      "duplicateFIELD4" : {
                        "doc_count_error_upper_bound" : 0,
                        "sum_other_doc_count" : 0,
                        "buckets" : [ ]
                      }
                    }
                  ]
                }
              },
              {
                "key" : <valueFromField2>,
                "doc_count" : 2,
                "duplicateFIELD3" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : <valueFromField3>,
                      "doc_count" : 2,
                      "duplicateFIELD4" : {
                        "doc_count_error_upper_bound" : 0,
                        "sum_other_doc_count" : 0,
                        "buckets" : [ ]
                      }
                    }
                  ]
                }
              }
            ]
          }
        },
        {
          "key" : <valueFromField1>,
          "doc_count" : 4,
          "duplicateFIELD2" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : <valueFromField2>,
                "doc_count" : 2,
                "duplicateFIELD3" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : <valueFromField3>,
                      "doc_count" : 2,
                      "duplicateFIELD4" : {
                        "doc_count_error_upper_bound" : 0,
                        "sum_other_doc_count" : 0,
                        "buckets" : [ ]
                      }
                    }
                  ]
                }
              },
              {
                "key" : <valueFromField2>,
                "doc_count" : 2,
                "duplicateFIELD3" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : <valueFromField3>,
                      "doc_count" : 2,
                      "duplicateFIELD4" : {
                        "doc_count_error_upper_bound" : 0,
                        "sum_other_doc_count" : 0,
                        "buckets" : [ ]
                      }
                    }
                  ]
                }
              }
            ]
          }
        },
        ...

I'm skipping some of the output which looks rather similar.

I can now scan through this complex deeply nested data structure and find that no documents are stored in all of these nested buckets. But this seems rather cumbersome. I guess there might be a better (more straight-forward) way of doing this.

Also, if I want to check more than four fields, this nested structure will grow and grow and grow. So it does not scale very well and I want to avoid this.

Can I improve my solution so that I do get a simple list of all documents which are duplicates? (Maybe the ones which are duplicates of each other grouped together somehow.) or is there a completely different approach (such as without aggregation) which does not have the drawbacks I described here?

EDIT: I found an approach using the script feature of ES here, but in my version of ES this returns just an error message. Maybe someone can point out to me how to do it in ES 5.0? My trials up to now did not work.

EDIT: I found a way to use a script for my approach which uses the modern way (language "painless"):

{
  "size": 0,
  "aggs": {
    "duplicateFOO": {
      "terms": {
        "script": {
          "lang": "painless",
          "inline": "doc['FIELD1'].value + doc['FIELD2'].value + doc['FIELD3'].value + doc['FIELD4'].value"
        },                   
        "min_doc_count": 2
      }                        
    }                         
  }
}

This seems to work for very small amounts of data and results in an error for realistic amounts of data (circuit_breaking_exception: [request] Data too large, data for [<reused_arrays>] would be larger than limit of [6348236390/5.9gb]). Any idea on how I can fix this? Probably adjust some configuration of the ES to make it use larger internal buffers or similar?


There does not seem to be a proper solution for my situation which avoids the nesting in a general way.

Fortunately three of my four fields have a very limited value range; the first can only be 1 or 2, the second can be 1, 2, or 3 and the third can be 1, 2, 3, or 4. Since these are just 24 combinations I currently go with filtering one 24th out of the complete data set before applying the aggregation, then of just one (the remaining fourth field). I then have to apply all actions 24 times (once with each combination of the three limited fields mentioned above), but this is still more feasible than handling the complete data set at once.

The query (i. e. one of the 24 queries) I send now look something like this:

{
  "size": 0,
  "query": {
    "bool": {
      "must": [
        { "match": { "FIELD1": 2 } },
        { "match": { "FIELD2": 3 } },
        { "match": { "FIELD3": 4 } } ] } },
  "aggs": {
    "duplicateFIELD4": {
      "terms": {
        "field": "FIELD4",
        "min_doc_count": 2 } } } }

The results for this of course are not nested anymore. But this cannot be done if more than one field holds arbitrary values of a larger range.

I also found out that, if nesting must be done, the fields with the most limited value range (e. g. just two values like "1 or 2") should be innermost, and the one with the largest value range should be outermost. This improves performance greatly (but still not enough in my case). Doing it wrong can let you end up with an unusable query (no response within hours, and finally an out of memory on the server side).

I now think that aggregating properly is the key to solve a problem like mine. The approach using a script to have a flat bucket list (as described in my question) is bound to overload the server as it cannot distribute the task in any way. In the case that no double is found at all, it has to hold a bucket for each document in memory (with just one document in it). Even if just a few doubles can be found, this cannot be done for larger data sets. If nothing else is possible, one will need to split the data set into groups artificially. E. g. one can create 16 sub-data sets by building a hash out of the relevant fields and use the last 4 bits to put the document in on of the 16 groups. Each group can then be handled separately; doubles are bound to fall into one group using this technique.

But independently from these general thoughts, the ES API should provide any means to paginate through the result of aggregations. It's a pity that there is no such option (yet).

like image 277
Alfe Avatar asked Dec 06 '16 15:12

Alfe


1 Answers

Your last approach seems to be the best one. And you can update your elasticsearch settings as following:

indices.breaker.request.limit: "75%"
indices.breaker.total.limit: "85%"

I have chosen 75% because the default is 60% and it is 5.9gb in your elasticsearch and your query is becoming ~6.3gb which is around 71.1% based on your log.

circuit_breaking_exception: [request] Data too large, data for [<reused_arrays>] would be larger than limit of [6348236390/5.9gb]

And finally indices.breaker.total.limit must be greater than indices.breaker.fielddata.limit according to elasticsearch document.

like image 136
Dulguun Avatar answered Nov 06 '22 07:11

Dulguun