Mongoose - запрос геопространственного ссылочного поля [дубликат]

Вы можете использовать easy_install вместо pip, он работает для меня.

45
задан jschr 3 July 2012 в 23:45
поделиться

6 ответов

В современном MongoDB больше 3.2 вы можете использовать $lookup в качестве альтернативы .populate() в большинстве случаев. Это также имеет преимущество, заключающееся в том, что на самом деле выполняется соединение «на сервере» в отличие от того, что .populate() делает на самом деле «несколькими запросами» для «эмулирования» соединения.

Таким образом, .populate() на самом деле не является «объединением» в смысле того, как это делает реляционная база данных. Оператор $lookup , с другой стороны, фактически выполняет работу на сервере и более или менее аналогичен «LEFT JOIN» :

Item.aggregate(
  [
    { "$lookup": {
      "from": ItemTags.collection.name,
      "localField": "tags",
      "foreignField": "_id",
      "as": "tags"
    }},
    { "$unwind": "$tags" },
    { "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
    { "$group": {
      "_id": "$_id",
      "dateCreated": { "$first": "$dateCreated" },
      "title": { "$first": "$title" },
      "description": { "$first": "$description" },
      "tags": { "$push": "$tags" }
    }}
  ],
  function(err, result) {
    // "tags" is now filtered by condition and "joined"
  }
)

NB Здесь .collection.name фактически оценивает «строку», которая является фактическим именем коллекции MongoDB, присвоенной модели. Поскольку mongoose «плюрализует» имена коллекций по умолчанию, а $lookup нуждается в фактическом имени коллекции MongoDB в качестве аргумента (поскольку это операция с сервером), то это удобный трюк для использования в коде Mongoose, поскольку (g34)

Хотя мы также могли использовать $filter на массивах для удаления нежелательных элементов, это на самом деле самый эффективная форма из-за Оптимизация трубопровода агрегации для специального условия как $lookup , за которым следует как $unwind , так и $match .

Это на самом деле приводит к тому, что три этапа конвейера перекатываются в один:

   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

Это очень оптимально, поскольку фактическая операция «фильтрует коллекцию сначала присоединиться », затем возвращает результаты и« раскручивает »массив. Оба метода используются, поэтому результаты не нарушают предел BSON в 16 МБ, что является ограничением, которое клиент не имеет.

Единственная проблема заключается в том, что она кажется «противоинтуитивной» в некотором роде, особенно если вы хотите получить результаты в массиве, но это то, что здесь $group здесь, так как он восстанавливает исходную форму документа.

Также очень жаль, что мы просто не может в настоящий момент написать $lookup в том же конечном синтаксисе, который использует сервер. ИМХО, это надзор, который нужно исправить. Но на данный момент простое использование последовательности будет работать и является наиболее жизнеспособным вариантом с максимальной производительностью и масштабируемостью.

Приложение - MongoDB 3.6 и выше

Хотя приведенный здесь шаблон довольно оптимизирован из-за того, что другие этапы катятся в $lookup , у него есть одна неудача в том, что «LEFT JOIN», который обычно присущ как $lookup , а действия populate() отрицаются «оптимальным» использованием $unwind здесь, который не сохраняет пустые массивы. Вы можете добавить опцию preserveNullAndEmptyArrays, но это исключает описанную выше последовательность «оптимизированная» и по существу оставляет все три стадии неповрежденными, которые обычно объединяются в оптимизации.

MongoDB 3.6 расширяется с помощью «более выразительной» формы $lookup , позволяющей выражать «субподрядку». Это не только позволяет сохранить «LEFT JOIN», но и позволяет получить оптимальный запрос для уменьшения возвращаемых результатов и с упрощенным синтаксисом:

Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

$expr , используемый для соответствия объявленному «локальному» значению с «чужим» значением, фактически является тем, что MongoDB делает «внутренне» теперь с оригинальным синтаксисом $lookup . Выражая в этой форме, мы можем адаптировать исходное выражение $match в самой «подтрубке».

На самом деле, как истинный «конвейер агрегации», вы можете сделать почти все, что вы можете сделать с конвейером агрегации в этом выражении «под-конвейер», включая «вложение» уровней $lookup в другие связанные коллекции.

Дальнейшее использование немного выходит за рамки того, что здесь задается здесь, но в отношении даже «вложенной популяции» тогда новый шаблон использования $lookup позволяет сделать это примерно одинаковым, а "lot" более мощный в своем полном использовании.


Рабочий пример

Ниже приведен пример использования статического метода в модели. Как только этот статический метод будет реализован, вызов просто станет:

  Item.lookup(
    {
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    },
    callback
  )

Или усиление, чтобы быть немного более современным, становится:

  let results = await Item.lookup({
    path: 'tags',
    query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
  })

. Очень похоже на .populate() в структуре, но на самом деле он делает соединение на сервере. Для полноты, использование здесь возвращает возвращаемые данные обратно в экземпляры экземпляров mongoose в соответствии с родительским и дочерним случаями.

Это довольно тривиально и легко адаптируется или просто используется как для большинства распространенных случаев.

NB. Использование async здесь просто для краткости запуска прилагаемого примера. Фактическая реализация не зависит от этой зависимости.

const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt,callback) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  this.aggregate(pipeline,(err,result) => {
    if (err) callback(err);
    result = result.map(m => {
      m[opt.path] = m[opt.path].map(r => rel(r));
      return this(m);
    });
    callback(err,result);
  });
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

