具有连接的近几何

时间:2019-03-28 11:46:37

标签: node.js mongodb mongoose mongodb-query aggregation-framework

我很想从我的mongodb服务器中获取结果,查询:获取最近代理商的汽车

这是我尝试过的,但是没有排序就得到结果

let cars = await Cars.find({disponible: true})
      .populate({
        path: 'agency', 
        match: {
          "location": { 
            $near: { 
              $geometry: { 
                coordinates: [ latitude , longitude ] 
              }, 
            }
          }
        },
        select: 'name'
      })
      .select('name agency');
      // send result via api
      res.status(200).json({cars})

我的模式


//Car Schema

const carSchema = new Schema({
  name: { type: String, required: true},
  agency: {type: Schema.Types.ObjectId, ref: 'agencies'},
}, { timestamps: true });

//Agency Schema

const agencySchema = new Schema({
  name: { type: String, required: true},
  location: {
    type: {
      type: String, 
      enum: ['Point'], 
      default: 'Point'
    },
    coordinates: {
        type: [Number],
        required: true
    }
  },
}, { timestamps: true });

我想和代理商一起买车,但要按最近的代理商进行排序

1 个答案:

答案 0 :(得分:1)

populate()无法正常工作的原因

由于多种原因,您将无法使用populate()来执行此操作。主要原因是populate()所做的基本上就是将您的外部引用与具有给定查询参数的另一个集合的结果结合起来。

实际上,如果使用$near查询,结果可能会很奇怪,因为您可能没有收到足够的“近”结果来与所有父引用完全结婚。

>

Querying after populate in Mongoose的现有答案中,还有关于populate()的“外部约束”限制的更多细节,当然还有关于$lookup的现代解决方案。

使用$lookup$geoNear

实际上,您需要一个$lookup和一个$geoNear,但您还必须以另一种方式“加入”以达到您的期望。因此,从Agency模型中,您可以做到:

Agency.aggregate([
    // First find "near" agencies, and project a distance field
    { "$geoNear": {
      "near": {
        "type": "Point",
        "coordinates": [ longitude , latitude ] 
      },
      "distanceField": "distance",
      "spherical" true
    }},
    // Then marry these up to Cars - which can be many
    { "$lookup": {
      "from": Car.collection.name,
      "let": { "agencyId": "$_id" },
      "pipeline": [
        { "$match": {
          "disponible": true,
          "$expr": { "$eq": [ "$$agencyId", "$agency" ] }
        }}
      ],
      "as": "cars"
    }},

    // Unwinding denormalizes that "many"
    { "$unwind": "$cars" },

    // Group is "inverting" the result
    { "$group": {
      "_id": "$cars._id",
      "car": { "$first": "$cars" },
      "agency": { 
         "$first": {
           "$arrayToObject": {
             "$filter": {
               "input": { "$objectToArray": "$$ROOT" },
               "cond": { "$ne": [ "$$this.k", "cars" ] }
             }
           }
         }
      }
    }},

    // Sort by distance, nearest is least
    { "$sort": { "agency.distance": 1 } },

    // Reformat to expected output
    { "$replaceRoot": {
      "newRoot": {
        "$mergeObjects": [ "$car", { "agency": "$agency" } ]
      }
    }}
])

如前所述,$geoNear部分必须排在最前面。最重要的是,为了将强制性索引用于此类查询,它基本上必须是聚合管道中的非常第一阶段。尽管确实存在给定的$lookup形式,但您“可以” 实际上在$near pipeline内使用$lookup表达式,从$match阶段开始,它不会返回您期望的结果,因为基本上约束已经在匹配的_id值上。在这方面,populate()确实存在相同的问题。

当然,尽管$geoNear具有"query"约束,但是您不能在该选项内使用$expr,因此这排除了在{strong>内部再次{3}} pipeline。是的,约束冲突仍然基本上是相同的问题。

因此,这意味着您$lookup来自Agency模型。这个流水线阶段还有另一件事,那就是它实际上将"distanceField"投影到结果文档中。因此,文档中的新字段(在示例中称为"distance")将指示匹配的文档距查询点有多远。这对于以后进行排序很重要。

当然,您希望此“加入”到Car,所以您想做一个$geoNear。请注意,由于MongoDB不了解猫鼬模型,因此$lookup管道阶段希望"from"是服务器上的实际集合名称。猫鼬模型通常会从您身上提取此细节(尽管通常是模型名称的复数形式,用小写字母表示),但是您始终可以如图所示从模型的.collection.name属性访问此细节。

其他参数是"let",您可以在其中引用当前_id文档的Agency。在$lookup$expr中使用它,以便比较本地键和外键是否符合实际的“加入”条件。 $match中的其他约束条件也进一步将匹配的“汽车”过滤为符合这些条件的

