【问题标题】:MongoDB custom sort order for a query with pagination带有分页的查询的 MongoDB 自定义排序顺序
【发布时间】:2021-09-19 05:15:01
【问题描述】:

我在具有此架构的 MongoDB 集合中有一些文档:

{
    "_id": {
        "$oid": "60c1e8e318afd80016ce58b1"
    },
    "searchPriority": 1,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "Delhi"
    }
},
{
    "_id": {
        "$oid": "60c1f2f418afd80016ce58b5"
    },
    "searchPriority": 2,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "Delhi"
    }
},
{
    "_id": {
        "$oid": "60cb429eadd33c00139d2be7"
    },
    "searchPriority": 1,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "Gurugram"
    }
},
{
    "_id": {
        "$oid": "60c21be618afd80016ce5905"
    },
    "searchPriority": 2,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "New Delhi"
    }
},
{
    "_id": {
        "$oid": "60e306d29e452d00134b978f"
    },
    "searchPriority": 3,
    "isLive": false,
    "vehicleCondition": "USED",
    "vehicleDetails": {
        "city": "New Delhi"
    }
}

vehicleCondition 可以是NEWUSEDisLive 可以是truefalsesearchPriority 将是一个介于 1 到 3 之间的整数。(较小的数字意味着它应该在搜索结果)

这里,除了_id,其他字段都不是唯一的。我在isLivevehicleDetails.citysearchPriority 上创建了一个复合索引。

在我的应用程序中,我将执行一些这种形式的查询:

  • 查找isLivetruevehicleDetails.city 为的所有汽车 DelhiNew DelhiGurugramvehicleConditionUSED(或NEW)。

为此,我可以像这样进行查找查询:

db.collection.find({"isLive": true, "vehicleDetails.city": { $in: [ "Gurugram", "Delhi", "New Delhi" ] }, "vehicleCondition": "USED" }, {})

我希望此查询的结果按此顺序排序:

  • 在查找查询中属于 $in 数组中第一个城市的所有汽车,优先级最低
  • 在查找查询中属于 $in 数组中第一个城市的所有汽车,优先级第二低
  • 所有属于$in arrarray 内第一个城市的汽车 查询,具有第三低优先级
  • 在查找查询中属于 $in 数组中第二个城市的所有汽车,优先级最低
  • 在查找查询中属于 $in 数组中第二个城市的所有汽车,优先级第二低
  • 在查找查询中属于$in 数组内的第二个城市的所有汽车,具有第三低优先级 查找查询中属于 $in 数组中第三个城市的所有汽车,优先级最低
  • 在查找查询中属于 $in 数组中第三个城市的所有汽车,优先级第二低
  • 在查找查询中属于 $in 数组中的第三个城市的所有汽车,具有第三低优先级

我该怎么做?由于此查询返回的文档数量可能非常大,因此我将使用分页来限制返回的文档数量。这个额外的要求会对这个问题的可能解决方案有任何影响吗?

【问题讨论】:

  • 不幸的是,您不能轻松地执行自定义排序顺序。您可以使用 MongoDB 聚合操作您的文档以执行自定义顺序,但 MongoDB 在这种情况下不能使用索引。最好的解决方案是:存储预煮订单并使用它进行排序。
  • @Valijon 你能解释一下“存储预煮订单并将其用于分类”是什么意思吗?
  • 我的意思是,inside $in arrray 的顺序无关紧要,您可以定义顺序静态方式:Gurugram 始终是最低的 (10X),然后是 Delhi (20X),等等...检查一下:mongoplayground.net/p/8zTHzlLYiqD
  • @Valijon 感谢您的回答,但不幸的是,静态优先级对我不起作用,因为 $in 数组会因上下文而异。例如:在某些情况下也可以是 ["New Delhi", "Gurugram", "Delhi"],在这种情况下,我希望 "New Delhi" 的结果排在首位。我在 SQL 数据库方面的经验为 0,您认为在 SQL 中可以实现这样的事情吗?
  • 不。您提供的订单仅用于搜索

