RustでMongoDBクライアントを実装し、正規表現でクエリする

## 概要

MongoDBのクライアントをRustで実装した際に、正規表現を用いたコレクションの検索がなかなか上手くいかずに結構ハマったので、本記事で共有する。
※MongoDBもRustも初心者なので実はもっと良い方法があるかもしれない

## 必要環境

サーバもクライアントもLinux上でのみ動作確認

* サーバ側
    * MongoDBがインストールされていること
        * 動作確認バージョン : 4.4.4
    * インストールの参考サイト
        * https://docs.mongodb.com/manual/installation/
* クライアント側
    * Rustがインストールされていること
        * 動作確認バージョン
            * rustc : 1.43.1
            * cargo : 1.43.0
    * インストールの参考サイト
        * https://www.rust-lang.org/tools/install

## サーバ側 MongoDBのコレクションの作成

まずは、検索対象のサンプルコレクションをMongoDBに作成

* MongoDBにログイン。ユーザ名はパスワードは各環境で任意

```
$ mongo -u <username> -p <password>
```

* thomasというdb名で、登場キャラクターのコレクション(character)を作成

```
> use thomas
> db.createCollection("character")
```

* ドキュメントの登録

```
> db.character.insertMany([
  { name: "トーマス", description: "いつでも元気" },
  { name: "パーシー", description: "たよりになるね" },
  { name: "ゴードン", description: "とっても強い" },
  { name: "エミリー", description: "しっかり屋さん" },
  { name: "トップハム", description: "とっても厳しい" },
])
```

上のドキュメントの中で、指定した文字列が含まれるものを検索したい。
純粋なMongoDBのコマンドであれば、

* MongoDB コマンドメモとか書き
    * https://qiita.com/svjunic/items/285e9cf20169d70aa1fa

のように``db.character.find({<検索条件>},...)``の書式で検索可能だが、Rustの場合にこれをどう実現するかを記載する。

## クライアント側をRustで実装

### Cargoプロジェクトの概要

あくまでサンプルなので、``Cargo.toml``と``main.rs``のみの最小限構成とする。

```
.
├── Cargo.toml  # プロジェクトの設定ファイル
└── src
    └── main.rs # クライアントを実装するソース
```

```toml:Cargo.toml
[package]
name = "mongo_client"
version = "0.1.0"
edition = "2018"

[dependencies]
mongodb = "1.2.1" # MongoDBのドライバー
tokio = "0.2.25"  # 非同期処理のランタイム
```

### とりあえず正規表現を使わずないクエリを実装

以下のソースは、``thomas.character``のコレクションから、nameフィールドが「トーマス」と完全一致するドキュメントを検索するもの。ここまではWebに載っている情報で比較的（といっても多少はつまづくが…）すんなりと実装ができた。
※本題ではないが、mongodbクレート内のモジュールは基本的には「非同期処理」で動作するようで、これらのモジュールを呼ぶ関数内ではasyncや.awaitをつけなければならないみたい。

```rs:main.rs
use mongodb::Collection;

/// DBのコレクション情報を取得
async fn get_collection() -> Collection {

    use mongodb::Client;

    // MongoDBにアクセスするためのURIを作成
    // username,password等の各引数は環境毎に要設定
    let mongo_uri = format!(
        "mongodb://{}:{}@{}:{}", 
        username,
        password,
        hostname,
        port,
    );

    let client = Client::with_uri_str(&mongo_uri)
                    .await.unwrap();

    // コレクションを返す
    client.database("thomas")
          .collection("character")
}

#[tokio::main]
async fn main() {

    // DBのコレクション情報を取得
    let coll = get_collection().await;

    // mongodbクレートのDocumentモジュールの内容を
    // 簡潔に表現するためのマクロ
    use mongodb::bson::doc;

    // dbにクエリを投げる
    let mut cursor = coll.find(
            doc!{"name": "トーマス"},
            None,
        ).await.unwrap();

    // クエリ結果(cursor)からDocumentを抽出するのに必要
    use tokio::stream::StreamExt;

    // クエリ結果を出力
    while let Some(doc) = cursor.next().await {
        println!("{}", doc.unwrap());
    }
}
```

