2021/07/12

Elasticsearchサーバーをローカルで立てて、Rustを使ってアクセスしてみる

rustelasticsearch

以前書いたPythonでの記事のRust版になります。

インストール

Cargo.tomlに依存関係を追加

  • elasticsearch = "7.12.1-alpha.1"です。
  • tokioとserdeは一緒に入れないと不便です。
[dependencies]
elasticsearch = "7.12.1-alpha.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.8.0", features = ["full"] }
dotenv = "0.15.0"

elastic clientの取得

fn get_client() -> Elasticsearch {
    let cluster_address = env::var("ELASTICSEARCH_URL")
        .unwrap_or("http://localhost:9200".into());
    let url = Url::parse(&cluster_address).unwrap();
    let conn_pool = SingleNodeConnectionPool::new(url);
    let builder = TransportBuilder::new(conn_pool);
    let transport = builder.build()
        .expect("could not build http transport to elasticsearch...");
    Elasticsearch::new(transport)
}

indexの作成

#[tokio::test]
async fn test_create_index_if_not_exists() -> Result<(), Box<dyn Error>> {
    dotenv().ok();
    let client = get_client();
    let index_name = "faq_items";
    let exists = client
        .indices()
        .exists(IndicesExistsParts::Index(&[index_name]))
        .send()
        .await?;
    if exists.status_code() != StatusCode::NOT_FOUND {
        println!("index found!");
        return Ok(())
    }
    let response = client
        .indices()
        .create(IndicesCreateParts::Index(index_name))
        .body(json!(
                {
                    "settings": {
                        "analysis": {
                            "char_filter": {
                                "normalize": {
                                    "type": "icu_normalizer",
                                    "name": "nfkc",
                                    "mode": "compose"
                                }
                            },
                            "tokenizer": {
                                "ja_kuromoji_tokenizer": {
                                    "mode": "search",
                                    "type": "kuromoji_tokenizer",
                                },
                                "ja_ngram_tokenizer": {
                                    "type": "ngram",
                                    "min_gram": 2,
                                    "max_gram": 2,
                                    "token_chars": [
                                        "letter",
                                        "digit"
                                    ],
                                },
                            },
                            "analyzer": {
                                "ja_kuromoji_index_analyzer": {
                                    "type": "custom",
                                    "char_filter": [
                                        "normalize",
                                        "html_strip"
                                    ],
                                    "tokenizer": "ja_kuromoji_tokenizer",
                                    "filter": [
                                        "kuromoji_baseform",
                                        "kuromoji_part_of_speech",
                                        "cjk_width",
                                        "ja_stop",
                                        "kuromoji_stemmer",
                                        "lowercase"
                                    ]
                                },
                                "ja_kuromoji_search_analyzer": {
                                    "type": "custom",
                                    "char_filter": [
                                        "normalize",
                                        "html_strip"
                                    ],
                                    "tokenizer": "ja_kuromoji_tokenizer",
                                    "filter": [
                                        "kuromoji_baseform",
                                        "kuromoji_part_of_speech",
                                        "cjk_width",
                                        "ja_stop",
                                        "kuromoji_stemmer",
                                        "lowercase"
                                    ]
                                },
                                "ja_ngram_index_analyzer": {
                                    "type": "custom",
                                    "char_filter": [
                                        "normalize",
                                        "html_strip"
                                    ],
                                    "tokenizer": "ja_ngram_tokenizer",
                                    "filter": [
                                        "lowercase"
                                    ]
                                },
                                "ja_ngram_search_analyzer": {
                                    "type": "custom",
                                    "char_filter": [
                                        "normalize",
                                        "html_strip"
                                    ],
                                    "tokenizer": "ja_ngram_tokenizer",
                                    "filter": [
                                        "lowercase"
                                    ]
                                }
                            }
                        }
                    },
                    "mappings": {
                        "properties": {
                            "id": {
                                "type": "long"
                            },
                            "title": {
                                "type": "text",
                                "search_analyzer": "ja_kuromoji_search_analyzer",
                                "analyzer": "ja_kuromoji_index_analyzer"
                            },
                            "body": {
                                "type": "text",
                                "search_analyzer": "ja_kuromoji_search_analyzer",
                                "analyzer": "ja_kuromoji_index_analyzer",
                                "fields": {
                                    "ngram": {
                                        "type": "text",
                                        "search_analyzer": "ja_ngram_search_analyzer",
                                        "analyzer": "ja_ngram_index_analyzer"
                                    }
                                }
                            }
                        }
                    }
                }
            ))
        .send()
        .await?;
    assert!(response.status_code().is_success(), "response status should be 2**");
    Ok(())
}

documentをindex

  • indexに含める構造体を定義しておきます
  • jsonにserialize/deserializeできるようにしておくと便利です
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct FaqItem {
    pub id: i32,
    pub title: String,
    pub body: String,
}
  • FaqItemを先程作ったindexに追加(または更新)します
#[tokio::test]
async fn test_create_or_update_document() -> Result<(), Box<dyn Error>> {
    dotenv().ok();
    let client = get_client();
    let index_name = "faq_items";
    let faq_item = FaqItem {
        id: 1,
        title: "システムメンテナンスのお知らせ".into(),
        body: "2021年3月12日23:00よりシステムメンテナンスを実施します。".into(),
    };
    let response = client
        .index(IndexParts::IndexId(index_name, faq_item.id.to_string().as_str()))
        .body(faq_item)
        .send()
        .await?;
    assert!(response.status_code().is_success(), "response status should be 2**");
    Ok(())
}

documentを検索

#[tokio::test]
async fn test_search_document() -> Result<(), Box<dyn Error>> {
    dotenv().ok();
    let client = get_client();
    let index_name = "faq_items";
    let response = client
        .search(SearchParts::Index(&[index_name]))
        .from(0)
        .size(10)
        .body(json!({
          "query": {
            "bool": {
              "must": [
                {
                  "multi_match": {
                    "query": "システム",
                    "fields": [
                      "body.ngram^1"
                    ],
                    "type": "phrase"
                  }
                }
              ],
              "should": [
                {
                  "multi_match": {
                    "query": "システム",
                    "fields": [
                      "body^1"
                    ],
                    "type": "phrase"
                  }
                }
              ]
            }
          }
        }))
        .send()
        .await?;
    assert!(response.status_code().is_success(), "response status should be 2**");
    let response_body = response.json::<Value>().await?;
    println!("{:?}", response_body);
    let faq_items: Vec<FaqItem> = response_body["hits"]["hits"]
        .as_array()
        .unwrap()
        .iter()
        .map(|hit| serde_json::from_value(hit["_source"].clone()).unwrap())
        .collect();
    for faq_item in faq_items {
        println!("{:?}", faq_item)
    }
    Ok(())
}

感想

elasticsearchのrust client・・とても便利でした。

rustのライブラリは入れたあと、使うまで戸惑うことも多いのですが、比較的簡単に操作できました。