1

MongoDB has $indexOfArray to let you find the element's array index, for example:

$indexOfArray: ["$article.date", ISODate("2019-03-29")]

Is it possible to use comparison operators with $indexOfArray together, like:

$indexOfArray: ["$article.date", {$gte: ISODate("2019-03-29")}]
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
nickmit
  • 53
  • 6

1 Answers1

3

Not it's not possible with $indexOfArray as that will only look for an equality match to an expression as the second argument.

Instead you can make a construct like this:

db.data.insertOne({
    "_id" : ObjectId("5ca01e301a97dd8b468b3f55"),
    "array" : [
            ISODate("2018-03-01T00:00:00Z"),
            ISODate("2018-03-02T00:00:00Z"),
            ISODate("2018-03-03T00:00:00Z")
    ]
})

db.data.aggregate([
  { "$addFields": {
    "matchedIndex": {
      "$let": {
        "vars": {
          "matched": {
            "$arrayElemAt": [
              { "$filter": {
                "input": {
                  "$zip": {
                    "inputs": [ "$array", { "$range": [ 0, { "$size": "$array" } ] }]
                  }
                },
                "cond": { "$gte": [ { "$arrayElemAt": ["$$this", 0] }, new Date("2018-03-02") ] }
              }},
              0
            ]
          }
        },
        "in": {
          "$arrayElemAt": [{ "$ifNull": [ "$$matched", [0,-1] ] },1]
        }
      }
    }
  }}
])

Which would return for the $gte of Date("2018-03-02"):

{
        "_id" : ObjectId("5ca01e301a97dd8b468b3f55"),
        "array" : [
                ISODate("2018-03-01T00:00:00Z"),
                ISODate("2018-03-02T00:00:00Z"),
                ISODate("2018-03-03T00:00:00Z")
        ],
        "matchedIndex" : 1
}

Or -1 where the condition was not met in order to be consistent with $indexOfArray.

The basic premise is using $zip in order to "pair" with the array index positions which get generated from $range and $size of the array. This can be fed to a $filter condition which will return ALL matching elements to the supplied condition. Here it is the first element of the "pair" ( being the original array content ) via $arrayElemAt matching the specified condition using $gte

{ "$gte": [ { "$arrayElemAt": ["$$this", 0] }, new Date("2018-03-02") ] }

The $filter will return either ALL elements after ( in the case of $gte ) or an empty array where nothing was found. Consistent with $indexOfArray you only want the first match, which is done with another wrapping $arrayElemAt on the output for the 0 position.

Since the result could be an omitted value ( which is what happens by $arrayElemAt: [[], 0] ) then you use [$ifNull][8] to test the result ans pass a two element array back with a -1 as the second element in the case where the output was not defined. In either case that "paired" array has the second element ( index 1 ) extracted again via $arrayElemAt in order to get the first matched index of the condition.

Of course since you want to refer to that whole expression, it just reads a little cleaner in the end within a $let, but that is optional as you can "inline" with the $ifNull if wanted.

So it is possible, it's just a little more involved than placing a range expression inside of $indexOfArray.

Note that any expression which actually returns a single value for equality match is just fine. But since operators like $gte return a boolean, then that would not be equal to any value in the array, and thus the sort of processing with $filter and then extraction is what you require.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • Thanks for the answer. The articles were $push into the array, I was thinking to use first unread article array index compare with array size to figure out unread count, trying to avoid scan the whole array. It seems it would be more efficient just use $filter && $size. Thanks again anyway. – nickmit Apr 01 '19 at 19:49