Search code examples
node.jselasticsearchelasticsearch-5elasticsearch-painless

elasticsearch node.js API remove an object from an array on a document using painless script results in array Index Out of Bounds


I want to remove items (an object) from an array on a document in elasticsearch, however whenever I try and run my update script using painless, I receive an Array Index Out of Bounds exception.

I'm using the javascript elasticsearch npm package to search elasticsearch for the relevant documents which then returns me data like:

"_index": "centres",
"_type": "doc",
"_id": "51bc77d1-b514-4f4e-85fa-412def6829f5",
"_score": 1,
"_source": {
    "id": "cbaa7daa-f1a2-4ac3-8d7c-fc981245d21c",
    "name": "Five House",
    "openDays": [
        {
            "title": "new open Day",
            "endDate": "2022-03-22T00:00:00.000Z",
            "id": "82be934b-eeb1-419c-96ed-a58808b30df7"
        },
        {
            "title": "last open Day",
            "endDate": "2020-12-24T00:00:00.000Z",
            "id": "8cc339b9-d2f8-4252-b68a-ed0a49cbfabd"
        }
    ]
}

I then want to go through and remove certain items from the openDays array. I've created an array of the items I want to remove, so for the above example:

[
  {
    id: '51bc77d1-b514-4f4e-85fa-412def6829f5',
    indexes: [
        {
            "title": "last open Day",
            "endDate": "2020-12-24T00:00:00.000Z",
            "id": "8cc339b9-d2f8-4252-b68a-ed0a49cbfabd"
        }
    ]
  }
]

I'm then trying to run an update via the elasticsearch node client like this:

for (const centre of updates) {
    if (centre.indexes.length) {
        await Promise.all(centre.indexes.map(async (theIndex) => {
            const updated = await client.update({
                index: 'centres',
                type: 'doc',
                id: centre.id,
                body: {
                    script: {
                        lang: 'painless',
                        source: "ctx._source.openDays.remove(ctx._source.openDays.indexOf('openDayID'))",
                        params: {
                            "openDayID": theIndex.id
                        }
                    }
                }
            }).catch((err) => {throw err;});
        }))
            .catch((err) => {throw err;});

        await client.indices.refresh({ index: 'centres' }).catch((err) => { throw err;});
    }
}

When I run this though, it returns a 400 with an "array_index_out_of_bounds_exception" error:

  -> POST http://localhost:9200/centres/doc/51bc77d1-b514-4f4e-85fa-412def6829f5/_update
  {
    "script": {
      "lang": "painless",
      "source": "ctx._source.openDays.remove(ctx._source.openDays.indexOf(\u0027openDayID\u0027))",
      "params": {
        "openDayID": "8cc339b9-d2f8-4252-b68a-ed0a49cbfabd"
      }
    }
  }
  <- 400
  {
    "error": {
      "root_cause": [
        {
          "type": "remote_transport_exception",
          "reason": "[oSsa7mn][172.17.0.2:9300][indices:data/write/update[s]]"
        }
      ],
      "type": "illegal_argument_exception",
      "reason": "failed to execute script",
      "caused_by": {
        "type": "script_exception",
        "reason": "runtime error",
        "script_stack": [],
        "script": "ctx._source.openDays.remove(ctx._source.openDays.indexOf(\u0027openDayID\u0027))",
        "lang": "painless",
        "caused_by": {
          "type": "array_index_out_of_bounds_exception",
          "reason": null
        }
      }
    },
    "status": 400
  }

I'm not quite sure where I'm going wrong with this. Am I using the indexOf painless script correctly? Does indexOf allow for the searching of properties on objects in arrays?


Solution

  • I stumbled across this question and answer: Elasticsearch: Get object index with Painless script

    The body of the update script needs changing like so:

    Promise.all(...
    const inline = `
        def openDayID = '${theIndex.id}'; 
        def openDays = ctx._source.openDays;
        def openDayIndex = -1;
        for (int i = 0; i < openDays.length; i++)
        { 
            if (openDays[i].id == openDayID) 
            { 
                openDayIndex = i;  
            } 
        }
        if (openDayIndex != -1) {
            ctx._source.openDays.remove(openDayIndex);
        }
    `;
    const updated = await client.update({
    index: 'centres',
    type: 'doc',
    id: centre.id,
    body: {
        script: {
            lang: 'painless',
            inline: inline,
        },
    }
    }).catch((err) => {throw err;});
    
    await client.indices.refresh({ index: 'centres' }).catch((err) => { throw err;});
    })).catch(... //end of Promise.all
    

    I am not au fait with painless scripting, so there are most likely better ways of writing this e.g. breaking once the index of the ID is found.

    I have also had to move the refresh statement into the Promise.all since if you're trying to remove more than one item from the array of objects, you'll be changing the document and changing the index. There is probably a better way of dealing with this too.