PyMongo聚合查询过滤器?

时间:2018-08-17 15:16:25

标签: python mongodb mongodb-query aggregation-framework pymongo

所以我正在寻找类似的问题,但并不能完全回答我要寻找的问题。如果这是重复的;随时将我指向适当的位置。

我有一个集合,它是一些相当大的文档的“真理之源”。在进入主要分析之前,我想使用查询引擎进行一些预过滤。

查询1:

仅检索document.financials.entrycount $ gte 4.的文档。因此,基本上在文档中,我有一个用于财务的子文档。我想用它作为过滤器。我只想返回条目数大于4的文档。

查询2:

能够进行数学运算并将其与数字进行比较以进行检索。

例如:

var randomArray = [];

while(randomArray.length < 10) {
  var random = Math.round(Math.random() * 100);
  if(randomArray.indexOf(random) === -1) {
    randomArray.push(random);
  }
 
}
console.log(randomArray);

这些数字在子文档中。

最后能够将它们组合在一起。

下面是一个示例文档,预计其仅包含季度财务报告。

(totalAssets + totalCash) / (totalDebt + totalLiabilities) < .5 

1 个答案:

答案 0 :(得分:1)

坦率地说,这里的主要问题是文档结构。最重要的是,对于任何形式的数据库来说,名为“按键”的“子文档”通常都不是一件好事,这包括MongoDB。

在使用单个文档处理客户端代码时,“按键查找”可能更有效,而MongoDB可以更好地将“数组”或“集合”之类的结构用于此类自然的“列表”。 / p>

聚合表达式

可行的替代方法是使用诸如$objectToArray之类的聚合运算符,以便将该表单“强制”为“自然列表”进行处理,以便您可以按该列表中的条目数进行过滤:

collection.aggregate([
  { "$match": {
    "$expr": {
      "$gte": [
        { "$size": { "$objectToArray": "$quarterly_financials" } },
        4
      ]
    }
  }}
])

请注意,这是使用MongoDB 3.6及更高版本中的$expr。如果您没有该支持版本,但仍从更高版本的MongoDB 3.4发行版中获得$objectToArray(即使文档说它实际上是在那些更高发行版中,则为3.6),则可以在其中使用类似$redact的名称$match或常规find()的位置。

附加的计算表达式也是如此。最重要的是,您仍然需要“数组转换”才能真正处理和遍历那些“列表元素” 。因此,如果只有那些满足条件的子项加起来等于所需的四个,那么您将在数组元素上使用$filter条件进行更改:

collection.aggregate([
  { "$match": {
      "$expr": {
        "$gte": [
          { "$size": { 
            "$filter": {
              "input": { "$objectToArray": "$quarterly_financials" },
              "cond": {
                "$lt": [
                  { "$divide": [
                    { "$add": [ "$$this.v.totalAssets", "$$this.v.totalCash" ] },
                    { "$add": [ 
                      "$$this.v.totalDebt",
                      { "$ifNull": [ "$$this.v.totalLiabilities", 0 ] }
                    ]}
                  ]},
                  .5
                ]
              }
            }
          }},
          4
        ]
      }
  }}
])

因此,在“强制转换为数组” 之后,将使用$filter检查每个列表项,以使用$divide$add这样的运算符确定数学表达式的位置满足$lt的逻辑条件,然后使用$size运算符考虑剩下的过滤数组的“ length”

还要注意,$objectToArray本质上将每个子文档转换为具有以下形式的列表:

   {
        "k" : "2018-06-30",
        "v" : {
            "cashChange" : 97525000,
            "cashFlow" : 106786000,
            "costOfRevenue" : 548491000,
            "currentAssets" : 565191000,
            "currentCash" : 227929000,
            "currentDebt" : 245322000,
            "grossProfit" : 117654000,
            "netIncome" : -21150000,
            "operatingExpense" : 47334000,
            "operatingGainsLosses" : null,
            "operatingIncome" : 70320000,
            "operatingRevenue" : 664531000,
            "researchAndDevelopment" : null,
            "shareholderEquity" : 1776073000,
            "totalAssets" : 5348343000,
            "totalCash" : 234280000,
            "totalDebt" : 2501488000,
            "totalLiabilities" : null,
            "totalRevenue" : 666145000
        }
    }

这意味着您要查找的所有内容都在新转换的“列表”中每个文档的"v"属性(或“ value” )下。 "k"属性当然是您以“子文档”形式命名的“密钥”。

还需要$ifNull处理属性的null(对于Python来说是None)的值,或者实际上是“ missing” 属性,合适的。

JavaScript评估

确实不建议这样做,但是您的MongoDB不支持较新的运算符(例如$objectToArray)的另一种替代方法是在服务器上使用JavaScript评估,例如$wheremapReduce处理这种逻辑。

相同的原则适用于您必须首先“强制”为数组形式。 (这里省略了“ shell形式”的简写示例。只是所有其他语言中的字符串):

collection.find(function() {
  var quarter = this.quarterly_financials;
  return Object.keys(quarter).filter( k => 
    ( 
      ( quarter[k].totalAssets + quarter[k].totalCash ) /
      ( quarter[k].totalDebt + ( quarter[k].totalLiabilites || 0 ) )
    ) < .5
  ).length >= 4
})