标签: mongodb mongoose aggregation-framework


【解决方案1】:

我希望这对你有用

 let x = ["Gurugram","Delhi", "New Delhi"];

db.collection.aggregate([
        {
            $match: {
                "isLive": true,
                "vehicleDetails.city": {
                    $in: x
                },
                "vehicleCondition": "USED"
            }
        },
        {
            $project: {
                "_id": 1,
                "searchPriority": 1,
                "isLive": 1,
                "vehicleCondition": 1,
                "vehicleDetails": 1,
                index: { $indexOfArray: [x, "$vehicleDetails.city"] },
            }
        },
        { $sort: { index: 1, searchPriority: 1 } },
        {
            $project: {
                "index": 0,
            }
        }
    ]).toArray();

【讨论】:

  • 这将执行阻塞排序并将超过 100MB 的聚合查询限制。如果我启用允许使用磁盘的选项,查询将非常非常慢。另外,我不确定这将如何与分页一起使用。我应该把limitskip 子句放在哪里?每次我用不同的skip 分页值调用它时,它会执行排序吗?
  • {$project: {"index": 0, } }, {$limit :100}, { $skip : 5 } ]).toArray();
  • 每次我使用这个查询时它不会执行match, project, 'sort' 吗?
  • 会的。当执行查询时,管道查询将被执行。如果你不希望每次都访问mongodb,那么,设置一个查找查询来获取所有文档,然后使用JS中的过滤功能过滤掉需要的。但这比查询需要更多时间
【解决方案2】:

所以我已经阅读了另一个答案(它提供了技术解决方案),但是根据您的 cmets 并要求它不适合。

所以先在这里使用aggregate,虽然技术上解决了一些问题。

正如你提到的查询可以有大量的文档匹配,聚合管道与find 方法不同确实将它们全部加载到内存中,这会创造性地导致性能问题,我还看到你提到了一些关于不有一个索引。这将导致对每个 API 调用进行“收集”扫描。

我建议你做的是:

  1. 首先,您绝对必须在isLive, vehicleCondition, "vehicleDetails.city" 上建立一个复合索引,以防万一您没有。这只是大规模使用的必需品。

  2. 既然我们已经解决了这个问题,我建议您将调用分成几部分,我将粘贴一些 puesdo 代码,这些代码可能看起来有点到处都是,但我相信这是最好的方法您可以使用 Mongo 来实现,因为这些查询中的每一个都假定通过使用先前构建的索引是有效的。

我将简要解释一下方法,我们希望能够独立于其他城市查询每个城市,这样我们就可以使用“自定义排序”功能而无需将所有匹配项加载到内存中。

为此,我们需要知道每个城市需要“跳过”和“限制”多少,例如城市#2(德里)限制将是(限制 - 城市#1(古鲁格拉姆)匹配)。

所以这是伪代码,我故意把它简单化了,这样可以理解。不过,我会在最后添加一些想法以进行一些基本的改进。

let limit = 10; // determined by req?
const skip = 0; // determined by req?
const cities = ['Gurugram', 'Delhi', 'New Delhi'];

// we need this to resolve the proper skip / limit. the last city is not relevant.
const countPromises = [];
for (let i = 0; i < cities.length - 1; i++) {
    countPromises.push(db.collection.countDocuments({
        'isLive': true,
        'vehicleDetails.city': cities[i],
        'vehicleCondition': 'USED',
    }));
}
await Promise.all(countPromises);

// first city initial skip
const citySkips = [skip];

for (let i = 0; i < countPromises.length - 1; i++) {
    // if we have x results in the first city then we need to skip-x skipping for the next city.
    citySkips.push(Math.max(skip - countPromises[0], 0));
}

