LoginSignup
2
0

More than 1 year has passed since last update.

Elasticsearch.net を使う

Last updated at Posted at 2022-07-07

はじめに

ElasticsearchのC#クライアントであるElasticsearch.netを使ってElasticsearchを操作するやり方の基本操作についてまとめました。
web上にElasticsearch.netについての情報が少なく、いろいろ調査・試行錯誤してわかったことです。

前提

  • Elasticsearch: 7.17.3
  • Elasticsearch.net: 7.17.2
  • .Net Framework 4.7
  • visual studio pro 2019 / win10
  • データモデルは不定形
  • 内容を全部完全に理解したわけでなく、予想してる内容もたくさん入ってます。

前置き

C#クライアントには2種類ある

公式ページにも書かれているとおり、
Elasticsearchのver7時点では、C#クライアントには以下の2種類あります。

  • Elasticsearch.net : low level client ; データ型不定形
  • NEST : high level client ; データ型固定形

上記違いがあるものの、NESTでもobject型を使ってデータ型不定形を扱えるようです。
Elasticsearchのver8からはC#クライアントが更新?されているようなので、ver8はこの記事の対象外です。

Elasticsearch.net

公式ページにほぼすべて書いてあります。
私の場合、Elasticsearch.netを使うことになりましたが、可能ならNESTを使った方がいいと思います。クエリの記述の場合など、NESTの方が簡単に書けると思います。

返り値

データ型不定形のレスポンスは、StringResponse型を使いました。
応答のbodyが文字列型ということのようです。他にもByteResponse型も用意されているようです。
応答結果はエラーの場合を含めてすべてresponse.Body(string型)で得られ、辞書型データ(json)をシリアライズした形になっています。
応答結果詳細の取り出しは、JObjectなどでパースして辞書型に変換してからキーを使って取り出しました。

using Newtonsoft.Json.Linq;

var response = client.Search<StringResponse>("indexName", ...);
JObject body = JObject.Parse(response.Body);
int count = (int) body["hits"]["total"]["value"];
var jobj = body["hits"]["hits"][0]["_source"];
var dic = jobj.ToObject<Dictionary<string, string>>();
Console.WriteLine(dic["Name"]);

送信データの書式が2種類

クライアントから送信データを渡すときの書き方として主に2種類あり、string型とobject型です。
好きな方を使えます。
データを渡すときに文字列型の場合はそのまま渡し、object型の時はPostData.Serializableを使います。

string型

string data = @"
    ""Tokyo"": ""Tokyo"",
    ""Kanagawa"": ""Yokohama"",
    ""Miyagi"": ""Sendai"",
    ""Fukui"": ""Fukui""
";
var response = client.Index<StringResponse>("indexName", "documentId", data);

object型

var data = new {
    Tokyo = "Tokyo",
    Kanagawa = "Yokohama",
    Miyagi = "Sendai",
    Fukui = "Fukui"
};
var response = client.Index<StringResponse>("indexName", "documentId", PostData.Serializable(data));

表記例

コネクション

公式サイトに書かれていますが、以下で作成します。
細かいオプション設定ができるようですが、今は使いません。

string url = "http://localhost:9200";
var settings = new ConnectionConfiguration(new Uri(url));
var client = new ElasticLowLevelCient(settings);

using宣言

using Elasticsearch.Net;

インデックスの状態確認

インデックス一覧取得 : Cat/Indices

var response = client.Cat.Indices<StringResponse>();

結果はresponse.Bodyに入ってきますが、Elasticsearchに存在するインデックスのみを取得するには一工夫要りそうです。
上記コマンドは単にhttp://localhost:9200/_cats/indicesを実行しているだけなので。

インデックス名を指定してインデックスの存在確認 : Indices.Exists

var response = client.Indices.Exists<StringResponse>("indexName");

結果はresponse.Successresponse.HttpStatusCodeで判定できます。

  • インデックスが存在するとき: response.Success = true
  • インデックスが存在しないとき: response.Success = false

