ブログ

JavaScript で Elasticsearch を正しく使う方法、パート II

本番環境でのベストプラクティスと、コーディングエラーを減らすためにServerless環境でElasticsearch Node.jsクライアントを実行する方法についてご覧ください。

これは、JavaScript での Elasticsearch シリーズの第 2 部です。最初の部分では、環境を正しくセットアップし、Node.js クライアントを構成し、データをインデックスして検索する方法を学びました。この第 2 部では、実稼働のベスト プラクティスを実装し、サーバーレス環境で Elasticsearch Node.jsクライアントを実行する方法を学習します。

以下を確認します:

ここで 例付きのソースコードを確認できます

制作のベストプラクティス

Elasticsearchにおけるエラー処理。

Node.js の Elasticsearch クライアントの便利な機能は、Elasticsearch で発生する可能性のあるエラーのオブジェクトを公開し、さまざまな方法で検証して処理できることです。

すべてを表示するには、次のコマンドを実行します。

const { errors } = require('@elastic/elasticsearch')
console.log(errors)

検索の例に戻り、起こりうるエラーのいくつかを処理してみましょう。

app.get("/search/lexic", async (req, res) => {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

      if (error.body.error.type === "parsing_exception") {
        errorMessage = "Query malformed, make sure mappings are set correctly";
      }

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error: errorMessage,
      });
    }

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});

ResponseError 特に、回答が4xxまたは5xx場合に発生します。これは、要求が正しくないか、サーバーが利用できないことを意味します。

このタイプのエラーは、テキストタイプのフィールドで用語クエリを実行するなど、間違ったクエリを生成することでテストできます。

デフォルトエラー:

 {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}

カスタマイズされたエラー:

{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}

各タイプのエラーを特定の方法でキャプチャして処理することもできます。たとえば、 TimeoutErrorに再試行ロジックを追加できます。

app.get("/search/semantic", async (req, res) => {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});

テスト

テストはアプリの安定性を保証する上で重要です。Elasticsearch から分離された方法でコードをテストするには、クラスターを作成するときにライブラリelasticsearch-js-mock を使用できます。

このライブラリを使用すると、実際のクライアントと非常によく似たクライアントをインスタンス化できますが、クライアントの HTTP レイヤーのみをモックのレイヤーに置き換え、残りは元のレイヤーと同じにすることで、構成に応答します。

自動テスト用に、モック ライブラリとAVAをインストールします。

npm install @elastic/elasticsearch-mock

npm install --save-dev ava

テストを実行するためにpackage.jsonファイルを構成します。次のようになっていることを確認してください:

"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}

それでは、 test.jsファイルを作成し、モッククライアントをインストールしましょう。

const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})

次に、セマンティック検索のモックを追加します。

function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () => {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}

これで、コードのテストを作成し、Elasticsearch 部分が常に同じ結果を返すことを確認できるようになりました。

import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) => {
  const indexName = "vet-visits";
  const query = "Which pets had nail trimming?";

  createSemanticSearchMock(query, indexName);

  async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
    try {
      const result = await esClient.search({
        index: indexName,
        body: {
          query: {
            semantic: {
              field: "semantic_field",
              query: q,
            },
          },
        },
      });

      return {
        success: true,
        results: result.hits.hits,
      };
    } catch (error) {
      if (error instanceof errors.TimeoutError) {
        return {
          success: false,
          results: null,
          error: error.body.error.reason,
        };
      }

      return {
        success: false,
        results: null,
        error: error.message,
      };
    }
  }

  const result = await performSemanticSearch(esClient, query, indexName);

  t.true(result.success, "The search must be successful");
  t.true(Array.isArray(result.results), "The results must be an array");

  if (result.results.length > 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});

テストを実行してみましょう。

npm run test

完了です!これからは、外部要因ではなくコードに100%重点を置いてアプリをテストできます。

サーバーレス環境

Elastic Serverlessでクライアントを実行する方法

クラウドまたはオンプレミスでの Elasticsearch の実行について説明しましたが、Node.js クライアントはElastic Cloud Serverlessへの接続もサポートしています。

Elastic Cloud Serverless を使用すると、Elastic がインフラストラクチャを内部で処理するためインフラストラクチャについて心配する必要がないプロジェクトを作成でき、インデックスを作成するデータとそのデータにアクセスする期間のみを考慮すれば済みます。

使用の観点から見ると、Serverless はコンピューティングとストレージを切り離し、検索インデックス作成の両方に自動スケーリング機能を提供します。これにより、実際に必要なリソースのみを増やすことができます。

クライアントは、Serverless に接続するために次の調整を行います。

  • スニッフィングをオフにし、スニッフィング関連のオプションを無視します

  • 最初のノードを除いて、config で渡されたすべてのノードを無視し、ノードのフィルタリングと選択のオプションも無視します。

  • 圧縮と `TLSv1_2_method` を有効にします(Elastic Cloud 用に構成した場合と同じ)

  • すべてのリクエストに `elastic-api-version` HTTP ヘッダーを追加します

  • デフォルトでは `WeightedConnectionPool` ではなく `CloudConnectionPool` を使用します

  • 標準の MIME タイプを優先して、ベンダーの `content-type` および `accept` ヘッダーをオフにします。

サーバーレス プロジェクトを接続するには、パラメーター serverMode: serverless を使用する必要があります。

const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});

Function-as-a-Service環境でクライアントを実行する方法

この例では Node.js サーバーを使用しましたが、AWS lambda、GCP Run などの機能を備えた Function-as-a-Service 環境を使用して接続することもできます。

'use strict'

const { Client } = require('@elastic/elasticsearch')

const client = new Client({
  // client initialisation
})

exports.handler = async function (event, context) {
  // use the client
}

もう 1 つの例は、同じくサーバーレスである Vercel などのサービスに接続することです。これを実行する方法の完全な例を確認できますが、検索エンドポイントの最も重要な部分は次のようになります。

const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);

このエンドポイントは /api フォルダーにあり、サーバー側から実行されるため、クライアントは検索用語に対応する「テキスト」パラメータのみを制御できます。

Function-as-a-Service を使用する意味は、24 時間 365 日稼働するサーバーとは異なり、関数は関数を実行するマシンのみを起動し、関数が終了するとマシンは休止モードになり、消費するリソースが少なくなることです。

この構成は、アプリケーションがあまり多くのリクエストを受け取らない場合には便利ですが、そうでない場合はコストが高くなる可能性があります。また、関数のライフサイクルと実行時間 (場合によっては数秒しかないこともあります) も考慮する必要があります。

まとめ

この記事では、実稼働環境で非常に重要なエラーの処理方法を学びました。また、Elasticsearch サービスをモックしながらアプリケーションをテストする方法についても説明しました。これにより、クラスターの状態に関係なく信頼性の高いテストが提供され、コードに集中できるようになります。

最後に、Elastic Cloud Serverless と Vercel アプリケーションの両方をプロビジョニングして、完全にサーバーレスなスタックを立ち上げる方法を示しました。

関連記事

JavaScript、Mastra、Elasticsearch を使用したエージェント型 RAG アシスタントの構築

JD Armada

JavaScript で Elasticsearch を正しく使う方法、パート 1

Jeffrey Rengifo

最先端の検索体験を構築する準備はできましたか?

十分に高度な検索は 1 人の努力だけでは実現できません。Elasticsearch は、データ サイエンティスト、ML オペレーター、エンジニアなど、あなたと同じように検索に情熱を傾ける多くの人々によって支えられています。ぜひつながり、協力して、希望する結果が得られる魔法の検索エクスペリエンスを構築しましょう。

はじめましょう