実行すると、nameが「トーマス」のドキュメントがフィルタリングされて出力

```
$ cargo run
   Compiling mongo_client v0.1.0 (/work/rust/qiita/mongo_client)
    Finished dev [unoptimized + debuginfo] target(s) in 9.08s
     Running `target/debug/mongo_client`
{ _id: ObjectId("607bd3721566974cfd205900"), name: "トーマス", description: "いつでも元気" }
```

ちなみにmain内で使われる関数findはMongoDBのコレクションを検索するもので、第１引数ではその検索条件を指定するが、``doc!``マクロを使わずに、以下のようにMongoDBクレートの構造体Documentを定義して記載することも可能。検索条件の値にString型以外(i32やDatetime等)を指定したい場合はこう書かないと表現出来ないのかもしれない（推測）。

```rs
#[tokio::main]
async fn main() {

    (中略) 

    // doc!マクロを使わずに
    // Documentモジュールでクエリのフィルタを表現
    use mongodb::bson::document::Document;
    use mongodb::bson::Bson;

    // 検索用のフィルタを定義
    let mut filter = Document::new();
    filter.insert("name".to_string(),
                  Bson::String("トーマス".to_string()));

    // dbにクエリを投げる
    let mut cursor = coll.find(
            filter,
            None,
        ).await.unwrap();

    (中略)

}
```

### 部分一致で検索を試みるも…

次は、descriptionフィールドに「とっても」を含むドキュメントを検索してみる。少し調べると、検索する値の前後にスラッシュ(/)を入れることで前方一致、後方一致、部分一致検索ができるとのことで、元ソースのfind関数の引数を色々試してみるが、上手く行かない…。

* 以下だとビルドが通らない

```rs
    // dbにクエリを投げる
    let mut cursor = coll.find(
            doc!{"description": /とっても/ },
            None,
        ).await.unwrap();
```

* 以下だとスラッシュを文字列と見なしてしまい、「とっても」を部分一致で検索できない

```rs
    // dbにクエリを投げる
    let mut cursor = coll.find(
            doc!{"description": "/とっても/" },
            None,
        ).await.unwrap();
```

### 解決策

次のように、**検索値を入れる部分を``{$regex : (正規表現)}``にすることで解決！**

```rs
#[tokio::main]
async fn main() {

    (中略)

    // mongodbクレートのDocumentモジュールの内容を
    // 簡潔に表現するためのマクロ
    use mongodb::bson::doc;

    // dbにクエリを投げる
    let mut cursor = coll.find(
            doc!{"description": {"$regex": "^とっても"}},
            None,
        ).await.unwrap();

    (中略)

```

実行してみると、ちゃんと意図した検索が引っかかる

```
$ cargo run
   Compiling mongo_client v0.1.0 (/work/rust/qiita/mongo_client)
    Finished dev [unoptimized + debuginfo] target(s) in 9.06s
     Running `target/debug/mongo_client`
{ _id: ObjectId("607bd3721566974cfd205902"), name: "ゴードン", description: "とっても強い" }
{ _id: ObjectId("607bd3721566974cfd205904"), name: "トップハム", description: "とっても厳しい" }
```

#### [参考] doc!マクロを使わない場合の正規表現による方法

あくまでご参考だが、マクロを使わないでDocumentモジュールで愚直にフィルタリングする場合は、Documentの第２要素目にRegex型の値を指定すればOK。本記事を書き始めた時はこの方法が最適解だと思っていたが、doc!マクロで同じことができるのに後で気づいた…。

```rs
#[tokio::main]
async fn main() {

    (中略)

    // doc!マクロを使わずに
    // Documentモジュールでクエリのフィルタを表現
    use mongodb::bson::document::Document;
    use mongodb::bson::Regex;

    // 検索用の正規表現を定義
    let regex = Regex {
        pattern: "^とっても".to_string(),
        options: "i".to_string(),
    };

    // 検索用のフィルタを定義
    let mut filter = Document::new();
    filter.insert("description".to_string(), regex);

    // dbにクエリを投げる
    let mut cursor = coll.find(
            filter,
            None,
        ).await.unwrap();

    (中略)

```