尽管“噪音较小” 并不是最佳选择,因为用于评估服务器上JavaScript表达式的“ cost” 远高于自然语言聚合表达式。某些环境和服务器配置实际上根本不允许您使用此类JavaScript表达式的可能性。

还请注意,如果您真的希望在稍后的分析阶段进行“聚合”,则需要将该逻辑分解为mapReduce,因为不可能在聚合中使用$where查询表达式管道。

备用结构

最后,由于所有内容都取决于您的“命名键”中的“ “创建列表” ”,因此更好的方法通常是首先以这种方式构造数据(请注意,{{3} }):

{
  "symbol": "AAWW",
  "quarterly_financials": [
    { 
      "tranDate": { "$date": "2017-09-30T00:00:00Z"},
      "cashChange": -106467000,
      "cashFlow": 82299000,
      "costOfRevenue": 439135000,
      "currentAssets": 449776000,
      "currentCash": 176280000,
      "currentDebt": 196509000,
      "grossProfit": 96613000,
      "netIncome": -24162000,
      "operatingExpense": 43690000,
      "operatingGainsLosses": 378000,
      "operatingIncome": 52923000,
      "operatingRevenue": 535748000,
      "researchAndDevelopment": null,
      "shareholderEquity": 1575169000,
      "totalAssets": 4687302000,
      "totalCash": 175926000,
      "totalDebt": 2105344000,
      "totalLiabilities": null,
      "totalRevenue": 535748000
    },
    {
      "tranDate": { "$date": "2017-12-31T00:00:00Z" },
      "cashChange": 115584000,
      "cashFlow": 136613000,
      "costOfRevenue": 474565000,
      "currentAssets": 587586000,
      "currentCash": 291864000,
      "currentDebt": 218013000,
      "grossProfit": 153387000,
      "netIncome": 209448000,
      "operatingExpense": 46628000,
      "operatingGainsLosses": -95000,
      "operatingIncome": 106759000,
      "operatingRevenue": 627952000,
      "researchAndDevelopment": null,
      "shareholderEquity": 1789856000,
      "totalAssets": 4955462000,
      "totalCash": 294413000,
      "totalDebt": 2226999000,
      "totalLiabilities": null,
      "totalRevenue": 627952000
    },
    { 
      "tranDate": { "$date": "2018-03-31T00:00:00Z" },
      "cashChange": -161460000,
      "cashFlow": 69125000,
      "costOfRevenue": 498924000,
      "currentAssets": 433193000,
      "currentCash": 130404000,
      "currentDebt": 223308000,
      "grossProfit": 91090000,
      "netIncome": 9612000,
      "operatingExpense": 50521000,
      "operatingGainsLosses": null,
      "operatingIncome": 40569000,
      "operatingRevenue": 590014000,
      "researchAndDevelopment": null,
      "shareholderEquity": 1792299000,
      "totalAssets": 5016832000,
      "totalCash": 136421000,
      "totalDebt": 2270870000,
      "totalLiabilities": null,
      "totalRevenue": 590014000
    },
    { 
      "tranDate": { "$date": "2018-06-30T00:00:00Z" },
      "cashChange": 97525000,
      "cashFlow": 106786000,
      "costOfRevenue": 548491000,
      "currentAssets": 565191000,
      "currentCash": 227929000,
      "currentDebt": 245322000,
      "grossProfit": 117654000,
      "netIncome": -21150000,
      "operatingExpense": 47334000,
      "operatingGainsLosses": null,
      "operatingIncome": 70320000,
      "operatingRevenue": 664531000,
      "researchAndDevelopment": null,
      "shareholderEquity": 1776073000,
      "totalAssets": 5348343000,
      "totalCash": 234280000,
      "totalDebt": 2501488000,
      "totalLiabilities": null,
      "totalRevenue": 666145000
    }
  ]
}

由于它已经是一个“列表”,因此只需跳过extended JSON format部分(或与JavaScript相似的部分):

collection.aggregate([
  { "$match": {
    "$expr": {
      "$gte": [
        { "$size": { 
          "$filter": {
            "input": "$quarterly_financials",
            "cond": {
              "$lt": [
                { "$divide": [
                  { "$add": [ "$$this.totalAssets", "$$this.totalCash" ] },
                  { "$add": [ 
                    "$$this.totalDebt",
                    { "$ifNull": [ "$$this.totalLiabilities", 0 ] }
                  ]}
                ]},
                .5
              ]
            }
          }
        }},
        4
      ]
    }
  }}
])

使用这种结构还有很多其他优点,其中大多数通常甚至会完全避免使用评估表达式并能够使用自然查询表达式。实际上,如果您的条件并非实际上四个需要依赖于这样的“已过滤” 条件,那么如果您已经 >“预先计算” 在存储时预先在每个列表条目中使用数学表达式。

“结构” 始终是您真正应该考虑的,以获得最佳查询性能,因为任何形式的“评估” 都会引起集合扫描,并且非常昂贵。

因此,“使用列表”中的事物是列表,而计算是“静态的”,那么请事先进行处理并存储它们,而不是在运行时进行计算。