function log(body) {
  console.log(JSON.stringify(body, undefined, 2))
}
async.series(
  [
    // Clean data
    (callback) => async.each(mongoose.models,(model,callback) =>
      model.remove({},callback),callback),

    // Create tags and items
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
              callback),

          (tags, callback) =>
            Item.create({ "title": "Something","description": "An item",
              "tags": tags },callback)
        ],
        callback
      ),

    // Query with our static
    (callback) =>
      Item.lookup(
        {
          path: 'tags',
          query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
        },
        callback
      )
  ],
  (err,results) => {
    if (err) throw err;
    let result = results.pop();
    log(result);
    mongoose.disconnect();
  }
)

Или немного более современный для узла 8.x и выше с async/await и без дополнительных зависимостей:

const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';

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

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m => 
    this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
  ));
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

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

(async function() {
  try {

    const conn = await mongoose.connect(uri);

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

    // Create tags and items
    const tags = await ItemTag.create(
      ["movies", "funny"].map(tagName =>({ tagName }))
    );
    const item = await Item.create({ 
      "title": "Something",
      "description": "An item",
      tags 
    });

    // Query with our static
    const result = (await Item.lookup({
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);

    mongoose.disconnect();

  } catch (e) {
    console.error(e);
  } finally {
    process.exit()
  }
})()

И от MongoDB 3.6 и выше, даже без создания $unwind и $group :

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/looktest';

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

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });

itemSchema.statics.lookup = function({ path, query }) {
  let rel =
    mongoose.model(this.schema.path(path).caster.options.ref);

  // MongoDB 3.6 and up $lookup with sub-pipeline
  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": path,
      "let": { [path]: `$${path}` },
      "pipeline": [
        { "$match": {
          ...query,
          "$expr": { "$in": [ "$_id", `$$${path}` ] }
        }}
      ]
    }}
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m =>
    this({ ...m, [path]: m[path].map(r => rel(r)) })
  ));
};

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

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

(async function() {

  try {

    const conn = await mongoose.connect(uri);

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

    // Create tags and items
    const tags = await ItemTag.insertMany(
      ["movies", "funny"].map(tagName => ({ tagName }))
    );

    const item = await Item.create({
      "title": "Something",
      "description": "An item",
      tags
    });

    // Query with our static
    let result = (await Item.lookup({
      path: 'tags',
      query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);


    await mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()
17
ответ дан Neil Lunn 26 August 2018 в 01:33
поделиться

то, о чем вы просите, напрямую не поддерживается, но может быть достигнуто путем добавления другого шага фильтра после возвращения запроса.

во-первых, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } ) определенно то, что вам нужно сделать, чтобы отфильтровать теги документы. то после возвращения запроса вам нужно будет вручную отфильтровать документы, у которых нет документов tags, которые соответствуют критериям заполнения. что-то вроде:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags.length;
   })
   // do stuff with docs
});
35
ответ дан aaronheckmann 26 August 2018 в 01:33
поделиться

Обновление: Пожалуйста, взгляните на комментарии - этот ответ не соответствует вашему вопросу, но, возможно, он отвечает на другие вопросы пользователей, которые натолкнулись (я думаю, что из-за upvotes), поэтому я не буду удалять это " answer ":

Сначала: я знаю, что этот вопрос действительно устарел, но я искал именно эту проблему, и эта публикация была опубликована в Google # 1. Поэтому я реализовал версию docs.filter (принятый ответ), но, как я читал в mongoose v4.6.0 docs , мы теперь можем просто использовать:

Item.find({}).populate({
    path: 'tags',
    match: { tagName: { $in: ['funny', 'politics'] }}
}).exec((err, items) => {
  console.log(items.tags) 
  // contains only tags where tagName is 'funny' or 'politics'
})

Надеемся, что это поможет будущему пользователей поисковой системы.

13
ответ дан Fabian 26 August 2018 в 01:33
поделиться

Ответ @aaronheckmann работал для меня, но мне пришлось заменить return doc.tags.length; на return doc.tags != null;, потому что это поле содержит null, если оно не совпадает с условиями, записанными внутри заполнения. Итак, окончательный код:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
1
ответ дан HernanFila 26 August 2018 в 01:33
поделиться

После того, как я недавно столкнулся с той же проблемой, я придумал следующее решение:

Сначала найдите все ItemTags, где tagName либо «смешно», либо «политика» и возвращает массив ItemTag _ids.

Затем найдите элементы, которые содержат все ItemTag _ids в массиве тегов

ItemTag
  .find({ tagName : { $in : ['funny','politics'] } })
  .lean()
  .distinct('_id')
  .exec((err, itemTagIds) => {
     if (err) { console.error(err); }
     Item.find({ tag: { $all: itemTagIds} }, (err, items) => {
        console.log(items); // Items filtered by tagName
     });
  });
0
ответ дан OllyBarca 26 August 2018 в 01:33
поделиться

Попробуйте заменить

.populate('tags').where('tags.tagName').in(['funny', 'politics']) 

на

.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )
15
ответ дан Sergio Tulentsev 26 August 2018 в 01:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: