JavaScript で Elasticsearch を正しく使う方法、パート 1
JavaScript で本番環境対応の Elasticsearch バックエンドを作成する方法を説明します。 ElasticsearchをJavaScriptで使用して、クライアント/サーバーのベストプラクティスに従ってElasticsearchドキュメントにクエリを実行するために、さまざまな検索エンドポイントを持つサーバーを作成する方法をご覧ください。
これは、JavaScript で Elasticsearch を使用する方法を説明するシリーズの最初の記事です。このシリーズでは、JavaScript 環境で Elasticsearch を使用する方法の基本を学習し、検索アプリを作成するための最も関連性の高い機能とベスト プラクティスを確認します。最後には、JavaScript を使用して Elasticsearch を実行するために必要なすべてのことを理解できるようになります。
この最初の部分では、次の点を確認します。
ここで 例付きのソースコードを確認できます 。
Elasticsearch Node.js クライアントとは何ですか?
Elasticsearch Node.js クライアントは、Elasticsearch API からの HTTP REST 呼び出しを JavaScript に配置する JavaScript ライブラリです。これにより、処理が容易になり、ドキュメントのインデックス作成などのタスクを一括で簡素化するヘルパーが利用できるようになります。
環境
フロントエンド、バックエンド、それともサーバーレス?
JavaScript クライアントを使用して検索アプリを作成するには、Elasticsearch クラスターとクライアントを実行する JavaScript ランタイムという少なくとも 2 つのコンポーネントが必要です。
JavaScript クライアントはすべての Elasticsearch ソリューション (クラウド、オンプレミス、サーバーレス) をサポートしており、クライアントがすべてのバリエーションを内部で処理するため、ソリューション間に大きな違いはありません。そのため、どれを使用するかについて心配する必要はありません。
ただし、JavaScript ランタイムはブラウザから直接ではなく、サーバーから実行する必要があります。

これは、ブラウザから Elasticsearch を呼び出すと、ユーザーがクラスター API キー、ホスト、クエリ自体などの機密情報を取得する可能性があるためです。Elasticsearch では、クラスターをインターネットに直接公開せず、このすべての情報を抽象化する中間層を使用して、ユーザーがパラメータのみを確認できるようにすることを推奨しています。このトピックの詳細については、ここ をご覧ください。
次のようなスキーマを使用することをお勧めします。

この場合、クライアントは検索用語とサーバーの認証キーのみを送信し、サーバーはクエリと Elasticsearch との通信を完全に制御します。
クライアントの接続
まず、次の手順に従って API キーを作成します。
前の例に従って、シンプルな Express サーバーを作成し、Node.JS サーバーからのクライアントを使用してそのサーバーに接続します。
NPM を使用してプロジェクトを初期化し、Elasticsearch クライアントとExpress をインストールします。後者は、Node.js でサーバーを起動するためのライブラリです。Express を使用すると、HTTP 経由でバックエンドと対話できます。
プロジェクトを初期化しましょう:
npm init -y
依存関係をインストールします:
npm install @elastic/elasticsearch express split2 dotenv
詳しく説明しましょう:
@elastic/elasticsearch : 公式Node.jsクライアントです
express : 軽量なNode.jsサーバーを立ち上げてElasticsearchを公開できるようになります
split2 : テキスト行をストリームに分割します。ndjsonファイルを1行ずつ処理するのに便利です
dotenv : .env を使用して環境変数を管理できるようにしますファイル
.envを作成するプロジェクトのルートにあるファイルを作成し、次の行を追加します。
ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"この方法では、 dotenvパッケージを使用してこれらの変数をインポートできます。
server.jsファイルを作成します:
const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
require("dotenv").config(); //environment variables setup
const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;
const PORT = 3000;
const app = express();
app.listen(PORT, () => {
console.log("Server running on port", PORT);
});
app.use(bodyParser.json());
let esClient = new Client({
node: ELASTICSEARCH_ENDPOINT,
auth: { apiKey: ELASTICSEARCH_API_KEY },
});
app.get("/ping", async (req, res) => {
try {
const result = await esClient.info();
res.status(200).json({
success: true,
clusterInfo: result,
});
} catch (error) {
console.error("Error getting Elasticsearch info:", error);
res.status(500).json({
success: false,
clusterInfo: null,
error: error.message,
});
}
});このコードは、ポート 3000 をリッスンし、認証用の API キーを使用して Elasticsearch クラスターに接続する基本的な Express.js サーバーをセットアップします。これには、GET リクエストを介してアクセスすると、Elasticsearch クライアントの.info()メソッドを使用して Elasticsearch クラスターに基本情報を照会する /ping エンドポイントが含まれています。
クエリが成功した場合は、クラスター情報が JSON 形式で返され、それ以外の場合はエラー メッセージが返されます。サーバーは、JSON リクエスト本体を処理するために body-parser ミドルウェアも使用します。
ファイルを実行してサーバーを起動します。
node server.js
答えは次のようになるはずです:
Server running on port 3000それでは、エンドポイント/pingを参照して、Elasticsearch クラスターのステータスを確認しましょう。
curl http://localhost:3000/ping
{
"success": true,
"clusterInfo": {
"name": "instance-0000000000",
"cluster_name": "61b7e19eec204d59855f5e019acd2689",
"cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
"version": {
"number": "9.0.0",
"build_flavor": "default",
"build_type": "docker",
"build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
"build_date": "2025-04-08T15:13:46.049795831Z",
"build_snapshot": false,
"lucene_version": "10.1.0",
"minimum_wire_compatibility_version": "8.18.0",
"minimum_index_compatibility_version": "8.0.0"
},
"tagline": "You Know, for Search"
}
}文書のインデックス作成
接続すると、セマンティック検索用のsemantic_textやフルテキストクエリ用の text などのマッピングを使用してドキュメントのインデックスを作成できます。これら 2 つのフィールド タイプを使用すると、ハイブリッド検索も実行できます。
マッピングを生成し、ドキュメントをアップロードするために、新しいload.jsファイルを作成します。
Elasticsearchクライアント
まずクライアントをインスタンス化して認証する必要があります。
const { Client } = require("@elastic/elasticsearch");
const ELASTICSEARCH_ENDPOINT = "cluster/project_endpoint";
const ELASTICSEARCH_API_KEY = "apiKey";
const esClient = new Client({
node: ELASTICSEARCH_ENDPOINT,
auth: { apiKey: ELASTICSEARCH_API_KEY },
});セマンティックマッピング
動物病院に関するデータを含むインデックスを作成します。飼い主様、ペット様、訪問の詳細に関する情報を保存します。
名前や説明など、全文検索を実行するデータはテキストとして保存されます。動物の種や品種などのカテゴリのデータは、キーワードとして保存されます。
さらに、すべてのフィールドの値を semantic_text フィールドにコピーして、その情報に対してもセマンティック検索を実行できるようにします。
const INDEX_NAME = "vet-visits";
const createMappings = async (indexName, mapping) => {
try {
const body = await esClient.indices.create({
index: indexName,
body: {
mappings: mapping,
},
});
console.log("Index created successfully:", body);
} catch (error) {
console.error("Error creating mapping:", error);
}
};
await createMappings(INDEX_NAME, {
properties: {
owner_name: {
type: "text",
copy_to: "semantic_field",
},
pet_name: {
type: "text",
copy_to: "semantic_field",
},
species: {
type: "keyword",
copy_to: "semantic_field",
},
breed: {
type: "keyword",
copy_to: "semantic_field",
},
vaccination_history: {
type: "keyword",
copy_to: "semantic_field",
},
visit_details: {
type: "text",
copy_to: "semantic_field",
},
semantic_field: {
type: "semantic_text",
},
},
});バルクヘルパー
クライアントのもう 1 つの利点は、一括ヘルパーを使用してインデックスを一括で作成できることです。バルク ヘルパーを使用すると、同時実行、再試行、関数を通過して成功または失敗した各ドキュメントの処理などを簡単に処理できます。
このヘルパーの魅力的な機能は、ストリームを操作できることです。この機能を使用すると、ファイル全体をメモリに保存して Elasticsearch に一度に送信するのではなく、ファイルを 1 行ずつ送信できます。
Elasticsearch にデータをアップロードするには、プロジェクトのルートに data.ndjson というファイルを作成し、以下の情報を追加します (または、ここからデータセットを含むファイルをダウンロードすることもできます)。
{"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."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}バルク ヘルパーがファイル行を Elasticsearch に送信する間、split2 を使用してファイル行をストリーミングします。
const { createReadStream } = require("fs");
const split = require("split2");
const indexData = async (filePath, indexName) => {
try {
console.log(`Indexing data from ${filePath} into ${indexName}...`);
const result = await esClient.helpers.bulk({
datasource: createReadStream(filePath).pipe(split()),
onDocument: () => {
return {
index: { _index: indexName },
};
},
onDrop(doc) {
console.error("Error processing document:", doc);
},
});
console.log("Bulk indexing successful elements:", result.items.length);
} catch (error) {
console.error("Error indexing data:", error);
throw error;
}
};
await indexData("./data.ndjson", INDEX_NAME);上記のコードは.ndjsonを読み取りますファイルを 1 行ずつ読み込み、 helpers.bulkメソッドを使用して各 JSON オブジェクトを指定された Elasticsearch インデックスに一括インデックスします。createReadStreamとsplit2を使用してファイルをストリーミングし、各ドキュメントのインデックス メタデータを設定し、処理に失敗したドキュメントをログに記録します。完了すると、正常にインデックスが作成されたアイテムの数を記録します。
indexData関数を使用する代わりに、Kibana を使用して UI 経由でファイルを直接アップロードし、データ ファイルのアップロード UI を使用することもできます。
ファイルを実行して、ドキュメントを Elasticsearch クラスターにアップロードします。
node load.js
Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0Elasticsearchでのデータ検索
server.jsファイルに戻って、語彙検索、セマンティック検索、ハイブリッド検索を実行するためのさまざまなエンドポイントを作成します。
簡単に言えば、これらのタイプの検索は相互に排他的ではありませんが、回答する必要がある質問の種類によって異なります。
クエリタイプ | 使用事例 | 例題 |
|---|---|---|
語彙クエリ | 質問内の単語または語根は、索引文書に表示される可能性があります。質問とドキュメント間のトークンの類似性。 | 青いスポーツTシャツを探しています。 |
セマンティッククエリ | 質問内の単語は文書には表示されない可能性があります。質問とドキュメント間の概念的な類似性。 | 寒い季節用の服を探しています。 |
ハイブリッド検索 | 質問には語彙や意味の要素が含まれています。質問とドキュメント間のトークンと意味の類似性。 | ビーチでの結婚式用にSサイズのドレスを探しています。 |
質問の語彙部分はタイトルや説明、またはカテゴリ名の一部である可能性が高く、意味部分はそれらの分野に関連する概念です。青はおそらくカテゴリ名または説明の一部であり、ビーチウェディングはそうではないかもしれませんが、意味的にはリネンの衣服に関連している可能性があります。
語彙クエリ (/search/lexic?q=<query_term>)
語彙検索 (フルテキスト検索とも呼ばれる) とは、トークンの類似性に基づいて検索することを意味します。つまり、分析後、検索内のトークンを含むドキュメントが返されます。
語彙検索の実践チュートリアルは、こちらでご覧いただけます。
app.get("/search/lexic", async (req, res) => {
const { q } = req.query;
const INDEX_NAME = "vet-visits";
try {
const result = await esClient.search({
index: INDEX_NAME,
size: 5,
body: {
query: {
multi_match: {
query: q,
fields: ["owner_name", "pet_name", "visit_details"],
},
},
},
});
res.status(200).json({
success: true,
results: result.hits.hits
});
} catch (error) {
console.error("Error performing search:", error);
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});爪切りでテストする
curl http://localhost:3000/search/lexic?q=nail%20trimming答え:
{
"success": true,
"results": [
{
"_index": "vet-visits",
"_id": "-RY6RJYBLe2GoFQ6-9n9",
"_score": 2.7075968,
"_source": {
"pet_name": "Mochi",
"owner_name": "Daniel Kim",
"species": "Rabbit",
"visit_details": "Nail trimming and general health check. No issues.",
"breed": "Mixed",
"vaccination_history": []
}
},
{
"_index": "vet-visits",
"_id": "8BY6RJYBLe2GoFQ6-9n9",
"_score": 2.560356,
"_source": {
"pet_name": "Buddy",
"owner_name": "Alice Johnson",
"species": "Dog",
"visit_details": "Annual check-up and nail trimming. Healthy and active.",
"breed": "Golden Retriever",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Distemper"
]
}
}
]
}セマンティッククエリ (/search/semantic?q=<query_term>)
セマンティック検索は、語彙検索とは異なり、ベクトル検索を通じて検索用語の意味に類似した結果を見つけます。
セマンティック検索の実践チュートリアルは、こちらでご覧いただけます。
app.get("/search/semantic", async (req, res) => {
const { q } = req.query;
const INDEX_NAME = "vet-visits";
try {
const result = await esClient.search({
index: INDEX_NAME,
size: 5,
body: {
query: {
semantic: {
field: "semantic_field",
query: q
},
},
},
});
res.status(200).json({
success: true,
results: result.hits.hits,
});
} catch (error) {
console.error("Error performing search:", error);
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});テスト対象:誰がペディキュアをしましたか?
curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?答え:
{
"success": true,
"results": [
{
"_index": "vet-visits",
"_id": "-RY6RJYBLe2GoFQ6-9n9",
"_score": 4.861466,
"_source": {
"owner_name": "Daniel Kim",
"pet_name": "Mochi",
"species": "Rabbit",
"breed": "Mixed",
"vaccination_history": [],
"visit_details": "Nail trimming and general health check. No issues."
}
},
{
"_index": "vet-visits",
"_id": "8BY6RJYBLe2GoFQ6-9n9",
"_score": 4.7152824,
"_source": {
"pet_name": "Buddy",
"owner_name": "Alice Johnson",
"species": "Dog",
"visit_details": "Annual check-up and nail trimming. Healthy and active.",
"breed": "Golden Retriever",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Distemper"
]
}
},
{
"_index": "vet-visits",
"_id": "9RY6RJYBLe2GoFQ6-9n9",
"_score": 1.6717153,
"_source": {
"pet_name": "Rex",
"owner_name": "Tomás Herrera",
"species": "Dog",
"visit_details": "Follow-up for previous leg strain, improving well.",
"breed": "German Shepherd",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Leptospirosis"
]
}
},
{
"_index": "vet-visits",
"_id": "9xY6RJYBLe2GoFQ6-9n9",
"_score": 1.5600781,
"_source": {
"pet_name": "Simba",
"owner_name": "Leo Martínez",
"species": "Cat",
"visit_details": "Dental cleaning. Minor tartar buildup removed.",
"breed": "Maine Coon",
"vaccination_history": [
"Rabies",
"Feline Panleukopenia"
]
}
},
{
"_index": "vet-visits",
"_id": "-BY6RJYBLe2GoFQ6-9n9",
"_score": 1.2696637,
"_source": {
"pet_name": "Rocky",
"owner_name": "Rachel Green",
"species": "Dog",
"visit_details": "Skin rash, antihistamines prescribed.",
"breed": "Bulldog Mix",
"vaccination_history": [
"Rabies",
"Parvovirus"
]
}
}
]
}ハイブリッド クエリ (/search/hybrid?q=<query_term>)
ハイブリッド検索により、セマンティック検索と語彙検索を組み合わせることができるため、両方の長所を活用できます。つまり、トークンによる検索の精度と、セマンティック検索の意味の近似性の両方が得られます。
app.get("/search/hybrid", async (req, res) => {
const { q } = req.query;
const INDEX_NAME = "vet-visits";
try {
const result = await esClient.search({
index: INDEX_NAME,
body: {
retriever: {
rrf: {
retrievers: [
{
standard: {
query: {
bool: {
must: {
multi_match: {
query: q,
fields: ["owner_name", "pet_name", "visit_details"],
},
},
},
},
},
},
{
standard: {
query: {
bool: {
must: {
semantic: {
field: "semantic_field",
query: q,
},
},
},
},
},
},
],
},
},
size: 5,
},
});
res.status(200).json({
success: true,
results: result.hits.hits,
});
} catch (error) {
console.error("Error performing search:", error);
res.status(500).json({
success: false,
results: null,
error: error.message,
});
}
});「ペディキュアや歯科治療を受けた人はいますか?」という質問をしてテストします。
curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment対応:
{
"success": true,
"results": [
{
"_index": "vet-visits",
"_id": "9xY6RJYBLe2GoFQ6-9n9",
"_score": 0.032522473,
"_source": {
"pet_name": "Simba",
"owner_name": "Leo Martínez",
"species": "Cat",
"visit_details": "Dental cleaning. Minor tartar buildup removed.",
"breed": "Maine Coon",
"vaccination_history": [
"Rabies",
"Feline Panleukopenia"
]
}
},
{
"_index": "vet-visits",
"_id": "-RY6RJYBLe2GoFQ6-9n9",
"_score": 0.016393442,
"_source": {
"pet_name": "Mochi",
"owner_name": "Daniel Kim",
"species": "Rabbit",
"visit_details": "Nail trimming and general health check. No issues.",
"breed": "Mixed",
"vaccination_history": []
}
},
{
"_index": "vet-visits",
"_id": "8BY6RJYBLe2GoFQ6-9n9",
"_score": 0.015873017,
"_source": {
"pet_name": "Buddy",
"owner_name": "Alice Johnson",
"species": "Dog",
"visit_details": "Annual check-up and nail trimming. Healthy and active.",
"breed": "Golden Retriever",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Distemper"
]
}
},
{
"_index": "vet-visits",
"_id": "9RY6RJYBLe2GoFQ6-9n9",
"_score": 0.015625,
"_source": {
"pet_name": "Rex",
"owner_name": "Tomás Herrera",
"species": "Dog",
"visit_details": "Follow-up for previous leg strain, improving well.",
"breed": "German Shepherd",
"vaccination_history": [
"Rabies",
"Parvovirus",
"Leptospirosis"
]
}
},
{
"_index": "vet-visits",
"_id": "8xY6RJYBLe2GoFQ6-9n9",
"_score": 0.015384615,
"_source": {
"pet_name": "Luna",
"owner_name": "Jake Thompson",
"species": "Dog",
"visit_details": "Mild ear infection, cleaning and antibiotics given.",
"breed": "Labrador Mix",
"vaccination_history": [
"Rabies",
"Bordetella"
]
}
}
]
}まとめ
このシリーズの最初の部分では、クライアント/サーバーのベストプラクティスに従って、環境を設定し、さまざまな検索エンドポイントを持つサーバーを作成し、Elasticsearch ドキュメントをクエリする方法を説明しました。シリーズのパート 2では、本番環境のベスト プラクティスと、サーバーレス環境で Elasticsearch Node.js クライアントを実行する方法について学習します。
よくあるご質問
Node.js クライアントとは何ですか?
Node.jsクライアントは、Elasticsearch APIからのHTTP REST呼び出しをJavaScriptに変換するJavaScriptライブラリです。バッチで文書をインデキシングする作業を簡素化してくれるヘルパーが使いやすくなります。
フロントエンドからElasticsearchを呼び出す代わりに、サーバー側のNode.js環境を使用する必要があるのはなぜですか?
Securityが主な利点です。クライアントをバックエンド環境(Node.js with Expressなど)で実行することで、Cluster APIキー、ホストURL、内部クエリロジックなどの機密情報がブラウザに露出することを防止します。
Node.jsでElasticsearchの「Bulk Helper」を使う利点は何ですか?
Node.jsでElasticsearchの「Bulk Helper」を使用する主な利点は以下の通りです。 バッチインデキシング:文書を一つずつではなくグループでインデキシングする際の複雑さを自動的に処理します。 ストリームのサポート:split2のようなツールを使うと、ファイル(.ndjsonなど)を一行ずつストリーミングできます。これにより、サーバーのメモリにデータセット全体をロードすることなく、大規模なファイルを処理できます。

