JavaScriptスクリプトによる複雑なクエリ書き換えの実装

ビジネス要件として以下のようなニーズが存在します:

ゲートウェイでマルチクラスタ検索をどのようにサポートするか?実現したいのは、検索リクエストとして lp:9200/index1/_search を入力した場合、 このインデックスが3つのクラスタに存在し、クラスタ間で検索を実行したい。つまり、ゲートウェイが lp:9200/cluster01:index1,cluster02:index1,cluster03:index1/_search に書き換えられるか? インデックスは100以上あり、名前は必ずしも'app'とは限らず、複数のインデックスを一度に検索する可能性もある。

極限ゲートウェイに搭載されているフィルター content_regex_replace は正規表現による文字置換は実現可能ですが、 この要件はパラメータ付きの変数置換が必要であり、少し複雑なため、単純な正規表現置換では実現できません。 他に実現方法はありますか?

スクリプトフィルターの活用

もちろん可能です。上記の要件は、基本的にインデックス名 index1 をマッチングした後、 cluster01:index1,cluster02:index1,cluster03:index1 に置換するだけで実現できます。

答えはカスタムスクリプトの使用です。より複雑なビジネスロジックでも問題なく実現でき、 1行のスクリプトで実現できない場合は2行で対応可能です。

極限ゲートウェイが提供する JavaScript フィルターを活用すれば、この機能を柔軟に実装できます。 詳細は以下で説明します。

スクリプトの定義

まず、スクリプトファイルを作成し、ゲートウェイデータディレクトリの scripts サブディレクトリに配置します。

gateway ✗ tree data
data
└── gateway
    └── nodes
        └── c9bpg0ai4h931o4ngs3g
            ├── kvdb
            ├── queue
            ├── scripts
            │   └── search_path_modifier.js
            └── stats

スクリプトの内容は以下の通りです:

function transformRequest(context) {
    // 元のリクエストパスを取得
    var originalPath = context.Get("_ctx.request.path");
    
    // パスからインデックス名を抽出
    var pathPattern = /\/?(.*?)\/_search/;
    var matches = originalPath.match(pathPattern);
    var indexNames = [];
    
    if(matches && matches.length > 1) {
        indexNames = matches[1].split(",");
    }
    
    // 新しいインデックス名を生成
    var modifiedIndices = [];
    var clusterList = ["cluster01", "cluster02", "cluster03"];
    
    if(indexNames.length > 0) {
        for(var i=0; i<indexNames.length; i++){
            if(indexNames[i].trim().length > 0) {
                for(var j=0; j<clusterList.length; j++){
                    modifiedIndices.push(clusterList[j] + ":" + indexNames[i]);
                }
            }
        }
    }

    // 新しいパスを構築して設定
    if (modifiedIndices.length > 0){
        var newPath = "/" + modifiedIndices.join(",") + "/_search";
        context.Put("_ctx.request.path", newPath);
    }
}

通常の JavaScript と同様に、特定の関数 transformRequest を定義してリクエスト内のコンテキスト情報を処理します。 _ctx.request.path はゲートウェイのビルトインコンテキスト変数で、リクエストのパスを取得するために使用します。 スクリプト内では context.Get("_ctx.request.path") でアクセスします。

中では JavaScript の正規表現マッチングと文字列処理を使用し、文字列の連結を行って新しいパス newPath 変数を生成し、 最後に context.Put("_ctx.request.path", newPath) でゲートウェイのリクエストパス情報を更新し、 クエリ条件内のパラメータ置換を実現します。

ゲートウェイのビルトインコンテキスト変数の一覧については、Request Context を参照してください。

ゲートウェイの定義

次に、ゲートウェイ設定を作成し、そのスクリプトを呼び出すために javascript フィルターを使用します。

entry:
  - name: elasticsearch_gateway
    enabled: true
    router: cluster_router
    max_concurrency: 10000
    network:
      binding: 0.0.0.0:8000

flow:
  - name: search_flow
    filter:
      - dump:
          context:
            - _ctx.request.path
      - javascript:
          file: search_path_modifier.js
      - dump:
          context:
            - _ctx.request.path
      - elasticsearch:
          elasticsearch: production

router:
  - name: cluster_router
    default_flow: search_flow

elasticsearch:
- name: production
  enabled: true
  schema: http
  hosts:
    - 192.168.3.188:9206

上記の例では、javascript フィルターを使用し、ロードするスクリプトファイルを search_path_modifier.js として指定しています。 また、スクリプト実行前後のパス情報を出力するために2つの dump フィルターを使用し、 最後に elasticsearch フィルターを使用してリクエストを Elasticsearch に転送してクエリを実行します。

