Mongodb Mongoose在数组中填充单个项

Mongodb Mongoose在数组中填充单个项,mongodb,mongoose,mongoose-populate,Mongodb,Mongoose,Mongoose Populate,我有一个模型,它有一个动态引用数组 var postSchema = new Schema({ name: String, targets: [{ kind: String, item: { type: ObjectId, refPath: 'targets.kind' } }] }); 我使用targets属性来存储对多个不同模型、用户、线程、附件等的引用 是否可以只填充我想要的引用 Post.find({}).populate({ // Does not

我有一个模型,它有一个动态引用数组

var postSchema = new Schema({
  name: String,
  targets: [{
    kind: String,
    item: { type: ObjectId, refPath: 'targets.kind' }
  }]
}); 
我使用targets属性来存储对多个不同模型、用户、线程、附件等的引用

是否可以只填充我想要的引用

Post.find({}).populate({
  // Does not work
  // match: { 'targets.kind': 'Thread' }, // I want to populate only the references that match. ex: Thread, User, Attachment
  path: 'targets.item',
  model: 'targets.kind',
  select: '_id title',
});

谢谢

这里的一个重要教训应该是
mongoose.set('debug',true)
是你的新“最好的朋友”。这将显示您正在编写的代码向MongoDB发出的实际查询,这一点非常重要,因为当您真正“看到它”时,它将清除您可能存在的任何误解

逻辑问题 让我们演示一下您尝试的操作失败的原因:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/polypop';

mongoose.set('debug', true);
mongoose.Promise = global.Promise;

const postSchema = new Schema({
  name: String,
  targets: [{
    kind: String,
    item: { type: Schema.Types.ObjectId, refPath: 'targets.kind' }
  }]
});

const fooSchema = new Schema({
 name: String
})

const barSchema = new Schema({
  number: Number
});

const Post = mongoose.model('Post', postSchema);
const Foo = mongoose.model('Foo', fooSchema);
const Bar = mongoose.model('Bar', barSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, { useNewUrlParser: true });

    // Clean all data
    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    );

    // Create some things
    let [foo, bar] = await Promise.all(
      [{ _t: 'Foo', name: 'Bill' }, { _t: 'Bar', number: 1 }]
        .map(({ _t, ...d }) => mongoose.model(_t).create(d))
    );

    log([foo, bar]);

    // Add a Post

    let post = await Post.create({
      name: 'My Post',
      targets: [{ kind: 'Foo', item: foo }, { kind: 'Bar', item: bar }]
    });

    log(post);

    let found = await Post.findOne();
    log(found);

    let result = await Post.findOne()
      .populate({
        match: { 'targets.kind': 'Foo' },    // here is the problem!
        path: 'targets.item',
      });

    log(result);

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }

})()
因此,这里的注释显示了
匹配
是逻辑的问题,所以让我们看看调试输出,看看原因:

