如何在MongoDB中构建关键字Historgam?

时间:2017-09-07 04:23:40

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

我正在使用MongoDB 3.4和PyMongo。我有一组关键字:

keywords = [ 'bar', 'foo', ..., 'zoo' ]

我也有一个系列:

docs = { 'data' : ' ... bar foo ... ',
         'data' : ' ... foo ... ',
         'data' : ' ... zoo ... ' }

我正在寻找一个PyMongo聚合查询,它会给我一个字典:

{ 'bar' : 0, 'foo' : 2, ..., 'zoo' : 0 }

1 个答案:

答案 0 :(得分:1)

没有任何特定于语言的语言,因为唯一的解决方案是全部聚合或使用mapReduce,后者在JavaScript函数中定义

只需设置一些示例数据:

db.wordstuff.insertMany([
  { 'data': "foo brick bar" },
  { 'data': "brick foo" },
  { 'data': "bar brick baz" },
  { 'data': "bax" },
  { 'data': "brin brok fu foo" }
])

聚合框架

然后你可以运行聚合语句:

db.wordstuff.aggregate([
  { "$project": {
    "_id": 0,
    "split": {
      "$filter": {
        "input": { "$split": [ "$data", " " ] },
        "cond": { "$in": [ "$$this", ["bar","foo","baz","blat"] ] }
      } 
    }   
  }},
  { "$unwind": "$split" },
  { "$group": { "_id": "$split", "count": { "$sum": 1 } }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$count" } }    
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": {
        "$map": {
          "input": ["bar","foo","baz","blat"],
          "as": "d",
          "in": {
            "$cond": {
              "if": { "$ne": [{ "$indexOfArray": ["$data.k","$$d"] },-1] },
              "then": { 
                "$arrayElemAt": [
                  "$data",
                  { "$indexOfArray": ["$data.k","$$d"] }
                ]
              },
              "else": { "k": "$$d", "v": 0 }
            }
          }
        }
      }
    }
  }}
])

实际上,所有的实际工作都是在这一点上完成的:

db.wordstuff.aggregate([
  { "$project": {
    "_id": 0,
    "split": {
      "$filter": {
        "input": { "$split": [ "$data", " " ] },
        "cond": { "$in": [ "$$this", ["bar","foo","baz","blat"] ] }
      } 
    }   
  }},
  { "$unwind": "$split" },
  { "$group": { "_id": "$split", "count": { "$sum": 1 } }},
])

这给你的输出如下:

{ "_id" : "baz", "count" : 1.0 }
{ "_id" : "bar", "count" : 2.0 }
{ "_id" : "foo", "count" : 3.0 }

所以这里真正的工作是由$split完成的,这是使用聚合框架的主要依赖,所以至少为了做到这一点你需要MongoDB 3.4。非常简单的前提是$split将单词作为数组成员单独输出,然后$filter将内容与要匹配的输入数组匹配。

$filter使用$in,这是MongoDB 3.4的另一个补充,以匹配每个列出的单词。还有其他运算符可以使用更长的语法来执行此操作,但我们知道我们已经需要MongoDB 3.4,因此这是最短的语法。

之后真正完成的是$unwind来自每个文档的匹配数组,然后$group将这些匹配的单词作为不同的列表以及出现次数获得。

从数据库的主要角度来看,这就是它的全部内容。

以下部分实际上是"可选"因为它们很容易在代码中重现,并且通过这样做可能看起来更清晰,更清晰。但只是为了演示需要MongoDB 3.4.4的新运算符,至少是为了引入$arrayToObject

基本原则是下一个$group"汇总"将光标中匹配的单词转换为单个文档中的数组。由于以后的原因,"k""v"也应用了非常具体的关键命名。

然后使用$replaceRoot阶段,因为返回的文档内容是从表达式计算的。该表达式使用$map迭代"输入数组"单词和匹配从聚合创建的条目。使用$indexOfArray完成此匹配。返回匹配值的匹配索引。

您在$cond中使用此功能,因为您要使用$arrayElemAt将该值转换为匹配的元素,或者可选择识别索引不匹配。这将返回聚合条目(从早期匹配中获得)或"默认"给定单词的0值。

最后一部分使用$arrayToObject将具有属性"k""v"的对象数组转换为"键/值"成对作为对象。

因此您可以要求MongoDB执行此操作,但实际上数据实际上是通过最小管道减少的,因此您也可以在客户端代码中执行此操作。它非常简单,对于JavaScript,您只需:

var words = db.wordstuff.aggregate([
  { "$project": {
    "_id": 0,
    "split": {
      "$filter": {
        "input": { "$split": [ "$data", " " ] },
        "cond": { "$in": [ "$$this", ["bar","foo","baz","blat"] ] }
      } 
    }   
  }},
  { "$unwind": "$split" },
  { "$group": { "_id": "$split", "count": { "$sum": 1 } }},
]).toArray();


var result = ["bar","foo","baz","blat"].map(
  w => ( words.map(wd => wd._id).indexOf(w) !== -1)
    ? words[words.map(wd => wd._id).indexOf(w)]
    :  { _id: w, count: 0 }
).reduce((acc,curr) => Object.assign(acc,{ [curr._id]: curr.count }),{})

因此,如果有任何语言特定的话,那就是其中的一部分。因此,如果您选择在其基础上运行聚合并处理生成的游标,那么python代码将是:

input = ["bar","foo","baz","blat"]

words = list(db.wordstuff.aggregate([
  { "$project": {
    "_id": 0,
    "split": {
      "$filter": {
        "input": { "$split": [ "$data", " " ] },
        "cond": { "$in": [ "$$this", input ] }
      } 
    }   
  }},
  { "$unwind": "$split" },
  { "$group": { "_id": "$split", "count": { "$sum": 1 } }},
]))

result = reduce(
  lambda x,y:
    dict(x.items() + { y['_id']: y['count'] }.items()),
    map(lambda w: words[map(lambda wd: wd['_id'],words).index(w)]
      if w in map(lambda wd: wd['_id'],words)
      else { '_id': w, 'count': 0 },
      input
    ),
    {}
)

任何一种方法都会得出相同的结果:

{
    "bar" : 2.0,
    "foo" : 3.0,
    "baz" : 1.0,
    "blat" : 0.0
}

的MapReduce

您甚至没有可用的最小MongoDB 3.4.0的替代情况是使用mapReduce代替该过程。同样,这需要作为JavaScript发送到服务器,这通常表示在"字符串"在大多数语言实现中(除了JavaScript本身):

db.wordstuff.mapReduce(
  function() {
    this.data.split(' ')
      .filter( w => words.indexOf(w) !== -1 )
      .forEach( w => emit(null,{ [w]: 1 }) );
  },
  function(key,values) {
    return [].concat.apply([],
      values.map(v => Object.keys(v).map(k => ({ k: k, v: v[k] })))
    ).reduce((acc,curr) => Object.assign(acc,{
      [curr.k]: (acc.hasOwnProperty(curr.k))
        ? acc[curr.k] + curr.v : curr.v
    }),{});
  },
  {
    "out": { "inline": 1 },
    "scope": { "words": ["bar","foo","baz","blat"] },
    "finalize": function(key,value) {
      return words.map( w => (value.hasOwnProperty(w))
        ? { [w]: value[w] } : { [w]: 0 }
      ).reduce((acc,curr) => Object.assign(acc,curr),{}) 
    }
  }
)

这会给你相同的结果,并且确实做同样的事情。稍微慢一些,因为MongoDB需要评估和处理JavaScript,而不是使用它自己的本地编码方法和聚合框架。