let finalResults = [];
for (let i = 0; i < cities.length; i++) {
    // assuming we skip over ALL city i results.
    if (citySkips[i] >= countPromises[i]) {
        continue;
    }
    const cityLimit = limit - finalResults.length;
    if (cityLimit <= 0) {
        break;
    }
    const cityResults = await db.collection.find({
        'isLive': true,
        'vehicleDetails.city': cities[i],
        'vehicleCondition': 'USED',
    }).sort({ sortPriority: 1 }).skip(citySkips[i]).limit(cityLimit);
    finalResults = finalResults.concat(cityResults);
}

好的,你可以做出改进:

  • 如果数据库不经常更新/您不关心极端准确度,您可以提前预先计算每个城市匹配计数(每天一次?每周一次?取决于您的应用程序)。这将加快确定每个城市的skiplimit 所需的countDocuments 部分。
  • 最后一个for 循环可以是类似于计数的Promise.all,以加快结果。同样,如果城市数量永远不会太高,这可能是一个很好的解决方案。
  • 最后,不清楚单辆车是否可以与多个城市相关,但如果是这种情况,则需要在已匹配的文档上添加排除条件。

【讨论】:

  • 我不太了解您的代码,但在发布此问题后,我想到可以按我想要的顺序查询各个城市。例如:如果我想先从 Gurugram 获得结果,然后是德里,然后是新德里,我将从查询 Gurugram 开始,然后在获得 Gurugram 的所有结果后,我将开始查询德里,最后是新德里。这样我就不必依赖聚合查询,也可以使用isLivevehicleConditionvehicleDetails.city 上的索引。所以所有的查询都会很快。我想你已经描述了同样的想法。
  • 是的,这就是我试图传达的内容,代码只是提前预先计算所需的 skiplimit 值,因此您可以同时进行实际提取(这是“最慢” 部分功能),但是只是一个一个地做没有问题。我觉得这是最好的方法
  • 我在这个问题中有一些细节,没有这些,这是最好的答案。但根据我目前的要求,我决定暂时采用不同的解决方案。我将为此使用地理空间查询。我发布了一个与此相关的新问题。你能看看吗?谢谢。
【解决方案3】:

您可以在匹配后添加自定义排序顺序字段,以根据输入的城市条件顺序和搜索优先顺序对字段进行排序。由于是计算出来的字段索引,不会用于排序。

您可以在末尾添加分页,就像对任何其他查询一样。

类似

db.collection.aggregate([
  {
    $match: {
      "isLive": true,
      "vehicleDetails.city": {
        $in: [
          "Gurugram",
          "Delhi",
          "New Delhi"
        ]
      },
      "vehicleCondition": "USED"
    }
  },
  {
    "$addFields": {
      "cityIndex": {
        "$indexOfArray": [
          [
            "Gurugram",
            "Delhi",
            "New Delhi"
          ],
          "$vehicleDetails.city"
        ]
      }
    }
  },
  {
    $sort: {
      cityIndex: 1,
      sortPriority: 1
    }
  },
  {
    $project: {
      cityIndex: 0
    }
  }
])

工作示例可以在这里找到 - https://mongoplayground.net/p/16-YOkPotLX

【讨论】:

  • 谢谢。您能否再回答一些问题(从未使用过聚合查询)。这个聚合查询将如何工作?每次我做.limit(X)时,它会先获取主内存中所有匹配的文档,然后对它们进行排序,然后返回所需的Xdocuments吗?在完成此排序时,是否整个数据库或正在使用的特定集合被阻止用于其他读/写操作?由于此查询将成为将非常频繁使用的 Web API 的一部分,有没有办法让它更快?由于它不使用索引,所以每次都会进行一次完整的集合扫描?
  • 不客气。是的。否 - 允许并发访问。我想不出任何能让它更快的方法。这是您可以为您的用例获得的最佳效果。只要您的工作集相同,它就不会进行集合扫描。
猜你喜欢
  • 2018-08-15
  • 1970-01-01
  • 1970-01-01
  • 2020-11-29
  • 2011-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多