现在每个代理商实际上可能有很多汽车,这是模型在单独的收藏中如此完成的一个基本原因。无论是一对一还是一对多,$match结果始终会生成一个数组。现在,基本上,我们现在希望此数组对每个找到的Agency进行“去规范化”并本质上“复制” Car细节。这就是$lookup出现的地方。另一个好处是,当您$unwind匹配“汽车”的数组时,约束不匹配任何内容的任何 empty数组都会有效地删除Agency可能的结果。

当然,这与您实际想要的结果是错误的方法,因为它实际上只是“一辆汽车”与“一个代理商”。这是$unwind进入的地方,并且“每辆车”收集信息。由于这种处理方式应该是“一对一”的,因此$first运算符将用作累加器。

其中有一个$group$objectToArray的奇特表达式,但实际上所做的就是从"cars"内容中删除"agency"字段,就像"$first": "$cars"将这些数据分开。

返回到所需输出的更接近,另一主要目的是$arrayToObject结果,因此“最近”结果是第一个列出的结果,就像最初的目标是一直。在此,您可以实际使用在原始$sort阶段添加到文档中的"distance"值。

这时您已经差不多了,所需要做的就是将文档重新构建为预期的输出形状。最后的$geoNear通过从较早的$replaceRoot输出中提取"car"值并将其提升为顶级对象以返回,然后在"agency"字段中“合并”来完成此操作显示为Car本身的一部分。显然,$group进行了实际的“合并”。

就是这样。它确实有效,但是您可能已经发现,实际上在单个查询中,您实际上并没有说“在这个 AND 附近以及其他约束”这个问题。关于“最近”结果的一个有趣的事情是,它们确实对应返回的结果具有必然的“限制”。

基本上,这是下一个要讨论的主题。

更改模型

尽管以上所有方法都很好,但它仍然不是很完美,并且存在一些问题。最明显的问题应该是它非常复杂,并且“连接”通常不利于性能。

另一个明显的缺陷是,正如您可能是在$mergeObjects阶段从"query"参数中收集的那样,您并没有真正获得等同于两个条件(与 AND 汽车最近的代理机构有责任:true),因为在单独的集合中,初始的“ near”不考虑其他约束。

甚至也无法按原计划从原始顺序完成此操作,然后再次回到populate()的问题。

不幸的是,真正的问题是设计。这可能很难吞下,但目前的设计本质上与“关系” 极为相似,根本不适合MongoDB处理此类操作的方式。

核心问题是“连接”,为了使事情正常运行,我们基本上需要摆脱它。然后,您通过 嵌入文档 在MongoDB design 中做到这一点,而不是在另一个集合中保留引用:

const carSchema = new Schema({
  name: { type: String, required: true},
  agency: {
    name: { type: String, required: true},
    location: {
      type: {
        type: String, 
        enum: ['Point'], 
        default: 'Point'
      },
      coordinates: {
          type: [Number],
          required: true
      }
    }
  }
}, { timestamps: true });

简而言之“ MongoDB不是不是一个关系数据库” ,它也不会真的“加入”作为itegral的一种不支持对您要查找的联接的约束。

好吧,$geoNear不支持它, 做事的方式也没有,但是官方的话一直是并且一直是 “真正的联接“ 在MongoDB中是 嵌入式详细信息 。这只是的意思。“如果要限制要执行的查询,则它属于同一文档

经过重新设计,查询变得简单起来:

Car.find({ 
  disponible: true,
  "agency.location": { 
    $near: { 
      $geometry: { 
        coordinates: [ latitude , longitude ] 
      }, 
    }
  }
})

是的,这意味着您可能会复制许多有关“代理商”的信息,因为许多汽车上可能都存在相同的数据。但是事实是,对于这种类型的查询使用,这实际上是MongoDB期望您建模的方式。

结论

因此,这里的实际选择取决于您的需求:

  • 通过使用$lookup$geoNear组合,可能会由于“双重过滤”而返回的结果可能少于预期的结果。请注意,$lookup默认情况下将仅返回100个结果,除非您进行更改。对于“分页”结果,这可能是不可靠的组合。

  • 由于两个条件都在同一个集合中,因此您可以对接受代理商详细信息的“重复”的数据进行重组,以获取正确的“双重约束”查询。它具有更多的存储和维护功能,但是对于“分页”结果,它的性能更高且完全可靠。

当然,如果使用所示的聚合方法或数据重组都不可接受,那么这只能表明MongoDB可能不是最适合此类问题,因此最好使用RDBMS,其中您决定必须保留规范化的数据,并能够在同一操作中同时查询两个约束。当然,您可以选择一个RDBMS,该RDBMS实际上支持使用此类GeoSpatial查询以及“ joins”。