Elasticsearchが起動していないときは、response.Body == ""となるので、これを使って判定しました。

データの登録

まず、用語の話です。
indexという言葉には2種類の意味があって、区別して使う必要があります。

  1. RDBのデータベースに相当するもの。名詞。
  2. データをデータベースに登録すること。動詞。

インデックス作成 : Indices.Create

データ無しでインデックスだけ先に作りたいケースです。
通常はデータ登録すれば自動でインデックスも作成されますので、この作業は不要と思います。
ここではインデックス作成時に、インデックスの設定も同時に行うこととします。
index.mapping.total_fields.limitindex.max_window_resultを設定することにします。
index.mapping.total_fields.limitはfield数の上限で、デフォルトは1,000に設定されています。
index.max_result_windowはクエリ時のデータ数の上限で、デフォルトは10,000です。

int fieldQty = 10_000;
int maxResult = 50_000;
string createIndexSettings = $@"{{
    ""settings"": {{
        ""index"": {{
            ""max_result_window"": {maxResult},
            ""mapping"": {{
                ""total_fields"": {{
                    ""limit"": {filedQty}
                }}
            }}
        }}
    }}
}}";
var response = client.Indices.Create<StringResponse>("indexName", createIndexSettings);

1個データ登録 : Index

個々のデータを登録する際にはindex名とdocument番号を指定します。
documentIdは省略可能で、省略するとElasticsearchが適当に付けてくれます。

var response = client.Index<StringResponse>("indexName", "documentId",
    @"{ 
        ""hamada"": ""my friend"",
        ""satamoto"": ""kaiten"",
        ""yamashita"": ""run fast"",
        ""matsuda"": ""never call me""
    }"
);

送信成功可否は、response.Success(bool型)やresponse.HttpStatusCode(int?型)で判定可能です。
成功時の返り値はそれぞれtrue, 201です。

bulk

大量データをまとめて送るケースです。
bulkの書式として、1行目にindex, idを指定、2行目に登録データを書き、それが1個のドキュメントとして認識されます。
公式ページでは送信データがobject型のケースしか示されていませんが、listやdictionaryを使って動的にデータを組み立てることができます。
以下はListに入ったDictionary型のデータをBulk送信する場合に、Bulkデータフォーマット用の新規リストを作成して、そこにBulk送信データを組み立ててから一気にBulk送信する例です。
Bulkを送信するときには、PostData.MultiJsonを使うようです。

var memlist = new List<Dictionary<string, string>>() {
    new Dictionary<string, string> {
        {"name", "Takashi"},
        {"residential area", "tomi town"},
        {"grade", "elem4"},
        {"sports", "soccer"},
        {"likes", "TV game"}
    },
    new Dictionary<string, string> {
        {"name", "Tomohisa"},
        {"residential area", "wake city"},
        {"grade", "elem2"},
        {"sports", "swimming"},
        {"likes", "train"}
    },
    new Dictionary<string, string> {
        {"name", "Shohei"},
        {"residential area", "mago city"},
        {"grade", "elem1"},
        {"sports", "judo"},
        {"likes", "insects"},
    }
};
var oneBulkShot = new List<object>();
for ( int i = 0; i < 3; i++) {
    var info = new { index = new { _index = "people", _id = i.ToString() }};
    oneBulkShot.Add(info);
    oneBulkShot.Add(memlist[i]);
}
var response = client.Bulk<StringResponse>(PostData.MultiJson(oneBulkShot));

クエリ

matchを使う

とりあえず以下でクエリ可能です。
条件にマッチするデータ検索には、基本的にはSearchを使います。
全インデックスを対象とする場合は、"*"と書きます。

// object型
var response = client.Search<StringResponse>("*", PostData.Serializable(new
    {   query = new { match = new {
        Kanagawa = "Yokohama"
    }}}
));

// string型
var response = client.Search<StringResponse>("*",
    @"{
        ""query"": {
            ""match"": {
                ""Kanagawa"": ""Yokohama""
            }
        }
    }"
);

結果はresponse.Bodyに入ります。