Mongoose: posts.deleteMany({}, {})
Mongoose: foos.deleteMany({}, {})
Mongoose: bars.deleteMany({}, {})
Mongoose: foos.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a0"), name: 'Bill', __v: 0 })
Mongoose: bars.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a1"), number: 1, __v: 0 })
[
  {
    "_id": "5bdbc70996ed8e3295b384a0",
    "name": "Bill",
    "__v": 0
  },
  {
    "_id": "5bdbc70996ed8e3295b384a1",
    "number": 1,
    "__v": 0
  }
]
Mongoose: posts.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a2"), name: 'My Post', targets: [ { _id: ObjectId("5bdbc70996ed8e3295b384a4"), kind: 'Foo', item: ObjectId("5bdbc70996ed8e3295b384a0") }, { _id: ObjectId("5bdbc70996ed8e3295b384a3"), kind: 'Bar', item: ObjectId("5bdbc70996ed8e3295b384a1") } ], __v: 0 })
{
  "_id": "5bdbc70996ed8e3295b384a2",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbc70996ed8e3295b384a4",
      "kind": "Foo",
      "item": {
        "_id": "5bdbc70996ed8e3295b384a0",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbc70996ed8e3295b384a3",
      "kind": "Bar",
      "item": {
        "_id": "5bdbc70996ed8e3295b384a1",
        "number": 1,
        "__v": 0
      }
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
{
  "_id": "5bdbc70996ed8e3295b384a2",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbc70996ed8e3295b384a4",
      "kind": "Foo",
      "item": "5bdbc70996ed8e3295b384a0"
    },
    {
      "_id": "5bdbc70996ed8e3295b384a3",
      "kind": "Bar",
      "item": "5bdbc70996ed8e3295b384a1"
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: bars.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a1") ] } }, { projection: {} })
Mongoose: foos.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a0") ] } }, { projection: {} })
{
  "_id": "5bdbc70996ed8e3295b384a2",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbc70996ed8e3295b384a4",
      "kind": "Foo",
      "item": null
    },
    {
      "_id": "5bdbc70996ed8e3295b384a3",
      "kind": "Bar",
      "item": null
    }
  ],
  "__v": 0
}
这是一个完整的输出,显示其他一切都在实际工作,事实上,如果没有
匹配
,您将获得项目的填充数据。但请仔细查看向
foo
bar
集合发出的两个查询:

Mongoose: bars.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a1") ] } }, { projection: {} })
Mongoose: foos.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a0") ] } }, { projection: {} })
因此,您在
match
下包含的
“targets.kind”
实际上是在
foo
bar
集合中搜索的,而不是
posts
集合中的。与输出的其余部分一起,这应该可以让您了解
populate()
实际上是如何工作的,因为没有任何内容明确要求只返回
类型的“数组项”:“Foo”

这种“过滤数组”的过程实际上甚至不是“真正的”自然的MongoDB查询,除了“第一个和单数匹配”之外,您通常会使用
.aggregate()
和操作符。您可以通过positional
$
操作符获得“singular”,但是如果您想要在有多个foo的地方使用“all foo”,那么它需要使用

所以这里真正的核心问题是
populate()
实际上是“过滤数组”的错误位置和错误操作。相反,您确实希望在执行任何其他操作来“填充”项目之前,只“智能地”返回所需的数组项

结构问题 注意到上面的列表是问题中暗示的寓言,为了“加入”并获得整体结果,提到了“多个模型”。虽然这在“RDBMS领域”中似乎是合乎逻辑的,但在MongoDB和“文档数据库”的一般“同类”中这样做肯定是不实际、不高效的

这里需要记住的关键是,“集合”中的“文档”不必都具有与RDBMS相同的“表结构”。结构可能会有所不同,虽然可能建议不要“大幅度变化”,但在单个集合中存储“多态对象”肯定是非常有效的。毕竟,您实际上希望将所有这些内容引用回同一个父对象,那么为什么它们需要位于不同的集合中呢?简单地说,他们根本不需要:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/polypop';

mongoose.set('debug', true);
mongoose.Promise = global.Promise;

const postSchema = new Schema({
  name: String,
  targets: [{
    kind: String,
    item: { type: Schema.Types.ObjectId, ref: 'Target' }
  }]
});

const targetSchema = new Schema({});

const fooSchema = new Schema({
  name: String
});

const barSchema = new Schema({
  number: Number
});

const bazSchema = new Schema({
  title: String
});

const log = data => console.log(JSON.stringify(data, undefined, 2));

const Post = mongoose.model('Post', postSchema);
const Target = mongoose.model('Target', targetSchema);
const Foo = Target.discriminator('Foo', fooSchema);
const Bar = Target.discriminator('Bar', barSchema);
const Baz = Target.discriminator('Baz', bazSchema);

(async function() {

  try {

    const conn = await mongoose.connect(uri,{ useNewUrlParser: true });

    // Clean data - bit hacky but just a demo
    await Promise.all(
      Object.entries(conn.models).map(([k, m]) => m.deleteMany() )
    );

    // Insert some things
    let [foo1, bar, baz, foo2] = await Promise.all(
      [
        { _t: 'Foo', name: 'Bill' },
        { _t: 'Bar', number: 1 },
        { _t: 'Baz', title: 'Title' },
        { _t: 'Foo', name: 'Ted' }
      ].map(({ _t, ...d }) => mongoose.model(_t).create(d))
    );

    log([foo1, bar, baz, foo2]);

    // Add a Post
    let post = await Post.create({
      name: 'My Post',
      targets: [
        { kind: 'Foo', item: foo1 },
        { kind: 'Bar', item: bar },
        { kind: 'Baz', item: baz },
        { kind: 'Foo', item: foo2 }
      ]
    });

    log(post);

    let found = await Post.findOne();
    log(found);

    let result1 = await Post.findOne()
      .populate({
        path: 'targets.item',
        match: { __t: 'Foo' }
      });
    log(result1);

    let result2 = await Post.aggregate([
      // Only get documents with a matching entry
      { "$match": {
        "targets.kind": "Foo"
      }},
      // Optionally filter the array
      { "$addFields": {
        "targets": {
          "$filter": {
            "input": "$targets",
            "cond": {
              "$eq": [ "$$this.kind", "Foo" ]
             }
          }
        }
      }},
      // Lookup from single source
      { "$lookup": {
        "from": Target.collection.name,
        "localField": "targets.item",
        "foreignField": "_id",
        "as": "matches"
      }},
      // Marry up arrays
      { "$project": {
        "name": 1,
        "targets": {
          "$map": {
            "input": "$targets",
            "in": {
              "kind": "$$this.kind",
              "item": {
                "$arrayElemAt": [
                  "$matches",
                  { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
                ]
              }
            }
          }
        }
      }}
    ]);
    log(result2);

    let result3 = await Post.aggregate([
      // Only get documents with a matching entry
      { "$match": {
        "targets.kind": "Foo"
      }},
      // Optionally filter the array
      { "$addFields": {
        "targets": {
          "$filter": {
            "input": "$targets",
            "cond": {
              "$eq": [ "$$this.kind", "Foo" ]
             }
          }
        }
      }},
      // Lookup from single source with overkill of type check
      { "$lookup": {
        "from": Target.collection.name,
        "let": { "targets": "$targets" },
        "pipeline": [
          { "$match": {
            "$expr": {
              "$in": [ "$_id", "$$targets.item" ]
            },
            "__t": "Foo"
          }}
        ],
        "as": "matches"
      }},
      // Marry up arrays
      { "$project": {
        "name": 1,
        "targets": {
          "$map": {
            "input": "$targets",
            "in": {
              "kind": "$$this.kind",
              "item": {
                "$arrayElemAt": [
                  "$matches",
                  { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
                ]
              }
            }
          }
        }
      }}
    ]);
    console.log(result3);    

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }

})()
这有点长,还有一些概念需要解决,但基本原则是,我们不需要对不同的类型使用“多个集合”,而是只使用一个。用于此目的的“猫鼬”方法用于模型设置,该模型设置与本部分代码相关:

const Post = mongoose.model('Post', postSchema);
const Target = mongoose.model('Target', targetSchema);
const Foo = Target.discriminator('Foo', fooSchema);
const Bar = Target.discriminator('Bar', barSchema);
const Baz = Target.discriminator('Baz', bazSchema);
这实际上只是从“单一”集合的“基本模型”调用
.discriminator()
,而不是调用
mongoose.model()
。这一点的真正好处在于,就代码的其余部分而言,
Baz
Bar
等都被视为透明的“模型”,但它们实际上在做一些真正酷的事情

因此,所有这些“相关的东西”(即使你不这么认为,它们确实是)实际上都保存在同一个集合中,但是使用单个模型的操作考虑了一个“自动”
kind
键。默认情况下,这是
\uu t
,但实际上您可以在选项中指定所需的内容

事实上,这些数据实际上都在同一个集合中,这一点非常重要,因为您基本上可以为不同类型的数据查询同一集合。简言之:

Foo.find({})
我会打电话给你

targets.find({ __t: 'Foo' })
这是自动完成的。但更重要的是

Target.find({ __t: { "$in": [ 'Foo', 'Baz' ] } })
将返回来自“单个集合”和“单个请求”的所有预期结果

因此,请查看此结构下修订的
populate()

let result1 = await Post.findOne()
  .populate({
    path: 'targets.item',
    match: { __t: 'Foo' }
  });
log(result1);
而是在日志中显示:

Mongoose: posts.findOne({}, { projection: {} })
Mongoose: targets.find({ __t: 'Foo', _id: { '$in': [ ObjectId("5bdbe2895b1b843fba050569"), ObjectId("5bdbe2895b1b843fba05056a"), ObjectId("5bdbe2895b1b843fba05056b"), ObjectId("5bdbe2895b1b843fba05056c") ] } }, { projection: {} })
请注意,即使相关的
ObjectId
值的所有“四个”都随请求一起发送,
\uu t:'Foo'
的附加约束也会绑定实际返回和合并的文档。当只填充
'Foo'
条目时,结果变得不言而喻。但也要注意“陷阱”:

填充后过滤 这实际上是一个更长、更多的主题,但如上面的输出所示,这里的基本内容是
populate()
对于实际“过滤”数组中的结果以仅获得所需的匹配项,仍然完全不做任何事情

另一件事是,从“性能”的角度来看,
populate()
真的不是一个好主意,因为真正发生的是“另一个查询”(在第二种形式中,我们只优化了一个或多个查询)根据您的结构,数据实际上被发送到数据库,并且结果在客户机上一起重建

总的来说,您最终返回的数据比实际需要的要多得多,并且最多只能依靠手动客户端筛选来丢弃这些不需要的结果。因此,“理想”的立场是
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba050569",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba05056c",
        "name": "Ted",
        "__v": 0
      }
    }
  ],
  "__v": 0
}
let result2 = await Post.aggregate([
  // Only get documents with a matching entry
  { "$match": {
    "targets.kind": "Foo"
  }},
  // Optionally filter the array
  { "$addFields": {
    "targets": {
      "$filter": {
        "input": "$targets",
        "cond": {
          "$eq": [ "$$this.kind", "Foo" ]
         }
      }
    }
  }},
  // Lookup from single source
  { "$lookup": {
    "from": Target.collection.name,
    "localField": "targets.item",
    "foreignField": "_id",
    "as": "matches"
  }},
  // Marry up arrays
  { "$project": {
    "name": 1,
    "targets": {
      "$map": {
        "input": "$targets",
        "in": {
          "kind": "$$this.kind",
          "item": {
            "$arrayElemAt": [
              "$matches",
              { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
            ]
          }
        }
      }
    }
  }}
]);
log(result2);
  {
    "_id": "5bdbe6aa2c4a2240c16802e2",
    "name": "My Post",
    "targets": [
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe6aa2c4a2240c16802de",
          "__t": "Foo",
          "name": "Bill",
          "__v": 0
        }
      },
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe6aa2c4a2240c16802e1",
          "__t": "Foo",
          "name": "Ted",
          "__v": 0
        }
      }
    ]
  }
let result3 = await Post.aggregate([
  // Only get documnents with a matching entry
  { "$match": {
    "targets.kind": "Foo"
  }},
  // Optionally filter the array
  { "$addFields": {
    "targets": {
      "$filter": {
        "input": "$targets",
        "cond": {
          "$eq": [ "$$this.kind", "Foo" ]
         }
      }
    }
  }},
  // Lookup from single source with overkill of type check
  { "$lookup": {
    "from": Target.collection.name,
    "let": { "targets": "$targets" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$in": [ "$_id", "$$targets.item" ]
        },
        "__t": "Foo"
      }}
    ],
    "as": "matches"
  }},
  // Marry up arrays
  { "$project": {
    "name": 1,
    "targets": {
      "$map": {
        "input": "$targets",
        "in": {
          "kind": "$$this.kind",
          "item": {
            "$arrayElemAt": [
              "$matches",
              { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
            ]
          }
        }
      }
    }
  }}
]);
log(result3);
Mongoose: posts.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba050569"), __t: 'Foo', name: 'Bill', __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056a"), __t: 'Bar', number: 1, __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056b"), __t: 'Baz', title: 'Title', __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056c"), __t: 'Foo', name: 'Ted', __v: 0 })
[
  {
    "_id": "5bdbe2895b1b843fba050569",
    "__t": "Foo",
    "name": "Bill",
    "__v": 0
  },
  {
    "_id": "5bdbe2895b1b843fba05056a",
    "__t": "Bar",
    "number": 1,
    "__v": 0
  },
  {
    "_id": "5bdbe2895b1b843fba05056b",
    "__t": "Baz",
    "title": "Title",
    "__v": 0
  },
  {
    "_id": "5bdbe2895b1b843fba05056c",
    "__t": "Foo",
    "name": "Ted",
    "__v": 0
  }
]
Mongoose: posts.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056d"), name: 'My Post', targets: [ { _id: ObjectId("5bdbe2895b1b843fba050571"), kind: 'Foo', item: ObjectId("5bdbe2895b1b843fba050569") }, { _id: ObjectId("5bdbe2895b1b843fba050570"), kind: 'Bar', item: ObjectId("5bdbe2895b1b843fba05056a") }, { _id: ObjectId("5bdbe2895b1b843fba05056f"), kind: 'Baz', item: ObjectId("5bdbe2895b1b843fba05056b") }, { _id: ObjectId("5bdbe2895b1b843fba05056e"), kind: 'Foo', item: ObjectId("5bdbe2895b1b843fba05056c") } ], __v: 0 })
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": {
        "_id": "5bdbe2895b1b843fba050569",
        "__t": "Foo",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": {
        "_id": "5bdbe2895b1b843fba05056a",
        "__t": "Bar",
        "number": 1,
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": {
        "_id": "5bdbe2895b1b843fba05056b",
        "__t": "Baz",
        "title": "Title",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": {
        "_id": "5bdbe2895b1b843fba05056c",
        "__t": "Foo",
        "name": "Ted",
        "__v": 0
      }
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": "5bdbe2895b1b843fba050569"
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": "5bdbe2895b1b843fba05056a"
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": "5bdbe2895b1b843fba05056b"
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": "5bdbe2895b1b843fba05056c"
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: targets.find({ __t: 'Foo', _id: { '$in': [ ObjectId("5bdbe2895b1b843fba050569"), ObjectId("5bdbe2895b1b843fba05056a"), ObjectId("5bdbe2895b1b843fba05056b"), ObjectId("5bdbe2895b1b843fba05056c") ] } }, { projection: {} })
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba050569",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba05056c",
        "name": "Ted",
        "__v": 0
      }
    }
  ],
  "__v": 0
}
Mongoose: posts.aggregate([ { '$match': { 'targets.kind': 'Foo' } }, { '$addFields': { targets: { '$filter': { input: '$targets', cond: { '$eq': [ '$$this.kind', 'Foo' ] } } } } }, { '$lookup': { from: 'targets', localField: 'targets.item', foreignField: '_id', as: 'matches' } }, { '$project': { name: 1, targets: { '$map': { input: '$targets', in: { kind: '$$this.kind', item: { '$arrayElemAt': [ '$matches', { '$indexOfArray': [ '$matches._id', '$$this.item' ] } ] } } } } } } ], {})
[
  {
    "_id": "5bdbe2895b1b843fba05056d",
    "name": "My Post",
    "targets": [
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe2895b1b843fba050569",
          "__t": "Foo",
          "name": "Bill",
          "__v": 0
        }
      },
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe2895b1b843fba05056c",
          "__t": "Foo",
          "name": "Ted",
          "__v": 0
        }
      }
    ]
  }
]
Mongoose: posts.aggregate([ { '$match': { 'targets.kind': 'Foo' } }, { '$addFields': { targets: { '$filter': { input: '$targets', cond: { '$eq': [ '$$this.kind', 'Foo' ] } } } } }, { '$lookup': { from: 'targets', let: { targets: '$targets' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$targets.item' ] }, __t: 'Foo' } } ], as: 'matches' } }, { '$project': { name: 1, targets: { '$map': { input: '$targets', in: { kind: '$$this.kind', item: { '$arrayElemAt': [ '$matches', { '$indexOfArray': [ '$matches._id', '$$this.item' ] } ] } } } } } } ], {})