ゲートウェイの起動

ゲートウェイを起動してテストします。

gateway ✗ ./bin/gateway
   ___   _   _____  __  __    __  _
  / _ \ /_\ /__   \/__\/ / /\ \ \/_\ /\_/\
 / /_\///_\\  / /\/_\  \ \/  \/ //_\\\_ _/
/ /_\\/  _  \/ / //__   \  /\  /  _  \/ \
\____/\_/ \_/\/  \__/    \/  \/\_/ \_/\_/

[GATEWAY] 軽量で強力かつ高性能なelasticsearchゲートウェイ
[GATEWAY] 1.0.0_SNAPSHOT, 2022-04-18 07:11:09, 2023-12-31 10:10:10, 8062c4bc6e57a3fefcce71c0628d2d4141e46953
[04-19 11:41:29] [INF] [app.go:174] ゲートウェイを初期化中
[04-19 11:41:29] [INF] [app.go:175] 設定ファイルを使用: /Users/medcl/go/src/infini.sh/gateway/gateway.yml
[04-19 11:41:29] [INF] [instance.go:72] ワークスペース: /Users/medcl/go/src/infini.sh/gateway/data/gateway/nodes/c9bpg0ai4h931o4ngs3g
[04-19 11:41:29] [INF] [app.go:283] ゲートウェイは稼働中
[04-19 11:41:30] [INF] [api.go:262] APIは以下でリッスン: http://0.0.0.0:2900
[04-19 11:41:30] [INF] [entry.go:312] エントリ [elasticsearch_gateway] は以下でリッスン: http://0.0.0.0:8000
[04-19 11:41:30] [INF] [module.go:116] すべてのモジュールが起動しました
[04-19 11:41:30] [INF] [actions.go:349] elasticsearch [production] が利用可能です

テストの実行

以下のクエリを実行して結果を確認します。

curl localhost:8000/abc,efg/_search

ゲートウェイの dump フィルターが出力するデバッグ情報を確認できます。

---- DUMPING CONTEXT ----
_ctx.request.path  :  /abc,efg/_search
---- DUMPING CONTEXT ----
_ctx.request.path  :  /cluster01:abc,cluster02:abc,cluster03:abc,cluster01:efg,cluster02:efg,cluster03:efg/_search

クエリ条件が要件通りに書き換えられています。素晴らしい!

DSLクエリ文の書き換え

さて、これまでクエリのインデックス部分のみを修正してきましたが、クエリリクエストの DSL も書き換え可能でしょうか? もちろん可能です。以下の例をご覧ください:

function modifyQueryDSL(context) {
    // 元のDSLクエリを取得
    var originalDSL = context.Get("_ctx.request.body");
    if (originalDSL && originalDSL.length > 0){
        // JSONオブジェクトに変換
        var queryObject = JSON.parse(originalDSL);
        
        // クエリパラメータを変更
        queryObject.size = 50;  // ヒット数を制限
        queryObject.timeout = "30s";  // タイムアウト設定
        
        // 集計処理を追加
        queryObject.aggregations = {
            "category_stats": {
                "terms": {
                    "field": "category",
                    "size": 5
                },
                "aggregations": {
                    "avg_price": {
                        "avg": {
                            "field": "price"
                        }
                    }
                }
            }
        };
        
        // 変更したクエリを設定
        context.Put("_ctx.request.body", JSON.stringify(queryObject));
    }
}

まずクエリリクエストを取得し、JSONオブジェクトに変換した後、 クエリオブジェクトを自由に変更し、保存して完了です。

テストしてみましょう:

curl -XPOST   localhost:8000/abc,efg/_search -d'{"query":{"match_all":{}}}'

出力:

---- DUMPING CONTEXT ----
_ctx.request.path  :  /abc,efg/_search
_ctx.request.body  :  {"query":{"match_all":{}}}
[04-19 18:14:24] [INF] [reverseproxy.go:255] elasticsearch [production] ホスト: [] => [192.168.3.188:9206]
---- DUMPING CONTEXT ----
_ctx.request.path  :  /abc,efg/_search
_ctx.request.body  :  {"query":{"match_all":{}},"size":50,"timeout":"30s","aggregations":{"category_stats":{"terms":{"field":"category","size":5},"aggregations":{"avg_price":{"avg":{"field":"price"}}}}}}

新しい世界が開けた感じがしませんか?

まとめ

JavaScriptスクリプトフィルターを活用することで、ビジネス要件を満たすための複雑なロジック操作を非常に柔軟に行うことができます。

タグ: javascript Elasticsearch ゲートウェイ クエリ書き換え スクリプト処理

5月24日 04:54 投稿