3

Using aggregation pipeline in spring Data, I have documents with nested arrays, and I want to project an array except the last item of it. for example for each document like :

{
    "_id" : ObjectId("59ce411c2708c97154d13150"),
    "doc1" : [ 
        {
            "nodeValue" : "AAA"
        }, 
        {
            "nodeValue" : "BBB"
        }, 
        {
            "nodeValue" : "CCC"
        }, 
        {
            "nodeValue" : "DDD"
        }
    ],
    "field2" : 20170102,
    "field3" : 4,
}

I want as result :

{
    "_id" : ObjectId("59ce411c2708c97154d13150"),
    "doc1" : [ 
        {
            "nodeValue" : "AAA"
        }, 
        {
            "nodeValue" : "BBB"
        }, 
        {
            "nodeValue" : "CCC"
        }, 
        {
            "nodeValue" : "DDD"
        }
    ],
    "doc1_without_last" : [ 
        {
            "nodeValue" : "AAA"
        }, 
        {
            "nodeValue" : "BBB"
        }, 
        {
            "nodeValue" : "CCC"
        }
    ],
    "field2" : 20170102,
    "field3" : 4,
}

I tried something like this, but I didn't find a operator that can $pop the array and remove the last item from it.

Aggregation agg = Aggregation.newAggregation(
                      Aggregation.project()
                        .andInclude("doc1","field2","field3"),
                       Aggregation.project().and(ArrayOperators.arrayOf("doc1")..).as("doc1_without_last")

                       new OutOperation("newCollection")
                ).withOptions(Aggregation.newAggregationOptions().allowDiskUse(true).build());

Thank you for your help

Dr. Mza
  • 171
  • 1
  • 11
  • There is a `$slice` there. – Alex Blex Nov 22 '17 at 13:22
  • the `$slice` just controls the number of items of an array to project. `$slice(-n)` project just the last n elements and `$slice(n)` for the first n elements. But how to project all datas of the array except the last one. – Dr. Mza Nov 22 '17 at 13:35
  • Sorry, forgot to mention that there is also `$size`. All but the last is `$slice($size - 1)`. Just in case https://docs.mongodb.com/manual/reference/operator/aggregation-array/ has all of them. – Alex Blex Nov 22 '17 at 13:40
  • there is no way to do it by spring data in pipeline `$slice` in java get only a numeric integer parameter. I had the idea to reverse the array to get simply the last one as the first like this `.and(ArrayOperators.arrayOf(doc1).reverse()).as("Reversed_doc1`) But how to exclude the `Reversed_doc1.0.nodeValue` from the projection – Dr. Mza Nov 22 '17 at 14:11
  • I am pretty sure you can build a custom $slice with serverside calculated `$size - 1` , see https://stackoverflow.com/questions/39393672/mongodb-aggregate-push-in-java-spring-data for inspiration – Alex Blex Nov 22 '17 at 15:05
  • with group() I can push a `BasicDbObject` to `group()` pipeline but I can't push it with project. I need now just to get the `$size` as a variable for each doc (is not the same size for all documents) and use it in '$slice()' – Dr. Mza Nov 23 '17 at 08:42
  • Sorry, cant' advise on java code, but I've posted a couple of queries that you need to build. If you find out how to to do it in spring, please put it as an answer. – Alex Blex Nov 23 '17 at 09:40

2 Answers2

3

The shell query can be like this:

db.collection.aggregate([
    { $project: {
        doc1: 1,
        doc1_without_last: { $slice: [ 
            "$doc1", 
            { $subtract: [ { $size: "$doc1" }, 1 ] } 
        ] }
    } }
])

Docs:

If there are any limitations in spring that don't let you build such query, you may employ $let to use query variables:

db.collection.aggregate([
    { $project: {
        doc1: 1,
        doc1_without_last: { $let: {
            vars: { size: { $subtract: [ { $size: "$doc1" }, 1 ] } },
            in: { $slice: [ "$letters", "$$size"] }
        } }    
    } }
])
Alex Blex
  • 34,704
  • 7
  • 48
  • 75
  • Thank you Alex for your help, I tried something like that : `.and(VariableOperators .define(VariableOperators.Let.ExpressionVariable.newVariable("SSIZE") .forExpression(AggregationFunctionExpressions.SUBTRACT.of(Fields.field("pathLengthInInteraction"),1))) .andApply(ArrayOperators.Slice.sliceArrayOf("doc").offset(?))).as("WITHOUT_LAST")` "pathLengthInInteraction is a the size already projected, the offset function support juste integer value, I can"t recuperate the `SSIZE` value – Dr. Mza Nov 24 '17 at 11:34
2

Finally, After Blowing my mind, the trick was simply that :

Aggregation agg = Aggregation.newAggregation(
Aggregation.project()
.andInclude("doc1","field1","field2")
.andExpression("slice(doc1, size(doc1)-1)").as("doc1_without_last"),
new OutOperation("newCollection")
                ).withOptions(Aggregation.newAggregationOptions().allowDiskUse(true).build());

the andExpression in projections serve to include some mongo operators in a String format.

Dr. Mza
  • 171
  • 1
  • 11