はじめに
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.Success
やresponse.HttpStatusCode
で判定できます。
- インデックスが存在するとき:
response.Success
= true - インデックスが存在しないとき:
response.Success
= false
Elasticsearchが起動していないときは、response.Body == ""
となるので、これを使って判定しました。
データの登録
まず、用語の話です。
indexという言葉には2種類の意味があって、区別して使う必要があります。
- RDBのデータベースに相当するもの。名詞。
- データをデータベースに登録すること。動詞。
インデックス作成 : Indices.Create
データ無しでインデックスだけ先に作りたいケースです。
通常はデータ登録すれば自動でインデックスも作成されますので、この作業は不要と思います。
ここではインデックス作成時に、インデックスの設定も同時に行うこととします。
index.mapping.total_fields.limit
とindex.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を使ってまとめたいです。