1. オンライン環境の問題
学生グパオは「ランタイムドキュメントを見ながらテストをしていました。agg が avg を要求すると、それが倍であるか長いかに関係なく、データは正確ではありません。運用環境でこの問題を解決するにはどうすればよいですか?」と質問しました。
2. 問題の分類と発生シナリオ
上記の問題は次のように分類できます: Elasticsearch 集計クエリの精度の問題。
日常のデータ処理業務では、Elasticsearchを使用したビッグデータのクエリ、統計、集計などの操作に遭遇することがよくあります。Elasticsearch は実際には優れた検索パフォーマンスを示しますが、平均化 (avg) などの一部の複雑な集計操作では、不正確なデータ精度の問題が発生する可能性があります。
次に、この問題の発生シナリオ、考えられる原因、解決策を詳しく紹介します。
Elasticsearchでは、主にアグリゲーション(集計)操作においてデータ精度の問題が発生します。たとえば、合計 (sum) や平均値 (avg) などの大きな数値演算を行う場合、データ型 (double またはlong) によって精度の問題が発生する可能性があります。これは、Elasticsearch では、集計操作を実行する際のパフォーマンスと効率を向上させるために、「浮動小数点計算」と呼ばれる手法を使用して大きな数値の計算を実行するためであり、この計算方法は、大きな数値を処理するときに精度が低下することがよくあります。
3. 問題の再発を最小限に抑える
この問題を簡単な例で説明してみましょう。Elasticsearch にいくつかの商品データが保存されているので、すべての商品の平均価格を計算したいと考えています。
データとクエリの DSL は次のとおりです (Elasticsearch 8.X 環境で検証)。
データ:
POST /product/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "商品1", "price" : 1234.56 }
{ "index" : { "_id" : "2" } }
{ "name" : "商品2", "price" : 7890.12 }
DSL のクエリ:
GET /product/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
平均価格は (1234.56 + 7890.12) / 2 = 4562.34 であると予想されますが、浮動小数点計算の精度により、返される結果は次の図に示すようにわずかに偏る可能性があります。
4. ソリューションの議論と実装
集計後の上記の精度の問題を解決するにはどうすればよいでしょうか? Elasticsearch の基礎知識と実践経験を組み合わせて、次の 3 つのソリューションを提供します。
解決策 1: 精度を向上させるために、scaled_float 型を使用します。
解決策 2: scripted_metric を使用して精度を向上させます。
オプション 3: ビジネス レベルでコードを記述し、自分で実装します。
次に、上記の 3 つの解決策を 1 つずつ実践し、解釈していきます。
4.1scaled_float型による精度向上
4.1.1scaled_floatとは何ですか?
scaled_float
これは、小数を含む数値を保存するために Elasticsearch によって提供される特別な数値データ型です。
float
およびとは異なりdouble
、scaled_float
は実際にはlong
型ですが、指定されたスケーリング係数を掛けた実際の浮動小数点数を格納する点が異なります。
多くのアプリケーション シナリオでは、価格や評価などの数値を小数点以下で保存する必要があります。float
および はdouble
一般的に使用されるデータ型ですが、いくつかの問題があります。たとえば、保存および並べ替えの際に精度が失われる可能性があり、整数型よりも多くの記憶領域を必要とします。代わりに、 scaled_float
float に実際に 1 を乗算しscaling factor
、結果を として保存します long
。
たとえば、 がscaling factor
100 の場合、数値 12.34 は 1234 として保存されます。クエリを実行して結果を返す場合、Elasticsearch はscaling factor
元の浮動小数点数で除算して返します。
4.1.2scaled_float の利点
精度がより正確になり、制御可能になります
float や double と比較すると、scaled_float は実際に格納された長整数であり、浮動小数点数の精度の問題がないため、格納と並べ替えの精度が高くなります。
よりよい性能
scaled_float は long 型を使用するため、占有する記憶領域が少なくなり、パフォーマンスが向上します。
より柔軟な
必要に応じてスケーリング係数を設定して、精度とパフォーマンスのバランスを取ることができます。より高い精度が必要な場合は、より大きなスケーリング係数を使用できます。プロジェクトでパフォーマンスとストレージ容量を重視する必要がある場合は、より小さいスケーリング係数を使用できます。
4.1.3 Elasticsearchでのscaled_floatの使用
Elasticsearch でscaled_float を使用するには、マッピングでフィールド タイプを定義し、スケーリング係数を指定する必要があります。例えば:
{
"properties": {
"price": {
"type": "scaled_float",
"scaling_factor": 100.0
}
}
}
このマッピングは、スケーリング係数 100 で、price という名前のscaled_float フィールドを定義します。これは、すべての価格が 100 倍されて同じ期間保存されることを意味します。
たとえば、12.34 の価格は 1234 として保存されます。
全体として、scaled_float は、浮動小数点数を格納する必要がある状況でより優れた精度とパフォーマンスを提供する非常に便利なツールです。
4.1.4 序盤の同様の問題を解決するための実戦
この例では、価格が浮動小数点である 2 つの製品があります。
scaled_float を使用する場合は、まずマッピングを設定する必要があります。価格を分単位の精度で保存したい場合は、scaling_factor を 100.0 に設定します。マッピングを定義する手順は次のとおりです。
まず、新しいインデックスを作成し、マッピングを定義します。
PUT /product
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100.0
}
}
}
}
このコマンドは、新しいインデックス製品を作成し、名前 (テキスト型) と価格 (scaled_float 型、scaling_factor 100.0 型) の 2 つのフィールドを定義します。
この場合、バッチ一括挿入データは次のようになります。
POST /product/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "商品1", "price" : 1234.56 }
{ "index" : { "_id" : "2" } }
{ "name" : "商品2", "price" : 7890.12 }
この処理では、price フィールドの値に scaling_factor (この場合は 100.0) が自動的に乗算され、long 型として格納されます。したがって、実際に保存される値は 123456 と 789012 です。
クエリを実行すると、Elasticsearch は自動的に価格を scaling_factor で除算し、元の浮動小数点数を返します。たとえば、次のクエリを実行するとします。
GET /product/_doc/1
返される結果は次のようになります。
{
"_index": "product",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {
"name": "商品1",
"price": 1234.56
}
}
価格は保存時に 100 倍されますが、クエリ時には 100 で除算されるため、表示される価格は 1234.56 のままです。
このようにして、高い精度を維持しながら、より少ない記憶容量とより良いパフォーマンスで価格を保存および照会することができます。
最終的には次のことが達成されます。
GET product/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
以下に示すように、結果の精度は予想どおりです。
4.2 scripted_metric を使用して精度を向上させる
このような状況に直面した場合は、Elasticsearch のもう 1 つの強力な機能であるスクリプト計算 (scripted_metric) を使用して解決できます。
scripted_metric を使用すると、次の DSL のような複雑な集計ロジックをカスタマイズできます。
####务必要删除索引
DELETE product
POST /product/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "商品1", "price" : 1234.56 }
{ "index" : { "_id" : "2" } }
{ "name" : "商品2", "price" : 7890.12 }
GET /product/_search
{
"size": 0,
"aggs": {
"avg_price": {
"scripted_metric": {
"init_script": "state.total = 0.0; state.count = 0",
"map_script": "state.total += params._source.price; state.count++",
"combine_script": "HashMap result = new HashMap(); result.put('total', state.total); result.put('count', state.count); return result",
"reduce_script": """
double total = 0.0; long count = 0;
for (state in states) {
total += state['total'];
count += state['count'];
}
double average = total / count;
DecimalFormat df = new DecimalFormat("#.00");
return df.format(average);
"""
}
}
}
}
Elasticsearch は分散型検索および分析エンジンです。つまり、データは複数のシャードにまたがって保存および処理できます。分散データを処理するために、Elasticsearch は、map-reduce と呼ばれるプログラミング モデルを使用します。このモデルは、マッピング (Map) とリダクション (Reduce) の 2 つのステップに分かれています。init_script、map_script、combine_script、reduce_script はすべて、より複雑な集計を行うためのこのモデルのコンポーネントです。
上記のスクリプトでは、次の 4 つのステップを定義しました。
init_script: 各シャード上の各集約の新しい状態を作成する初期化スクリプト。
map_script: 入力ドキュメントを処理し、その状態をマージ可能な形式に変換するマッピング スクリプト。
combo_script: ノード レベルで各シャードの状態をマージするための結合スクリプト。
Reduce_script: 状態をグローバルにマージするための Reduce スクリプト。
このようにして、より正確な平均を得ることができます。
上記のスクリプトの具体的な意味は次のように説明されます。
init_script: このスクリプトはシャードごとに 1 回実行され、シャードごとに新しい状態を作成します。
上記のスクリプトでは、合計 (合計) とカウンター (カウント) を含む状態オブジェクトを作成します。状態オブジェクトは {total: 0.0, count: 0} に初期化されます。
map_script: このスクリプトはドキュメントごとに 1 回実行されます。
上記のスクリプトでは、各ドキュメントのフィールドを読み取り、値をインクリメントしながらprice
この値をフィールドに追加します。こうすることで、すべてのドキュメントの価格の合計と、処理されたドキュメントの数が含まれます。total
count
total
count
combo_script: このスクリプトはシャードごとに 1 回実行され、各シャードの状態を結合します。
上記のスクリプトでは、合計をreturnにtotal
入れるだけです。マージする状態が多数ある場合は、このスクリプトで前処理が行われる可能性があります。count
HashMap
reduce_script: このスクリプトは、結果がマージされるときに 1 回実行され、すべてのフラグメントの状態が削減されて最終結果が計算されます。
上記のスクリプトでは、すべてのシャードの状態を反復処理し、合計を計算してtotal
、count
平均価格を計算します。DecimalFormat
平均価格を小数点以下 2 桁にフォーマットするために使用される文字列。
簡単に言えば、これは平均を計算する段階的なプロセスです。最初に状態を初期化し、次に各ドキュメントの状態を更新し、次に各シャードで状態をマージし、最後に状態をグローバルにマージして結果を計算します。
最終結果は次の図に示されており、期待された精度が達成されています。
4.3 ビジネスレベルでは、自分でコードを書きます。
アプリケーション レベルでの精度制御: 生データをアプリケーション層に取得し、アプリケーション層で正確な計算を実行します。この方法の利点は、非常に正確な結果が得られることですが、欠点は、大量のデータを処理する必要があり、ネットワーク送信と計算の負担が増大することです。
アプリケーション レベルでデータの精度の問題に対処するには、通常、次の 2 つの手順が必要です。
まず、生データを Elasticsearch から取得する必要があります。
その後、アプリケーション層で正確な計算が実行されます。
以下は、Java でデータ精度を処理する例です。
システム アプリケーションが Java で記述されていると仮定すると、Java の BigDecimal クラスを使用して正確な浮動小数点計算を行うことができます。簡単な例を次に示します。
BigDecimal price1 = new BigDecimal("1234.56");
BigDecimal price2 = new BigDecimal("7890.12");
BigDecimal average = price1.add(price2).divide(new BigDecimal(2), 2, RoundingMode.HALF_UP);
System.out.println(average); // 输出:4562.34
上の例では、最初に 2 つの価格を表す 2 つの BigDecimal オブジェクトを作成しました。次に、add メソッドを呼び出してそれらを合計し、次に、divide メソッドを呼び出して平均を計算します。最後に、RoundingMode.HALF_UP パラメータを使用して丸めモードを制御します。
このアプローチでは、すべてのデータをアプリケーション層で処理する必要があるため、データ量が大きい場合はパフォーマンスの問題が発生する可能性があることに注意してください。データ送信と計算の負担を軽減するには、Elasticsearch でより正確なクエリを使用して必要なデータのみを取得するか、Elasticsearch の集計機能を使用して返されるデータの量を削減する必要がある場合があります。
さらに、処理パフォーマンスを向上させるために並列処理やキャッシュなどのテクノロジーを使用するなど、アプリケーション層で最適化を行う必要がある場合があります。具体的な方法は、アプリケーションの特定の状況とニーズに応じて決定されます。
5. まとめ
一般に、Elasticsearch には集計操作の実行時にデータの精度が不正確になるという問題がある可能性がありますが、scaled_float タイプを使用して精度を向上させ、scripted_metric を使用して精度を向上させ、ビジネス レベルでコードを作成してより正確な結果を得ることで、より正確な結果を取得できます。結果。
同様の問題が発生した場合、実際の状況に応じて最適な解決策を選択する必要があります。一方では精度要件を考慮する必要があり、他方ではクエリのパフォーマンスとリソース消費も考慮する必要があります。ビジネスの実際のニーズに応じて、集計操作の精度を向上させるために、スクリプト計算をタイムリーに使用する必要があります。
推奨読書
より多くの乾物をより短時間で素早く入手できます。
世界中の約 2,000 人以上の Elastic 愛好家と一緒に改善しましょう!
大型モデルの時代、一歩先行く先進乾物を学ぼう!