1638714180
MongoDBの集約パイプライン は、その最も強力な機能の1つです。これらを使用すると、MongoDBデータベース内のデータの集計、変換、結合などの操作を実行する一連のステージに分割された式を記述できます。これにより、MongoDBデータベース内のドキュメントとコレクション全体で計算と分析を行うことができます。
このクイックスタートは、一連のRust投稿の2番目です。私の最初の投稿であるRustでの基本的なMongoDB操作から始めることを強くお勧め します。ここでは、ここで使用するサンプルデータを含む無料のMongoDBAtlasデータベースクラスターを正しくセットアップする方法を説明 します。それを読んで戻ってきてください。待ちます。これがないと、このクイックスタートガイドのコードを実行するためにデータベースを正しく設定できません。
要約すると、次のものが必要です。
Rustの最新バージョン。1.49を使用しましたが、最近のバージョンであれば問題なく動作するはずです。
選択したコードエディタ。私はお勧めVSコードで錆アナライザ拡張。
sample_mflix
データセットを含むMongoDBクラスター。これを設定する手順 は、このシリーズの最初のブログ投稿にあります。
MongoDBの集約パイプラインは非常に強力であるため、最初は少し圧倒されるように思われるかもしれません。このため、ゆっくりと始めます。まず、私はあなたがすでにのMongoDBので達成できることを行動複製パイプラインアップ構築する方法を紹介しますfind()
方法を、代わりに集約パイプラインを使用して$match
、$sort
と$limit
段階。次に、で実行できる範囲を超えるクエリを作成する方法を示し、別のコレクションからの関連ドキュメントを含めるためにfind
使用$lookup
する方法を示します。最後に、$group
ドキュメントをグループ化して新しいドキュメントの概要を作成する方法を示して、「アグリゲーション」を「アグリゲーションパイプライン」に入れます。
このクイックスタートシリーズのすべてのサンプルコードは、GitHubにあります。行き詰まったらチェックすることをお勧めしますが、そうでない場合は、チュートリアルに従って自分でコードを書く価値があります。
この投稿のすべてのパイプラインは、sample_mflix データベースのmovies
コレクションに対して実行され ます。これには、次のようなドキュメントが含まれています(同等のRust構造体よりも少し読みやすいため、Pythonでどのように表示されるかを示しています)。
{
'_id': ObjectId('573a1392f29313caabcdb497'),
'awards': {'nominations': 7,
'text': 'Won 1 Oscar. Another 2 wins & 7 nominations.',
'wins': 3},
'cast': ['Janet Gaynor', 'Fredric March', 'Adolphe Menjou', 'May Robson'],
'countries': ['USA'],
'directors': ['William A. Wellman', 'Jack Conway'],
'fullplot': 'Esther Blodgett is just another starry-eyed farm kid trying to '
'break into the movies. Waitressing at a Hollywood party, she '
'catches the eye of alcoholic star Norman Maine, is given a test, '
'and is caught up in the Hollywood glamor machine (ruthlessly '
'satirized). She and her idol Norman marry; but his career '
'abruptly dwindles to nothing',
'genres': ['Drama'],
'imdb': {'id': 29606, 'rating': 7.7, 'votes': 5005},
'languages': ['English'],
'lastupdated': '2015-09-01 00:55:54.333000000',
'plot': 'A young woman comes to Hollywood with dreams of stardom, but '
'achieves them only with the help of an alcoholic leading man whose '
'best days are behind him.',
'poster': 'https://m.media-amazon.com/images/M/MV5BMmE5ODI0NzMtYjc5Yy00MzMzLTk5OTQtN2Q3MzgwOTllMTY3XkEyXkFqcGdeQXVyNjc0MzMzNjA@._V1_SY1000_SX677_AL_.jpg',
'rated': 'NOT RATED',
'released': datetime.datetime(1937, 4, 27, 0, 0),
'runtime': 111,
'title': 'A Star Is Born',
'tomatoes': {'critic': {'meter': 100, 'numReviews': 11, 'rating': 7.4},
'dvd': datetime.datetime(2004, 11, 16, 0, 0),
'fresh': 11,
'lastUpdated': datetime.datetime(2015, 8, 26, 18, 58, 34),
'production': 'Image Entertainment Inc.',
'rotten': 0,
'viewer': {'meter': 79, 'numReviews': 2526, 'rating': 3.6},
'website': 'http://www.vcientertainment.com/Film-Categories?product_id=73'},
'type': 'movie',
'writers': ['Dorothy Parker (screen play)',
'Alan Campbell (screen play)',
'Robert Carson (screen play)',
'William A. Wellman (from a story by)',
'Robert Carson (from a story by)'],
'year': 1937}
そこ多くのデータがありますが、私は上の中心にすることがあります_id
、title
、year
、およびcast
フィールド。
集約パイプラインは、コレクションのaggregate() メソッドを使用してmongodbモジュールによって実行されます 。
の最初の引数aggregate()
は、実行される一連のパイプラインステージです。クエリと同様に、集計パイプラインの各ステージは BSONドキュメントです。これらdoc!
は、前の投稿で紹介したマクロを使用して作成することがよくあります。
集約パイプラインは、コレクション内のすべてのデータを処理します。パイプラインの各ステージは通過するドキュメントに適用され、あるステージから放出されたドキュメントはすべて、残りのステージがなくなるまで次のステージへの入力として渡されます。この時点で、パイプラインの最後のステージから発行されたドキュメントは、への呼び出しと同様の方法で、カーソルとしてクライアントプログラムに返されますfind()
。
などの個々のステージは、$match
特定の基準に一致するドキュメントのみを通過させるためのフィルターとして機能できます。他のステージのようなタイプの、$project
、$addFields
、とは$lookup
彼らがパイプラインを通過すると、個々の文書の内容を変更します。最後に、などの特定のステージタイプは、$group
渡されたドキュメント全体に基づいて、まったく新しいドキュメントのセットを作成します。これらのステージはいずれも、MongoDB自体に格納されているデータを変更しません。プログラムに戻す前にデータを変更するだけです。$ outのように、パイプラインの結果をMongoDBに保存できるステージがありますが、このクイックスタートでは取り上げません。
前回の投稿で使用したのと同じ環境で作業していると想定します。そのため、ファイルに依存関係としてmongodbクレートが既に構成されているCargo.toml
必要があり.env
、MONGODB_URI
環境変数を含むファイルが必要です。
まず、以下をRustコードに貼り付けます。
// Load the MongoDB connection string from an environment variable:
let client_uri =
env::var("MONGODB_URI").expect("You must set the MONGODB_URI environment var!");
// An extra line of code to work around a DNS issue on Windows:
let options =
ClientOptions::parse_with_resolver_config(&client_uri, ResolverConfig::cloudflare())
.await?;
let client = mongodb::Client::with_options(options)?;
// Get the 'movies' collection from the 'sample_mflix' database:
let movies = client.database("sample_mflix").collection("movies");
上記のコードは、データベース内のコレクションを指す、とCollection
呼ばれるインスタンスを提供します。movie_collectionmovies
パイプラインを作成し、それをで実行してaggregate
から、ループして結果の各映画の詳細を出力するコードを次に示します。プログラムに貼り付けます。
// Usually implemented outside your main function:
#[derive(Deserialize)]
struct MovieSummary {
title: String,
cast: Vec<String>,
year: i32,
}
impl fmt::Display for MovieSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}, {}, {}",
self.title,
self.cast.get(0).unwrap_or(&"- no cast -".to_owned()),
self.year
)
}
}
// Inside main():
let pipeline = vec![
doc! {
// filter on movie title:
"$match": {
"title": "A Star Is Born"
}
},
doc! {
// sort by year, ascending:
"$sort": {
"year": 1
}
},
];
// Look up "A Star is Born" in ascending year order:
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the results, convert each of them to a MovieSummary, and then print out.
while let Some(result) = results.next().await {
// Use serde to deserialize into the MovieSummary struct:
let doc: MovieSummary = bson::from_document(result?)?;
println!("* {}", doc);
}
このパイプラインには2つの段階があります。1つ目は$ matchステージで、これはfind()
。を使用してコレクションをクエリするのと似ています。読み取り操作クエリに基づいて、ステージを通過するドキュメントをフィルタリングします。これはパイプラインの最初のステージであるため、その入力はmovie
コレクション内のすべてのドキュメントです。$match
ステージのクエリtitle
は入力ドキュメントのフィールドでフィルタリングされるため、このステージから出力されるドキュメントは「スター誕生」というタイトルのみになります。
2番目のステージは$ sort ステージです。映画「アリー/スター誕生」のドキュメントのみがこのステージに渡されるため、結果として「アリー/スター誕生」と呼ばれるすべての映画が年のフィールドで並べ替えられ、最も古い映画が最初になります。
Aggregate()を呼び出すと、結果のドキュメントを指すカーソルが返されます。カーソルはストリームトレイトを実装します。メソッドを提供するStreamExtをインポートしている限り、カーソルは他のストリームと同じようにループできますnext()
。上記のコードは、返されたすべてのドキュメントをループし、タイトル、cast
配列の最初の俳優、映画が制作された年で構成される短い要約を出力します。
上記のコードを実行すると、次のようになります。
* A Star Is Born, Janet Gaynor, 1937
* A Star Is Born, Judy Garland, 1954
* A Star Is Born, Barbra Streisand, 1976
上記の例のように、集約パイプライン全体を単一のデータ構造として構築することは可能ですが、必ずしも良い考えではありません。パイプラインは長く複雑になる可能性があります。このため、パイプラインの各ステージを個別の変数として構築し、最後に次のようにステージをパイプラインに結合することをお勧めします。
// Match title = "A Star Is Born":
let stage_match_title = doc! {
"$match": {
"title": "A Star Is Born"
}
};
// Sort by year, ascending:
let stage_sort_year_ascending = doc! {
"$sort": { "year": 1 }
};
// Now the pipeline is easier to read:
let pipeline = vec![stage_match_title, stage_sort_year_ascending];
映画コレクションから「アリー/スター誕生」の最新作を入手したいと想像してみてください。
これは、次の順序で実行される3つの段階と考えることができます。
「アリー/スター誕生」の映画ドキュメントを入手してください。
年の降順で並べ替えます。
最初のドキュメントを除くすべてを破棄します。
最初の段階はすでに上記と同じstage_match_title
です。2番目のステージはと同じstage_sort_year_ascending
ですが、値がに1
変更されてい-1
ます。3番目のステージは$ limitステージです。
変更され、新しい、このようなコードになります。
// Sort by year, descending:
let stage_sort_year_descending = doc! {
"$sort": {
"year": -1
}
};
// Limit to 1 document:
let stage_limit_1 = doc! { "$limit": 1 };
let pipeline = vec![stage_match_title, stage_sort_year_descending, stage_limit_1];
上記の変更を行ってコードを実行すると、次の行が表示されます。
* A Star Is Born, Barbra Streisand, 1976
ちょっと待って!レディー・ガガとブラッドリー・クーパーとの素晴らしい作品のドキュメントがないのはなぜですか?
今学んだスキルを使って、コレクションの最新の日付を見つけてみませんか?それはあなたにあなたの答えを与えるでしょう!
これで、集計パイプラインを使用してコレクションのコンテンツをフィルタリング、並べ替え、制限する方法がわかりました。しかし、これらはあなたがすでに行うことができる単なる操作ですfind()
!なぜこれらの複雑で新しい集約パイプラインを使用したいのですか?
読んでください、私の友人、そして私はあなたにMongoDBアグリゲーションパイプラインの真の力をお見せします。
sample_mflix
データベースに隠れている汚い秘密があります。movies
コレクションだけでなく、と呼ばれるコレクションもありますcomments
。comments
コレクション内のドキュメントは次のようになります。
{
'_id': ObjectId('5a9427648b0beebeb69579d3'),
'movie_id': ObjectId('573a1390f29313caabcd4217'),
'date': datetime.datetime(1983, 4, 27, 20, 39, 15),
'email': 'cameron_duran@fakegmail.com',
'name': 'Cameron Duran',
'text': 'Quasi dicta culpa asperiores quaerat perferendis neque. Est animi '
'pariatur impedit itaque exercitationem.'}
映画へのコメントです。なぜ人々がこれらの映画にラテン語のコメントを書いているのかわかりませんが、それで行きましょう。2番目のフィールドは、コレクション内のドキュメントmovie_id,
の_id
値に対応しますmovies
。
だから、映画に関連したコメントです!
MongoDBを使用すると、映画をクエリしたり、リレーショナルデータベースのJOINなどの関連コメントを埋め込んだりできますか? はい、そうです— $ lookupステージで。
別のコレクションから関連ドキュメントを取得し、それらをプライマリコレクションのドキュメントに埋め込む方法を説明します。
まず、MovieSummary
構造体の定義を変更してcomments
、related_comments
BSONフィールドからロードされたフィールドを持つようにします。ドキュメントにComment
含まれるデータのサブセットを含む構造体を定義しcomments
ます。
#[derive(Deserialize)]
struct MovieSummary {
title: String,
cast: Vec<String>,
year: i32,
#[serde(default, rename = "related_comments")]
comments: Vec<Comment>,
}
#[derive(Debug, Deserialize)]
struct Comment {
email: String,
name: String,
text: String,
}
次に、新しいパイプラインを最初から作成し、次の手順から始めます。
// Look up related documents in the 'comments' collection:
let stage_lookup_comments = doc! {
"$lookup": {
"from": "comments",
"localField": "_id",
"foreignField": "movie_id",
"as": "related_comments",
}
};
// Limit to the first 5 documents:
let stage_limit_5 = doc! { "$limit": 5 };
let pipeline = vec![
stage_lookup_comments,
stage_limit_5,
];
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the results and print a summary and the comments:
while let Some(result) = results.next().await {
let doc: MovieSummary = bson::from_document(result?)?;
println!("* {}, comments={:?}", doc, doc.comments);
}
私が呼んstage_lookup_comments
だ$lookup
ステージはステージです。この$lookup
ステージcomments
では、同じムービーIDを持つコレクションからドキュメントを検索します。一致するコメントはrelated_comments
、という名前のBSONフィールドに配列としてリストされ、配列値には、この映画の_id
値がmovie_id
。であるすべてのコメントが含まれます。
$limit
圧倒されることなく適度な量の出力があることを確認するために、ステージを追加しました。
次に、コードを実行します。
上記のパイプラインの実行がかなり遅いことに気付くかもしれません。これには2つの理由があります。
23.5kの映画ドキュメントと50kのコメントがあります。
comments
コレクションに欠落しているインデックスがあります。インデックスについて教えるために、意図的に欠落しています。インデックスの問題を修正する方法については、ここでは説明しません。これについては、このシリーズの後半の投稿で、インデックスに焦点を当てて説明します。代わりに、開発中に低速の集約パイプラインを操作するための秘訣を紹介します。
パイプラインを作成してテストしている間、遅いパイプラインでの作業は苦痛です。 ただし、パイプライン
$limit
の開始時に一時的なステージを配置すると、クエリが高速になります(ただし、データセット全体を実行していないため、結果が異なる場合があります)。このパイプラインを書いていたとき、私はの最初の段階を持っていました
{ "$limit": 1000 }
。パイプラインの作成が終了したら、最初のステージをコメントアウトして、パイプラインがコレクション全体で実行されるようにすることができます。 最初のステージを削除することを忘れないでください。そうしないと、間違った結果が得られます。
上記の集約パイプラインは、5つの映画ドキュメントの要約を印刷します。私はあなたの映画の要約のほとんどまたはすべてがこれで終わることを期待しています:comments=[]
。
運が良ければ、配列にいくつかのドキュメントがあるかもしれませんが、ほとんどの映画にはコメントがないため、それはありそうにありません。次に、コメントが3つ以上ある映画のみに一致するようにいくつかのステージを追加する方法を示します。
理想的に$match
は、related_comments
フィールドの長さを取得し、それを式と照合する単一のステージを追加できるはず{ "$gt": 2 }
です。この場合、実際には2つのステップです。
フィールドcomment_count
の長さを含むフィールド(これを呼びます)を追加しrelated_comments
ます。
の値がcomment_count
2より大きい場合に一致します。
2つのステージのコードは次のとおりです。
// Calculate the number of comments for each movie:
let stage_add_comment_count = doc! {
"$addFields": {
"comment_count": {
"$size": "$related_comments"
}
}
};
// Match movie documents with more than 2 comments:
let stage_match_with_comments = doc! {
"$match": {
"comment_count": {
"$gt": 2
}
}
};
2つのステージは、$lookup
ステージの後と$limit
5つのステージの前にあります。
let pipeline = vec![
stage_lookup_comments,
stage_add_comment_count,
stage_match_with_comments,
limit_5,
]
ここにいる間、このコードの出力をクリーンアップして、コメントを少し適切にフォーマットします。
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the results and print a summary and the comments:
while let Some(result) = results.next().await {
let doc: MovieSummary = bson::from_document(result?)?;
println!("* {}", doc);
if doc.comments.len() > 0 {
// Print a max of 5 comments per movie:
for comment in doc.comments.iter().take(5) {
println!(
" - {} <{}>: {}",
comment.name,
comment.email,
comment.text.chars().take(60).collect::<String>(),
);
}
} else {
println!(" - No comments");
}
}
今、あなたがこのコードを実行するとき、あなたはもっとこのようなものが表示されます。
* Midnight, Claudette Colbert, 1939
- Sansa Stark <sansa_stark@fakegmail.com>: Error ex culpa dignissimos assumenda voluptates vel. Qui inventore
- Theon Greyjoy <theon_greyjoy@fakegmail.com>: Animi dolor minima culpa sequi voluptate. Possimus necessitatibu
- Donna Smith <donna_smith@fakegmail.com>: Et esse nulla ducimus tempore aliquid. Suscipit iste dignissimos v
ゲーム・オブ・スローンズのサンサ・スタークが彼女のラテン語を本当に知っているのを見るのは良いことですよね?
これで、パイプラインでルックアップを操作する方法を示しました。$group
ステージを使用して実際の集計を行う方法を示します。
$group
もう一度新しいパイプラインから始めます。
$group
私はゆっくりと、これを打破ますので、ステージでは、理解することがより困難な段階の一つです。
次のコードから始めます。
// Define a struct to hold grouped data by year:
#[derive(Debug, Deserialize)]
struct YearSummary {
_id: i32,
#[serde(default)]
movie_count: i64,
#[serde(default)]
movie_titles: Vec<String>,
}
// Some movies have "year" values ending with 'è'.
// This stage will filter them out:
let stage_filter_valid_years = doc! {
"$match": {
"year": {
"$type": "number",
}
}
};
/*
* Group movies by year, producing 'year-summary' documents that look like:
* {
* '_id': 1917,
* }
*/
let stage_group_year = doc! {
"$group": {
"_id": "$year",
}
};
let pipeline = vec![stage_filter_valid_years, stage_group_year];
// Loop through the 'year-summary' documents:
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the yearly summaries and print their debug representation:
while let Some(result) = results.next().await {
let doc: YearSummary = bson::from_document(result?)?;
println!("* {:?}", doc);
}
ではmovies
、コレクション、年間のいくつかは、「E」の文字が含まれています。このデータベースには、いくつかの厄介な値が含まれています。この場合、ドキュメントはほんの一握りであり、それらを削除するだけでよいと思うので、数値ではない$match
ドキュメントを除外するステージを追加しましたyear
。
このコードを実行すると、次のように表示されます。
* YearSummary { _id: 1959, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1980, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1977, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1933, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1998, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1922, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1948, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1965, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1950, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1968, movie_count: 0, movie_titles: [] }
...
各行は、集約パイプラインから発行されたドキュメントです。しかし、あなたはもう映画のドキュメントを見ていません。$group
指定によるステージ基入力ドキュメント_id
発現および出力それぞれ一意のための1つのドキュメント_id
値。この場合、式はです$year
。これは、year
フィールドの一意の値ごとに1つのドキュメントが発行されることを意味します。放出される各ドキュメントには、グループ化されたドキュメントのデータを集約して生成された値も含まれる可能性があります(通常は含まれます)。現在、YearSummary文書は、デフォルト値を使用しているmovie_count
とmovie_titles
。それを修正しましょう。
ステージ定義を次のように変更します。
let stage_group_year = doc! {
"$group": {
"_id": "$year",
// Count the number of movies in the group:
"movie_count": { "$sum": 1 },
}
};
これmovie_count
により1
、グループ内のすべてのドキュメントに追加した結果を含むフィールドが追加されます。つまり、グループ内の映画ドキュメントの数をカウントします。ここでコードを実行すると、次のように表示されます。
* YearSummary { _id: 2005, movie_count: 758, movie_titles: [] }
* YearSummary { _id: 1999, movie_count: 542, movie_titles: [] }
* YearSummary { _id: 1943, movie_count: 36, movie_titles: [] }
* YearSummary { _id: 1926, movie_count: 9, movie_titles: [] }
* YearSummary { _id: 1935, movie_count: 40, movie_titles: [] }
* YearSummary { _id: 1966, movie_count: 116, movie_titles: [] }
* YearSummary { _id: 1971, movie_count: 116, movie_titles: [] }
* YearSummary { _id: 1952, movie_count: 58, movie_titles: [] }
* YearSummary { _id: 2013, movie_count: 1221, movie_titles: [] }
* YearSummary { _id: 1912, movie_count: 2, movie_titles: [] }
...
のように、グループからのデータを要約できるアキュムレータ演算子がいくつかあり $sum
ます。放出されたドキュメント内のすべての映画タイトルの配列を作成したい場合は"movie_titles": { "$push": "$title" },
、$group
ステージに追加できます。その場合、次のYearSummary
ようなインスタンスが得られます。
* YearSummary { _id: 1986, movie_count: 206, movie_titles: ["Defense of the Realm", "F/X", "Mala Noche", "Witch from Nepal", ... ]}
次のステージを追加して、結果を並べ替えます。
let stage_sort_year_ascending = doc! {
"$sort": {"_id": 1}
};
let pipeline = vec! [
stage_filter_valid_years, // Match numeric years
stage_group_year,
stage_sort_year_ascending, // Sort by year (which is the unique _id field)
]
$match
ステージはパイプラインの開始に$sort
追加され、は終了に追加されることに注意してください。一般的なルールとして、パイプラインの早い段階でドキュメントを除外して、後の段階で処理するドキュメントが少なくなるようにする必要があります。また、パイプラインがコレクションに割り当てられた適切なインデックスを利用できる可能性が高くなります。
このクイックスタートシリーズのすべてのサンプルコードは、GitHubにあります。
を使用した集計$group
は、データに関する興味深いことを発見するための優れた方法です。この例では、毎年作られる映画の数を示していますが、各国の映画に関する情報を見たり、さまざまな俳優が作った映画を見たりすることも興味深いでしょう。
ドキュメントをフィルタリング、グループ化、および他のコレクションと結合するための集約パイプラインを構築する方法を学習しました。$limit
パイプラインの開始時にステージを配置すると、開発をスピードアップするのに役立つ可能性があることを学んだことを願っています(ただし、本番環境に移行する前に削除する必要があります)。また、フィルタリング式をパイプラインの最後ではなく最初に配置するなど、いくつかの基本的な最適化のヒントも学びました。
あなたを介して行ってきたように、あなたはおそらくがあることに気づいただろう トン異なるの ステージタイプ、 演算子、および アキュムレータ事業者は。アグリゲーションパイプラインのさまざまなコンポーネントの使用方法を学ぶことは、開発者としてMongoDBを効果的に使用することを学ぶ上で重要です。
アグリゲーションパイプラインを操作するのが大好きです。パイプラインで何ができるかにいつも驚いています。
リンク: https://www.mongodb.com/developer/quickstart/rust-quickstart-aggregation/
1638714180
MongoDBの集約パイプライン は、その最も強力な機能の1つです。これらを使用すると、MongoDBデータベース内のデータの集計、変換、結合などの操作を実行する一連のステージに分割された式を記述できます。これにより、MongoDBデータベース内のドキュメントとコレクション全体で計算と分析を行うことができます。
このクイックスタートは、一連のRust投稿の2番目です。私の最初の投稿であるRustでの基本的なMongoDB操作から始めることを強くお勧め します。ここでは、ここで使用するサンプルデータを含む無料のMongoDBAtlasデータベースクラスターを正しくセットアップする方法を説明 します。それを読んで戻ってきてください。待ちます。これがないと、このクイックスタートガイドのコードを実行するためにデータベースを正しく設定できません。
要約すると、次のものが必要です。
Rustの最新バージョン。1.49を使用しましたが、最近のバージョンであれば問題なく動作するはずです。
選択したコードエディタ。私はお勧めVSコードで錆アナライザ拡張。
sample_mflix
データセットを含むMongoDBクラスター。これを設定する手順 は、このシリーズの最初のブログ投稿にあります。
MongoDBの集約パイプラインは非常に強力であるため、最初は少し圧倒されるように思われるかもしれません。このため、ゆっくりと始めます。まず、私はあなたがすでにのMongoDBので達成できることを行動複製パイプラインアップ構築する方法を紹介しますfind()
方法を、代わりに集約パイプラインを使用して$match
、$sort
と$limit
段階。次に、で実行できる範囲を超えるクエリを作成する方法を示し、別のコレクションからの関連ドキュメントを含めるためにfind
使用$lookup
する方法を示します。最後に、$group
ドキュメントをグループ化して新しいドキュメントの概要を作成する方法を示して、「アグリゲーション」を「アグリゲーションパイプライン」に入れます。
このクイックスタートシリーズのすべてのサンプルコードは、GitHubにあります。行き詰まったらチェックすることをお勧めしますが、そうでない場合は、チュートリアルに従って自分でコードを書く価値があります。
この投稿のすべてのパイプラインは、sample_mflix データベースのmovies
コレクションに対して実行され ます。これには、次のようなドキュメントが含まれています(同等のRust構造体よりも少し読みやすいため、Pythonでどのように表示されるかを示しています)。
{
'_id': ObjectId('573a1392f29313caabcdb497'),
'awards': {'nominations': 7,
'text': 'Won 1 Oscar. Another 2 wins & 7 nominations.',
'wins': 3},
'cast': ['Janet Gaynor', 'Fredric March', 'Adolphe Menjou', 'May Robson'],
'countries': ['USA'],
'directors': ['William A. Wellman', 'Jack Conway'],
'fullplot': 'Esther Blodgett is just another starry-eyed farm kid trying to '
'break into the movies. Waitressing at a Hollywood party, she '
'catches the eye of alcoholic star Norman Maine, is given a test, '
'and is caught up in the Hollywood glamor machine (ruthlessly '
'satirized). She and her idol Norman marry; but his career '
'abruptly dwindles to nothing',
'genres': ['Drama'],
'imdb': {'id': 29606, 'rating': 7.7, 'votes': 5005},
'languages': ['English'],
'lastupdated': '2015-09-01 00:55:54.333000000',
'plot': 'A young woman comes to Hollywood with dreams of stardom, but '
'achieves them only with the help of an alcoholic leading man whose '
'best days are behind him.',
'poster': 'https://m.media-amazon.com/images/M/MV5BMmE5ODI0NzMtYjc5Yy00MzMzLTk5OTQtN2Q3MzgwOTllMTY3XkEyXkFqcGdeQXVyNjc0MzMzNjA@._V1_SY1000_SX677_AL_.jpg',
'rated': 'NOT RATED',
'released': datetime.datetime(1937, 4, 27, 0, 0),
'runtime': 111,
'title': 'A Star Is Born',
'tomatoes': {'critic': {'meter': 100, 'numReviews': 11, 'rating': 7.4},
'dvd': datetime.datetime(2004, 11, 16, 0, 0),
'fresh': 11,
'lastUpdated': datetime.datetime(2015, 8, 26, 18, 58, 34),
'production': 'Image Entertainment Inc.',
'rotten': 0,
'viewer': {'meter': 79, 'numReviews': 2526, 'rating': 3.6},
'website': 'http://www.vcientertainment.com/Film-Categories?product_id=73'},
'type': 'movie',
'writers': ['Dorothy Parker (screen play)',
'Alan Campbell (screen play)',
'Robert Carson (screen play)',
'William A. Wellman (from a story by)',
'Robert Carson (from a story by)'],
'year': 1937}
そこ多くのデータがありますが、私は上の中心にすることがあります_id
、title
、year
、およびcast
フィールド。
集約パイプラインは、コレクションのaggregate() メソッドを使用してmongodbモジュールによって実行されます 。
の最初の引数aggregate()
は、実行される一連のパイプラインステージです。クエリと同様に、集計パイプラインの各ステージは BSONドキュメントです。これらdoc!
は、前の投稿で紹介したマクロを使用して作成することがよくあります。
集約パイプラインは、コレクション内のすべてのデータを処理します。パイプラインの各ステージは通過するドキュメントに適用され、あるステージから放出されたドキュメントはすべて、残りのステージがなくなるまで次のステージへの入力として渡されます。この時点で、パイプラインの最後のステージから発行されたドキュメントは、への呼び出しと同様の方法で、カーソルとしてクライアントプログラムに返されますfind()
。
などの個々のステージは、$match
特定の基準に一致するドキュメントのみを通過させるためのフィルターとして機能できます。他のステージのようなタイプの、$project
、$addFields
、とは$lookup
彼らがパイプラインを通過すると、個々の文書の内容を変更します。最後に、などの特定のステージタイプは、$group
渡されたドキュメント全体に基づいて、まったく新しいドキュメントのセットを作成します。これらのステージはいずれも、MongoDB自体に格納されているデータを変更しません。プログラムに戻す前にデータを変更するだけです。$ outのように、パイプラインの結果をMongoDBに保存できるステージがありますが、このクイックスタートでは取り上げません。
前回の投稿で使用したのと同じ環境で作業していると想定します。そのため、ファイルに依存関係としてmongodbクレートが既に構成されているCargo.toml
必要があり.env
、MONGODB_URI
環境変数を含むファイルが必要です。
まず、以下をRustコードに貼り付けます。
// Load the MongoDB connection string from an environment variable:
let client_uri =
env::var("MONGODB_URI").expect("You must set the MONGODB_URI environment var!");
// An extra line of code to work around a DNS issue on Windows:
let options =
ClientOptions::parse_with_resolver_config(&client_uri, ResolverConfig::cloudflare())
.await?;
let client = mongodb::Client::with_options(options)?;
// Get the 'movies' collection from the 'sample_mflix' database:
let movies = client.database("sample_mflix").collection("movies");
上記のコードは、データベース内のコレクションを指す、とCollection
呼ばれるインスタンスを提供します。movie_collectionmovies
パイプラインを作成し、それをで実行してaggregate
から、ループして結果の各映画の詳細を出力するコードを次に示します。プログラムに貼り付けます。
// Usually implemented outside your main function:
#[derive(Deserialize)]
struct MovieSummary {
title: String,
cast: Vec<String>,
year: i32,
}
impl fmt::Display for MovieSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}, {}, {}",
self.title,
self.cast.get(0).unwrap_or(&"- no cast -".to_owned()),
self.year
)
}
}
// Inside main():
let pipeline = vec![
doc! {
// filter on movie title:
"$match": {
"title": "A Star Is Born"
}
},
doc! {
// sort by year, ascending:
"$sort": {
"year": 1
}
},
];
// Look up "A Star is Born" in ascending year order:
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the results, convert each of them to a MovieSummary, and then print out.
while let Some(result) = results.next().await {
// Use serde to deserialize into the MovieSummary struct:
let doc: MovieSummary = bson::from_document(result?)?;
println!("* {}", doc);
}
このパイプラインには2つの段階があります。1つ目は$ matchステージで、これはfind()
。を使用してコレクションをクエリするのと似ています。読み取り操作クエリに基づいて、ステージを通過するドキュメントをフィルタリングします。これはパイプラインの最初のステージであるため、その入力はmovie
コレクション内のすべてのドキュメントです。$match
ステージのクエリtitle
は入力ドキュメントのフィールドでフィルタリングされるため、このステージから出力されるドキュメントは「スター誕生」というタイトルのみになります。
2番目のステージは$ sort ステージです。映画「アリー/スター誕生」のドキュメントのみがこのステージに渡されるため、結果として「アリー/スター誕生」と呼ばれるすべての映画が年のフィールドで並べ替えられ、最も古い映画が最初になります。
Aggregate()を呼び出すと、結果のドキュメントを指すカーソルが返されます。カーソルはストリームトレイトを実装します。メソッドを提供するStreamExtをインポートしている限り、カーソルは他のストリームと同じようにループできますnext()
。上記のコードは、返されたすべてのドキュメントをループし、タイトル、cast
配列の最初の俳優、映画が制作された年で構成される短い要約を出力します。
上記のコードを実行すると、次のようになります。
* A Star Is Born, Janet Gaynor, 1937
* A Star Is Born, Judy Garland, 1954
* A Star Is Born, Barbra Streisand, 1976
上記の例のように、集約パイプライン全体を単一のデータ構造として構築することは可能ですが、必ずしも良い考えではありません。パイプラインは長く複雑になる可能性があります。このため、パイプラインの各ステージを個別の変数として構築し、最後に次のようにステージをパイプラインに結合することをお勧めします。
// Match title = "A Star Is Born":
let stage_match_title = doc! {
"$match": {
"title": "A Star Is Born"
}
};
// Sort by year, ascending:
let stage_sort_year_ascending = doc! {
"$sort": { "year": 1 }
};
// Now the pipeline is easier to read:
let pipeline = vec![stage_match_title, stage_sort_year_ascending];
映画コレクションから「アリー/スター誕生」の最新作を入手したいと想像してみてください。
これは、次の順序で実行される3つの段階と考えることができます。
「アリー/スター誕生」の映画ドキュメントを入手してください。
年の降順で並べ替えます。
最初のドキュメントを除くすべてを破棄します。
最初の段階はすでに上記と同じstage_match_title
です。2番目のステージはと同じstage_sort_year_ascending
ですが、値がに1
変更されてい-1
ます。3番目のステージは$ limitステージです。
変更され、新しい、このようなコードになります。
// Sort by year, descending:
let stage_sort_year_descending = doc! {
"$sort": {
"year": -1
}
};
// Limit to 1 document:
let stage_limit_1 = doc! { "$limit": 1 };
let pipeline = vec![stage_match_title, stage_sort_year_descending, stage_limit_1];
上記の変更を行ってコードを実行すると、次の行が表示されます。
* A Star Is Born, Barbra Streisand, 1976
ちょっと待って!レディー・ガガとブラッドリー・クーパーとの素晴らしい作品のドキュメントがないのはなぜですか?
今学んだスキルを使って、コレクションの最新の日付を見つけてみませんか?それはあなたにあなたの答えを与えるでしょう!
これで、集計パイプラインを使用してコレクションのコンテンツをフィルタリング、並べ替え、制限する方法がわかりました。しかし、これらはあなたがすでに行うことができる単なる操作ですfind()
!なぜこれらの複雑で新しい集約パイプラインを使用したいのですか?
読んでください、私の友人、そして私はあなたにMongoDBアグリゲーションパイプラインの真の力をお見せします。
sample_mflix
データベースに隠れている汚い秘密があります。movies
コレクションだけでなく、と呼ばれるコレクションもありますcomments
。comments
コレクション内のドキュメントは次のようになります。
{
'_id': ObjectId('5a9427648b0beebeb69579d3'),
'movie_id': ObjectId('573a1390f29313caabcd4217'),
'date': datetime.datetime(1983, 4, 27, 20, 39, 15),
'email': 'cameron_duran@fakegmail.com',
'name': 'Cameron Duran',
'text': 'Quasi dicta culpa asperiores quaerat perferendis neque. Est animi '
'pariatur impedit itaque exercitationem.'}
映画へのコメントです。なぜ人々がこれらの映画にラテン語のコメントを書いているのかわかりませんが、それで行きましょう。2番目のフィールドは、コレクション内のドキュメントmovie_id,
の_id
値に対応しますmovies
。
だから、映画に関連したコメントです!
MongoDBを使用すると、映画をクエリしたり、リレーショナルデータベースのJOINなどの関連コメントを埋め込んだりできますか? はい、そうです— $ lookupステージで。
別のコレクションから関連ドキュメントを取得し、それらをプライマリコレクションのドキュメントに埋め込む方法を説明します。
まず、MovieSummary
構造体の定義を変更してcomments
、related_comments
BSONフィールドからロードされたフィールドを持つようにします。ドキュメントにComment
含まれるデータのサブセットを含む構造体を定義しcomments
ます。
#[derive(Deserialize)]
struct MovieSummary {
title: String,
cast: Vec<String>,
year: i32,
#[serde(default, rename = "related_comments")]
comments: Vec<Comment>,
}
#[derive(Debug, Deserialize)]
struct Comment {
email: String,
name: String,
text: String,
}
次に、新しいパイプラインを最初から作成し、次の手順から始めます。
// Look up related documents in the 'comments' collection:
let stage_lookup_comments = doc! {
"$lookup": {
"from": "comments",
"localField": "_id",
"foreignField": "movie_id",
"as": "related_comments",
}
};
// Limit to the first 5 documents:
let stage_limit_5 = doc! { "$limit": 5 };
let pipeline = vec![
stage_lookup_comments,
stage_limit_5,
];
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the results and print a summary and the comments:
while let Some(result) = results.next().await {
let doc: MovieSummary = bson::from_document(result?)?;
println!("* {}, comments={:?}", doc, doc.comments);
}
私が呼んstage_lookup_comments
だ$lookup
ステージはステージです。この$lookup
ステージcomments
では、同じムービーIDを持つコレクションからドキュメントを検索します。一致するコメントはrelated_comments
、という名前のBSONフィールドに配列としてリストされ、配列値には、この映画の_id
値がmovie_id
。であるすべてのコメントが含まれます。
$limit
圧倒されることなく適度な量の出力があることを確認するために、ステージを追加しました。
次に、コードを実行します。
上記のパイプラインの実行がかなり遅いことに気付くかもしれません。これには2つの理由があります。
23.5kの映画ドキュメントと50kのコメントがあります。
comments
コレクションに欠落しているインデックスがあります。インデックスについて教えるために、意図的に欠落しています。インデックスの問題を修正する方法については、ここでは説明しません。これについては、このシリーズの後半の投稿で、インデックスに焦点を当てて説明します。代わりに、開発中に低速の集約パイプラインを操作するための秘訣を紹介します。
パイプラインを作成してテストしている間、遅いパイプラインでの作業は苦痛です。 ただし、パイプライン
$limit
の開始時に一時的なステージを配置すると、クエリが高速になります(ただし、データセット全体を実行していないため、結果が異なる場合があります)。このパイプラインを書いていたとき、私はの最初の段階を持っていました
{ "$limit": 1000 }
。パイプラインの作成が終了したら、最初のステージをコメントアウトして、パイプラインがコレクション全体で実行されるようにすることができます。 最初のステージを削除することを忘れないでください。そうしないと、間違った結果が得られます。
上記の集約パイプラインは、5つの映画ドキュメントの要約を印刷します。私はあなたの映画の要約のほとんどまたはすべてがこれで終わることを期待しています:comments=[]
。
運が良ければ、配列にいくつかのドキュメントがあるかもしれませんが、ほとんどの映画にはコメントがないため、それはありそうにありません。次に、コメントが3つ以上ある映画のみに一致するようにいくつかのステージを追加する方法を示します。
理想的に$match
は、related_comments
フィールドの長さを取得し、それを式と照合する単一のステージを追加できるはず{ "$gt": 2 }
です。この場合、実際には2つのステップです。
フィールドcomment_count
の長さを含むフィールド(これを呼びます)を追加しrelated_comments
ます。
の値がcomment_count
2より大きい場合に一致します。
2つのステージのコードは次のとおりです。
// Calculate the number of comments for each movie:
let stage_add_comment_count = doc! {
"$addFields": {
"comment_count": {
"$size": "$related_comments"
}
}
};
// Match movie documents with more than 2 comments:
let stage_match_with_comments = doc! {
"$match": {
"comment_count": {
"$gt": 2
}
}
};
2つのステージは、$lookup
ステージの後と$limit
5つのステージの前にあります。
let pipeline = vec![
stage_lookup_comments,
stage_add_comment_count,
stage_match_with_comments,
limit_5,
]
ここにいる間、このコードの出力をクリーンアップして、コメントを少し適切にフォーマットします。
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the results and print a summary and the comments:
while let Some(result) = results.next().await {
let doc: MovieSummary = bson::from_document(result?)?;
println!("* {}", doc);
if doc.comments.len() > 0 {
// Print a max of 5 comments per movie:
for comment in doc.comments.iter().take(5) {
println!(
" - {} <{}>: {}",
comment.name,
comment.email,
comment.text.chars().take(60).collect::<String>(),
);
}
} else {
println!(" - No comments");
}
}
今、あなたがこのコードを実行するとき、あなたはもっとこのようなものが表示されます。
* Midnight, Claudette Colbert, 1939
- Sansa Stark <sansa_stark@fakegmail.com>: Error ex culpa dignissimos assumenda voluptates vel. Qui inventore
- Theon Greyjoy <theon_greyjoy@fakegmail.com>: Animi dolor minima culpa sequi voluptate. Possimus necessitatibu
- Donna Smith <donna_smith@fakegmail.com>: Et esse nulla ducimus tempore aliquid. Suscipit iste dignissimos v
ゲーム・オブ・スローンズのサンサ・スタークが彼女のラテン語を本当に知っているのを見るのは良いことですよね?
これで、パイプラインでルックアップを操作する方法を示しました。$group
ステージを使用して実際の集計を行う方法を示します。
$group
もう一度新しいパイプラインから始めます。
$group
私はゆっくりと、これを打破ますので、ステージでは、理解することがより困難な段階の一つです。
次のコードから始めます。
// Define a struct to hold grouped data by year:
#[derive(Debug, Deserialize)]
struct YearSummary {
_id: i32,
#[serde(default)]
movie_count: i64,
#[serde(default)]
movie_titles: Vec<String>,
}
// Some movies have "year" values ending with 'è'.
// This stage will filter them out:
let stage_filter_valid_years = doc! {
"$match": {
"year": {
"$type": "number",
}
}
};
/*
* Group movies by year, producing 'year-summary' documents that look like:
* {
* '_id': 1917,
* }
*/
let stage_group_year = doc! {
"$group": {
"_id": "$year",
}
};
let pipeline = vec![stage_filter_valid_years, stage_group_year];
// Loop through the 'year-summary' documents:
let mut results = movies.aggregate(pipeline, None).await?;
// Loop through the yearly summaries and print their debug representation:
while let Some(result) = results.next().await {
let doc: YearSummary = bson::from_document(result?)?;
println!("* {:?}", doc);
}
ではmovies
、コレクション、年間のいくつかは、「E」の文字が含まれています。このデータベースには、いくつかの厄介な値が含まれています。この場合、ドキュメントはほんの一握りであり、それらを削除するだけでよいと思うので、数値ではない$match
ドキュメントを除外するステージを追加しましたyear
。
このコードを実行すると、次のように表示されます。
* YearSummary { _id: 1959, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1980, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1977, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1933, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1998, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1922, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1948, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1965, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1950, movie_count: 0, movie_titles: [] }
* YearSummary { _id: 1968, movie_count: 0, movie_titles: [] }
...
各行は、集約パイプラインから発行されたドキュメントです。しかし、あなたはもう映画のドキュメントを見ていません。$group
指定によるステージ基入力ドキュメント_id
発現および出力それぞれ一意のための1つのドキュメント_id
値。この場合、式はです$year
。これは、year
フィールドの一意の値ごとに1つのドキュメントが発行されることを意味します。放出される各ドキュメントには、グループ化されたドキュメントのデータを集約して生成された値も含まれる可能性があります(通常は含まれます)。現在、YearSummary文書は、デフォルト値を使用しているmovie_count
とmovie_titles
。それを修正しましょう。
ステージ定義を次のように変更します。
let stage_group_year = doc! {
"$group": {
"_id": "$year",
// Count the number of movies in the group:
"movie_count": { "$sum": 1 },
}
};
これmovie_count
により1
、グループ内のすべてのドキュメントに追加した結果を含むフィールドが追加されます。つまり、グループ内の映画ドキュメントの数をカウントします。ここでコードを実行すると、次のように表示されます。
* YearSummary { _id: 2005, movie_count: 758, movie_titles: [] }
* YearSummary { _id: 1999, movie_count: 542, movie_titles: [] }
* YearSummary { _id: 1943, movie_count: 36, movie_titles: [] }
* YearSummary { _id: 1926, movie_count: 9, movie_titles: [] }
* YearSummary { _id: 1935, movie_count: 40, movie_titles: [] }
* YearSummary { _id: 1966, movie_count: 116, movie_titles: [] }
* YearSummary { _id: 1971, movie_count: 116, movie_titles: [] }
* YearSummary { _id: 1952, movie_count: 58, movie_titles: [] }
* YearSummary { _id: 2013, movie_count: 1221, movie_titles: [] }
* YearSummary { _id: 1912, movie_count: 2, movie_titles: [] }
...
のように、グループからのデータを要約できるアキュムレータ演算子がいくつかあり $sum
ます。放出されたドキュメント内のすべての映画タイトルの配列を作成したい場合は"movie_titles": { "$push": "$title" },
、$group
ステージに追加できます。その場合、次のYearSummary
ようなインスタンスが得られます。
* YearSummary { _id: 1986, movie_count: 206, movie_titles: ["Defense of the Realm", "F/X", "Mala Noche", "Witch from Nepal", ... ]}
次のステージを追加して、結果を並べ替えます。
let stage_sort_year_ascending = doc! {
"$sort": {"_id": 1}
};
let pipeline = vec! [
stage_filter_valid_years, // Match numeric years
stage_group_year,
stage_sort_year_ascending, // Sort by year (which is the unique _id field)
]
$match
ステージはパイプラインの開始に$sort
追加され、は終了に追加されることに注意してください。一般的なルールとして、パイプラインの早い段階でドキュメントを除外して、後の段階で処理するドキュメントが少なくなるようにする必要があります。また、パイプラインがコレクションに割り当てられた適切なインデックスを利用できる可能性が高くなります。
このクイックスタートシリーズのすべてのサンプルコードは、GitHubにあります。
を使用した集計$group
は、データに関する興味深いことを発見するための優れた方法です。この例では、毎年作られる映画の数を示していますが、各国の映画に関する情報を見たり、さまざまな俳優が作った映画を見たりすることも興味深いでしょう。
ドキュメントをフィルタリング、グループ化、および他のコレクションと結合するための集約パイプラインを構築する方法を学習しました。$limit
パイプラインの開始時にステージを配置すると、開発をスピードアップするのに役立つ可能性があることを学んだことを願っています(ただし、本番環境に移行する前に削除する必要があります)。また、フィルタリング式をパイプラインの最後ではなく最初に配置するなど、いくつかの基本的な最適化のヒントも学びました。
あなたを介して行ってきたように、あなたはおそらくがあることに気づいただろう トン異なるの ステージタイプ、 演算子、および アキュムレータ事業者は。アグリゲーションパイプラインのさまざまなコンポーネントの使用方法を学ぶことは、開発者としてMongoDBを効果的に使用することを学ぶ上で重要です。
アグリゲーションパイプラインを操作するのが大好きです。パイプラインで何ができるかにいつも驚いています。
リンク: https://www.mongodb.com/developer/quickstart/rust-quickstart-aggregation/