[解決済み] mongodb は複数のフィールドで値をグループ化します。
質問
例えば、こんな文書があります。
{
"addr": "address1",
"book": "book1"
},
{
"addr": "address2",
"book": "book1"
},
{
"addr": "address1",
"book": "book5"
},
{
"addr": "address3",
"book": "book9"
},
{
"addr": "address2",
"book": "book5"
},
{
"addr": "address2",
"book": "book1"
},
{
"addr": "address1",
"book": "book1"
},
{
"addr": "address15",
"book": "book1"
},
{
"addr": "address9",
"book": "book99"
},
{
"addr": "address90",
"book": "book33"
},
{
"addr": "address4",
"book": "book3"
},
{
"addr": "address5",
"book": "book1"
},
{
"addr": "address77",
"book": "book11"
},
{
"addr": "address1",
"book": "book1"
}
といった具合です。
上位N件の住所と、住所ごとの上位M件の本を記述するリクエストはどのようにすればよいのでしょうか?
期待される結果の例
アドレス1|ブック1:5
| ブック_2: 10
| ブック_3: 50
| 合計:65
______________________
アドレス2|ブック1:10
| ブック_2: 10
|...
| book_M: 10
| 合計:M*10
...
______________________
アドレスN|book_1:20
| ブック_2: 20
|...
| book_M: 20
| 合計:M*20
解決方法は?
TLDRの概要
最近のMongoDBのリリースでは、これをブルートフォースで実行することができます。
$slice
基本的な集計結果だけで 大きな結果を得るには、各グループに対して並列クエリを実行します。
SERVER-9377
を解決することで、quot;limit" にアイテム数を制限することができます。
$push
を配列に追加しました。
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$project": {
"books": { "$slice": [ "$books", 2 ] },
"count": 1
}}
])
MongoDB 3.6 プレビュー
まだ解決していません
SERVER-9377
しかし、このリリースでは
$lookup
は、新しいオプションである "non-correlated" を許可します。
"pipeline"
式の代わりに引数として
"localFields"
と
"foreignFields"
オプションで指定します。これにより、別のパイプライン式に "自己結合" を適用することができるようになります。
$limit
を実行すると、quot;top-n" の結果が返される。
db.books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$lookup": {
"from": "books",
"let": {
"addr": "$_id"
},
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$addr", "$$addr"] }
}},
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
],
"as": "books"
}}
])
もうひとつは、当然ながら、変数に
$expr
を使って
$match
でマッチするアイテムを選択することができますが、大前提として、親からのマッチによって内部のコンテンツをフィルタリングすることができる、パイプラインの中のパイプラインと言えます。これらは両方ともパイプラインであるため、次のようにすることができます。
$limit
というように、それぞれの結果を別々に表示します。
これは並列クエリの次善の策であり、実際には
$match
は、quot;サブパイプライン" 処理でインデックスを使用することが許可され、使用できるようになりました。ということで、quot;limit to;を使用しないのはどちらでしょうか?
$push
という問いかけがありますが、実際にはもっとうまく機能するはずのものが提供されています。
オリジナルコンテンツ
トップの"N"の問題につまずいたようですね。ある意味、あなたの問題はかなり簡単に解決できるのですが、あなたが求めるような厳密な限定はできません。
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
])
これで、次のような結果が得られます。
{
"result" : [
{
"_id" : "address1",
"books" : [
{
"book" : "book4",
"count" : 1
},
{
"book" : "book5",
"count" : 1
},
{
"book" : "book1",
"count" : 3
}
],
"count" : 5
},
{
"_id" : "address2",
"books" : [
{
"book" : "book5",
"count" : 1
},
{
"book" : "book1",
"count" : 2
}
],
"count" : 3
}
],
"ok" : 1
}
つまり、アドレスの値に対する上位の結果を取得する一方で、基本となる "books" の選択が必要な量の結果のみに制限されないという点で、ご質問の内容とは異なります。
これは非常に難しいことがわかりましたが、照合する項目の数が増えるにつれて複雑さが増していきますが、実現することは可能です。シンプルにするために、最大でも2つのマッチングにとどめておくことができます。
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$unwind": "$books" },
{ "$sort": { "count": 1, "books.count": -1 } },
{ "$group": {
"_id": "$_id",
"books": { "$push": "$books" },
"count": { "$first": "$count" }
}},
{ "$project": {
"_id": {
"_id": "$_id",
"books": "$books",
"count": "$count"
},
"newBooks": "$books"
}},
{ "$unwind": "$newBooks" },
{ "$group": {
"_id": "$_id",
"num1": { "$first": "$newBooks" }
}},
{ "$project": {
"_id": "$_id",
"newBooks": "$_id.books",
"num1": 1
}},
{ "$unwind": "$newBooks" },
{ "$project": {
"_id": "$_id",
"num1": 1,
"newBooks": 1,
"seen": { "$eq": [
"$num1",
"$newBooks"
]}
}},
{ "$match": { "seen": false } },
{ "$group":{
"_id": "$_id._id",
"num1": { "$first": "$num1" },
"num2": { "$first": "$newBooks" },
"count": { "$first": "$_id.count" }
}},
{ "$project": {
"num1": 1,
"num2": 1,
"count": 1,
"type": { "$cond": [ 1, [true,false],0 ] }
}},
{ "$unwind": "$type" },
{ "$project": {
"books": { "$cond": [
"$type",
"$num1",
"$num2"
]},
"count": 1
}},
{ "$group": {
"_id": "$_id",
"count": { "$first": "$count" },
"books": { "$push": "$books" }
}},
{ "$sort": { "count": -1 } }
])
つまり、上位2つの「住所」エントリから、上位2つの「本」エントリを取得することができます。
しかし、私の考えでは、最初の形式のまま、返された配列の要素を単に "slice" して、最初の "N" 要素を取るようにします。
デモコード
このデモコードは、NodeJSのv8.xおよびv10.xリリースの現在のLTSバージョンで使用するのが適切です。これは主に
async/await
構文がありますが、一般的なフローにはそのような制限はなく、プレーンなプロミスやプレーンなコールバック実装に戻ってもほとんど変更せずに適応します。
index.js
const { MongoClient } = require('mongodb');
const fs = require('mz/fs');
const uri = 'mongodb://localhost:27017';
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const client = await MongoClient.connect(uri);
const db = client.db('bookDemo');
const books = db.collection('books');
let { version } = await db.command({ buildInfo: 1 });
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
// Clear and load books
await books.deleteMany({});
await books.insertMany(
(await fs.readFile('books.json'))
.toString()
.replace(/\n$/,"")
.split("\n")
.map(JSON.parse)
);
if ( version >= 3.6 ) {
// Non-correlated pipeline with limits
let result = await books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$lookup": {
"from": "books",
"as": "books",
"let": { "addr": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$addr", "$$addr" ] }
}},
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 },
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]
}}
]).toArray();
log({ result });
}
// Serial result procesing with parallel fetch
// First get top addr items
let topaddr = await books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]).toArray();
// Run parallel top books for each addr
let topbooks = await Promise.all(
topaddr.map(({ _id: addr }) =>
books.aggregate([
{ "$match": { addr } },
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]).toArray()
)
);
// Merge output
topaddr = topaddr.map((d,i) => ({ ...d, books: topbooks[i] }));
log({ topaddr });
client.close();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
books.json
{ "addr": "address1", "book": "book1" }
{ "addr": "address2", "book": "book1" }
{ "addr": "address1", "book": "book5" }
{ "addr": "address3", "book": "book9" }
{ "addr": "address2", "book": "book5" }
{ "addr": "address2", "book": "book1" }
{ "addr": "address1", "book": "book1" }
{ "addr": "address15", "book": "book1" }
{ "addr": "address9", "book": "book99" }
{ "addr": "address90", "book": "book33" }
{ "addr": "address4", "book": "book3" }
{ "addr": "address5", "book": "book1" }
{ "addr": "address77", "book": "book11" }
{ "addr": "address1", "book": "book1" }
関連
-
macシステムでのmongoDBデータベースのインストールと設定
-
mongodbのインストールと起動の詳細説明
-
[解決済み】MongoDBシェルですべてのコレクションを一覧表示するには?
-
MongoDB起動時の例外エラーと正しいシャットダウン方法
-
MongoDBクエリ
-
[解決済み] MongoDBに "like "を使ってクエリを実行する方法
-
[解決済み] コマンドラインからMongoDBデータベースを削除する方法を教えてください。
-
[解決済み] 別のフィールドの値を使って MongoDB のフィールドを更新する
-
[解決済み] MongoDB SELECT COUNT GROUP BY
-
[解決済み] 小規模な.NETアプリケーションに適したデータベースの選択とは?[クローズド]
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン