🌊

OpenSearchの.NETクライアントがGAになったので使ってみる

2022/10/30に公開

先に結論

背景

Elastic社とAWSの間で「いろいろあった」のは記憶に新しいと思います。[2]
https://www.publickey1.jp/blog/21/awselasticelasticsearchkibanaaws.html

その件には深入りしませんが、Amazon OpenSearch Service上でElasticsearchを利用している一開発者としては大いに面倒を被ることになりました。C#含め各言語のElasticsearchのクライアントライブラリからは、v7.14以降はAmazon OpenSearch Service(旧称Elasticsearch Service)上のデータベースに接続できなくなりました。 例外が発生します。[3]

以来、v7.13.xより上げてしまわないよう苦労する羽目になりました。そんな中OpenSearch側も、独自にクライアントライブラリの用意を進めています。
https://opensearch.org/docs/latest/clients/index/

Python・JavaScript・Goあたりの公開は早かったものの、.NETが出てこなくて頭を抱えていたところですが、先日公開され救われました。

NuGetパッケージ対照表

このような対照で理解できます。[4]

個人的な感想ですが、NESTって何のことかぱっとわかりづらく、ググラビリティも低いので、名前の整理は良くなった気がします。

Elasicsearch OpenSearch 説明
Elasticsearch.Net OpenSearch.Net 基盤のライブラリ。低級な操作を提供。
NEST OpenSearch.Client 高級な操作APIを提供。
NEST.JsonNetSerializer OpenSearch.Client.JsonNetSerializer JSONシリアライザを Newtonsoft.Json に変更できる。

筆者の環境

本記事での以降全てにおいて言えますが、2022年現在は (新しめの)Elasticsearch側のドキュメントや参考資料がほぼそのまま通用しますs/Elastics/OpenS/g で読み替えていけば良いです。・・・ということで記事が終わってしまいますが、一応なぞっていきます。OpenSearchの第三者情報がほとんど現状皆無に近いのはもちろん、Elasticsearch.Netすらかなり乏しいので、書けばなんでも価値はあるのではと思っています。

低級APIでの検索

参考: https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/elasticsearch-net-getting-started.html

クライアントの作り方の例です。

using OpenSearch.Net;

var settings = new ConnectionConfiguration(new Uri("https://xxxxx.com/yyyyy"));
var lowLevelClient = new OpenSearchLowLevelClient(settings);

検索の例です。クエリはJSONですが、C# 10以前におけるC#最大の難点の1つが、文字列リテラルでJSONが書きづらいことです。これはRaw string literalsの登場でまもなく解決を見ます。以下では古来からの書き方にしています。