using Newtonsoft.Json.Linq;

JObject body = JObject.Parse(response.Body);

検索結果が無い場合は body["hits"]["hits"][0]["_source"]は取り出せないので、例外となります。
マッチ件数は、パースしてから(int)body["hits"]["total"]["value"]で取得できます。
ただし検索結果が10,001件以上ある場合は、hits.total.valueは正確なマッチ件数を返してくれません。後述のCount項を参照してください。


matchは要素解析されるようで全文一致ではありませんので、意図しないデータを取得するケースがあります。
その場合はmatch_phraseやtermを使うといいようです。

match_phrase の場合

var response = client.Search<StringResponse>("*",
    @"{
        ""query"": {
            ""match_phrase"": {
                ""Kanagawa"": ""Yokohama""
            }
        }
    }"
);

termを使う

完全一致で検索するケースです。
文字列を扱う場合はtext,keywordの2種類の型があり、データ登録時にマッピングで指定できます。
両者の違いについてはこちらなどを参照ください。
データ登録時に自分でマッピングを指定しない場合(Elasticsearchが自動マッピングしてくれる)、通常文字列はtext型とkeyword型の2タイプでクエリができます。
keyword型を使う場合はkeyとなる語の後に.keywordを付けますが、text型を使う場合はkeyをそのまま使います。

// text型 ... matchを使う
var response = client.Search<StringResponse>("*",
    @"{
        ""query"": {
            ""match"": {
                ""Kanagawa"": ""Yokohama""
            }
        }
    }"
);

// keyword型 ... termを使う
var response = client.Search<StringResponse>("*",
    @"{
        ""query"": {
            ""term"": {
                ""Kanagawa.keyword"": ""Yokohama""
            }
        }
    }"
);

boolクエリを使う

これらの識別子の直下はobject{}ではなく、配列[]となります。

  • must: and
  • should: or
  • must not: not
var response = client.Search<StringResponse>("*",
    @"{ 
        ""query"": {
            ""bool"": {
                ""must"": [
                    { ""term"": {
                        ""grade.keyword"": ""elem2""
                    }},
                    { ""term"": {
                        ""likes.keyword"": ""train""
                    }}
                ]
            }
        }
    }"
);

boolクエリは、string型では書けましたが、NEST無しでobject型で書く方法はわかりませんでした。
NESTを使う方法は公式サイトを参照願います。
object型は object initializerというようなので、このフレーズで検索すれば情報が見つけやすいです。

インデックスのヒット件数を得る : Count

通常Searchなどでクエリをかけると、hits.total.valueにヒット件数が入りますが、ヒット件数が10,000件を超える場合にはhits.total.valueには10,000としか表示されずに困ることがあります。
この場合、countAPIを使って、以下のように取り出せました。
このCount関数はクエリを書かないと値を返してくれません。

var response = client.Count<StringResponse>("indexName", @"{
    ""query"": { ""match_all"": {} }
}");

データ数は、response.Bodyから"count"キーで取得できます。

using Newtonsoft.Json.Linq;

JObject jobj = JObject.Parse(response.Body);
int count = (int) jobj["count"];

refreshAPI : Indices.Refresh

データの格納直後にそのデータを読み出す処理をしてましたが、格納直後の読み出しそのままではうまく格納結果を得られないことがあります。
その場合データ登録時?にrefreshの設定(デフォルトではfalse)が使えそうでしたが、結局refreshAPIを使いました。登録(Bulk)直後にrefreshAPIを呼ぶ、という具合です。インデックスごとにrefreshを実行できます。1

var response = client.Indices.Refresh<StringResponse>("indexName");

万能関数 : DoRequest

すべてのケースに対応できる関数のようです。
公式ページの説明通りの書式が書けます。
別記事に書きました。

終わりに

間違い等ございましたらご指摘願いたいです。
次はNESTを使ってまとめたいです。

  1. Elasticsearchに実際に書き込むテストなんかで「indexへの反映に時間がかかって困る〜〜」って時

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0