var searchResponse = lowLevelClient.Search<StringResponse>("products", @"
{
    ""from"": 0,
    ""size"": 10,
    ""query"": {
        ""bool"": {
            ""must"": [            
                { ""match"": { ""name"": ""みかん"" } },
                { ""match"": { ""area"": ""和歌山"" } },
            ],
        }
    },
    ""sort"": [ { ""arrival_date_time"": ""desc"" } ]
";

bool successful = searchResponse.Success;
string responseJson = searchResponse.Body;

JSON文字列が書きづらいことの代案として行われることがあるのが、匿名クラスによる方法です。以下示します。これはこれで気を付けることがあります。

  • bool のような予約語との衝突がある。@bool で回避。
  • 匿名型は1つ1つオンリーワンなので、配列は new object[] にしないといけない
var searchResponse = lowLevelClient.Search<StringResponse>("products", PostData.Serializable(new
{
    from = 0,
    size = 10,
    query = new
    {
        @bool = new
        {
            must = new object[]
            {
                new { match = new { name = "みかん" } },
                new { match = new { area = "和歌山" } },
            },
        }
    },
    sort = new object[] { new { arrival_date_time = "desc" } }
}));

bool successful = searchResponse.Success;
string responseJson = searchResponse.Body;

高級APIでの検索

高級な方はOpenSearch.Client を使っていきます。前述の通りElasticsearchにおけるNESTに相当し、今のところほとんど変わりません。

私見としては、前述のようにJSONが書きづらいことが高級APIの動機の1つでもあったように思うのですが、Raw string literalsの導入以降はややそれが薄れるところは出てくるかもしれません。

クライアントの作成例です。

using OpenSearch.Client;

var connectionSettings = new ConnectionSettings(new Uri("https://xxxxx.com/yyyyy"));
var client = new OpenSearchClient(connectionSettings);

モデルを定義します。

using OpenSearch.Client;

[OpenSearchType]
public record Product
{
    [Number(Name = "id")]
    public long Id { get; set; }
    
    [Text(Name = "name")]
    public string? Name { get; set; }
    
    [Text(Name = "area")]
    public string? Area { get; set; }
    
    [Date(Name = "arrival_date_time")]
    public DateTime ArrivalDateTime { get; set; }
}

検索の例です。

var result = client.Search<Product>(s => s
    .Index("products")
    .Size(10)
    .Sort(sd => 
        sd.Field(f => 
	    f.Field(field => field.ArrivalDateTime).Order(SortOrder.Descending)))
    .Source(sf => 
        sf.Includes(f => 
	    f.Fields(new[]{"name", "arrival_date_time"})))
    .Query(q =>
        +q.Term(field => field.Name, "りんご") &&
        +q.Term(field => field.Area, "長野"));
	
var models = result.Hits.Select(h => h.Source).ToArray();

JSONシリアライザの変更

参考:https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/custom-serialization.html

OpenSearch.Client が内部で使用するJSONシリアライザは、Utf8Jsonを独自にカスタマイズしたものです。高速性を狙ってのものとのことですが、過去との互換性のためやルーズな処理を期待したい等で、長年デファクトスタンダードであった Newtonsoft.Json を使用したいこともあると思います。もしその必要があれば以下のようにします。

using OpenSearch.Client;
using OpenSearch.Client.JsonNetSerializer;

var pool = new SingleNodeConnectionPool(new Uri("https://xxxxx.com/yyyyy"));
var connectionSettings =
    new ConnectionSettings(pool, sourceSerializer: JsonNetSerializer.Default);
var client = new ElasticClient(connectionSettings);

ちなみに、Utf8Jsonを内蔵しているというのはもちろんフォーク元のNESTの仕様からであり、そちらも現状は同様なのですが、次期v8では System.Text.Json(STJ) への刷新が予定されています。
https://github.com/elastic/elasticsearch-net/issues/5941#:~:text=the upgrade process.-,System.Text.Json,-Currently%2C the high

かいつまんで適当な訳を示します。

  • 高級クライアントではUtf8Jsonを使用しており、それは Json.NET より高パフォーマンスなため。
  • それは良い価値をもたらしたけど、だんだんメンテが辛くなってきた。Utf8Json自体が現在はアーカイブ状態。
  • 最近STJが.NETに導入され、最初は機能不足ばかりだったもののだんだん穴が埋まってきて行けそうになった。v8では完全移行するよ。

今のところ OpenSearch.Client 側ではSTJに対応しようという動きは見えません。

JSONシリアライザの挙動に手を加えたいという場合、私も一時挑戦したのですが正直なかなかカスタムUtf8Jsonに対しては難しく感じまして[5][6]、情報も豊富なNewtonsoft.Jsonで差し替えてしまうのは、よほどシリアライズ自体の負荷が課題にならない限りはコスパの良い選択と考えます。Elasticsearch側はまもなくSTJになるということで、パフォーマンスとカスタマイズ性の両面を併せ持つことになりますね。OpenSearchではまだそれは見込めないようで、必要なら低級APIを使うのも手かもしれません。

脚注
  1. 個人的に検索しか使っていないので、確かめたのは検索のみです。 ↩︎

  2. 互いの言い分: Elastic / AWS ↩︎

  3. 参考: https://github.com/elastic/elasticsearch-net/issues/5950 ↩︎

  4. これ以外にもパッケージがありますが私の経験がなく不明です。 ↩︎

  5. 差し替えようとしてうまく動かなかったカスタムシリアライザ: https://gist.github.com/shimat/c24fc0f7fe0f26487dfa2b95422c5c51 ↩︎

  6. 素の状態ではなく「Elasticsearchカスタムの」Utf8Jsonに対する感想、と繰り返しておきます。 ↩︎

Discussion