これは、なにをしたくて書いたもの?
ElasticsearchのJavaScriptクライアントのドキュメントを見ると、TypeScriptのサポートがありそうだったので
こちらを使ってElasticsearchにアクセスするコードを書いてみようかなと。
TypeScript support | Elasticsearch JavaScript Client [7.16] | Elastic
Elasticsearch JavaScriptクライアント × TypeScript
ElasticsearchのJavaScriptクライアントに関するドキュメントはこちら。
Elasticsearch JavaScript Client [7.16] | Elastic
TypeScriptサポートに関する記述はあるのですが、こちらには型宣言のインストールについて記載がないのがちょっと
気になります。
TypeScript support | Elasticsearch JavaScript Client [7.16] | Elastic
GitHubリポジトリを見るとJavaScriptクライアント自体がTypeScriptで書かれているようで、型宣言を別途インストールする
必要はなさそうですね。
GitHub - elastic/elasticsearch-js: Official Elasticsearch client library for Node.js
探すと、古い情報が引っかかったりします…。
ところで、現在の型定義情報は「レガシー」扱いらしく、新しい型定義が次のメジャーバージョンで採用されるようです。
※デフォルトは「レガシー」の方です
TypeScript support / New type definitions
TypeScript support / How to migrate to the new type definition
なのですが、今回は現在の型定義を使いたいと思います。
その他、参考にしたドキュメントはこちら。
Introduction | Elasticsearch JavaScript Client [7.16] | Elastic
API Reference | Elasticsearch JavaScript Client [7.16] | Elastic
Configuration | Elasticsearch JavaScript Client [7.16] | Elastic
環境
今回の環境は、こちらです。
$ node --version v16.13.1 $ nvm --version 0.37.2
Elasticsearchに関しては、192.168.33.12で動作しているものとします。
設定はこんな感じで
$ sudo grep -v '^#' /etc/elasticsearch/elasticsearch.yml path.data: /var/lib/elasticsearch path.logs: /var/log/elasticsearch network.host: 0.0.0.0 discovery.type: "single-node" xpack.security.enabled: true
ビルトインユーザーのパスワードは自動生成しました。
$ sudo /usr/share/elasticsearch/bin/elasticsearch-setup-passwords auto Initiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user. The passwords will be randomly generated and printed to the console. Please confirm that you would like to continue [y/N]y Changed password for user apm_system PASSWORD apm_system = LtWE8jjtMgFSnAzLjLOw Changed password for user kibana_system PASSWORD kibana_system = E3ClxGwt0D41LVXgYt7q Changed password for user kibana PASSWORD kibana = E3ClxGwt0D41LVXgYt7q Changed password for user logstash_system PASSWORD logstash_system = fNur7zlk6qSnUseQrpEQ Changed password for user beats_system PASSWORD beats_system = 19Am6dZFtY0EzEUlc3tB Changed password for user remote_monitoring_user PASSWORD remote_monitoring_user = kbEeEnSDHNe8z6Cn1bQX Changed password for user elastic PASSWORD elastic = fFwiUpFAgEnQULb1Kgim
今回は、elastic
ユーザーを使用することにします。
バージョン情報。
$ curl -u elastic:fFwiUpFAgEnQULb1Kgim localhost:9200 { "name" : "elasticsearch-server", "cluster_name" : "elasticsearch", "cluster_uuid" : "NMxhp1roRYOO5Hi3WpcAVA", "version" : { "number" : "7.16.2", "build_flavor" : "default", "build_type" : "deb", "build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb", "build_date" : "2021-12-18T19:42:46.604893745Z", "build_snapshot" : false, "lucene_version" : "8.10.1", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
Javaのバージョン。
$ java --version openjdk 17.0.1 2021-10-19 OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)
プロジェクトを作成する
今回、テストコードで確認をしようと思います。まずはNode.jsプロジェクトを作成。
$ npm init -y $ npm i -D typescript $ npm i -D -E prettier $ npm i -D jest @types/jest ts-jest $ npx ts-jest config:init $ mkdir src test $ npm i -D @types/node
Jestもインストールしておきます。
ドキュメントに従って、Elasticsearchクライアントのインストール。
Installation | Elasticsearch JavaScript Client [7.16] | Elastic
$ npm i @elastic/elasticsearch
依存関係は、このようになりました。
"devDependencies": { "@types/jest": "^27.0.3", "@types/node": "^17.0.5", "jest": "^27.4.5", "prettier": "2.5.1", "ts-jest": "^27.1.2", "typescript": "^4.5.4" }, "dependencies": { "@elastic/elasticsearch": "^7.16.0" }
各種設定ファイルの内容は、こちら。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "esModuleInterop": true }, "include": [ "src" ] }
.prettierrc.json
{ "singleQuote": true }
jest.config.js
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', };
テストコードを書いて確認する
では、テストコードを使って確認していきましょう。
テストコードは、test/elasticsearchClient.test.ts
として作成します。
今回import
するものはこちら。
import { Client, estypes } from '@elastic/elasticsearch';
ここから先は、いくつかピックアップしてAPIを確認していきたいと思います。
Clientを作成する
ElasticsearchにアクセスするためのClient
は、このようにして作成します。
test('create Elasticsearch client', async () => { const client = new Client({ node: 'http://192.168.33.12:9200', auth: { username: 'elastic', password: 'fFwiUpFAgEnQULb1Kgim', }, }); });
ドキュメントとしては、このあたりも見るとよいでしょう。
Creating a child client | Elasticsearch JavaScript Client [7.16] | Elastic
Basic configuration | Elasticsearch JavaScript Client [7.16] | Elastic
ただ、この時点では正しく接続できているかはわからないみたいです。
ドキュメントを登録する
では、まずはドキュメントを登録してみましょう。扱うテーマは書籍とします。
TypeScriptサポートのドキュメントを見ていると、型を定義した方が良さそうなので、そのようにしておきます。
TypeScript support / Request & Response types
interface Book { isbn: string; title: string; price: number; }
登録。
test('register documents', async () => { const client = new Client({ node: 'http://192.168.33.12:9200', auth: { username: 'elastic', password: 'fFwiUpFAgEnQULb1Kgim', }, }); const indexName = 'myindex'; await client .create<Book>({ index: indexName, id: '978-4295009771', body: { isbn: '978-4295009771', title: 'Elastic Stack実践ガイド[Elasticsearch/Kibana編]', price: 3300, }, }) .then(() => client .create<Book>({ index: indexName, id: '978-4844398981', body: { isbn: '978-4844398981', title: 'Elasticsearch NEXT STEP', price: 1980, }, }) .then(() => client.create<Book>({ index: indexName, id: '978-1789956504', body: { isbn: '978-1789956504', title: 'Elasticsearch 7.0 Cookbook: Over 100 recipes for fast, scalable, and reliable search for your enterprise, 4th Edition', price: 6250, }, }) ) ); await client.indices.refresh({ index: indexName }); await client .count<estypes.CountResponse>({ index: indexName }) .then((res) => expect(res.body.count).toBe(3)); });
ドキュメントを3つ登録していますが、メソッドの呼び出し結果はPromise
になっているので、これをつないでいます。
await client .create<Book>({ index: indexName, id: '978-4295009771', body: { isbn: '978-4295009771', title: 'Elastic Stack実践ガイド[Elasticsearch/Kibana編]', price: 3300, }, }) .then(() => client .create<Book>({ index: indexName, id: '978-4844398981', body: { isbn: '978-4844398981', title: 'Elasticsearch NEXT STEP', price: 1980, }, }) .then(() => client.create<Book>({ index: indexName, id: '978-1789956504', body: { isbn: '978-1789956504', title: 'Elasticsearch 7.0 Cookbook: Over 100 recipes for fast, scalable, and reliable search for your enterprise, 4th Edition', price: 6250, }, }) ) );
インデックスは、ドキュメント登録時の自動作成に任せました。
確認のために、インデックスをリフレッシュ。
await client.indices.refresh({ index: indexName });
Count APIを使って、ドキュメントが登録されたことを確認。
await client .count<estypes.CountResponse>({ index: indexName }) .then((res) => expect(res.body.count).toBe(3));
CountResponse
を指定しておかないと、body#count
が解決できません…。
接続情報を間違った場合
検索に進む前に。
Client
を作成した時に、この時点では正しく接続できるかどうかはわからないようだ、と書きましたが。
いつわかるかというと、実際にElasticsearchにアクセスするオペレーションを行った時に顕在化するみたいです。
以下に、認証情報を設定しなかったり、接続先ポートを誤った場合にどうなるかを記載しています。
test('wrong connection', async () => { const noAuthClient = new Client({ node: 'http://192.168.33.12:9200', }); const indexName = 'myindex'; await noAuthClient .create<Book>({ index: indexName, id: '978-4295009771', refresh: true, body: { isbn: '978-4295009771', title: 'Elastic Stack実践ガイド[Elasticsearch/Kibana編]', price: 3300, }, }) .catch((e: Error) => { expect(e.name).toBe('ResponseError'); expect(e.message).toMatch( /^security_exception: \[security_exception\] Reason: missing authentication credentials for REST request \[\/myindex\/_create\/978-4295009771\?refresh=true\]/ ); }); const invalidDestinationClient = new Client({ node: 'http://192.168.33.12:9201', auth: { username: 'elastic', password: 'fFwiUpFAgEnQULb1Kgim', }, }); await invalidDestinationClient .create<Book>({ index: indexName, id: '978-4295009771', refresh: true, body: { isbn: '978-4295009771', title: 'Elastic Stack実践ガイド[Elasticsearch/Kibana編]', price: 3300, }, }) .catch((e: Error) => { expect(e.name).toBe('ConnectionError'); expect(e.message).toMatch(/^connect ECONNREFUSED 192.168.33.12:9201/); }); });
検索する
検索を行ってみましょう。Client#search
で行います。
test('search documents', async () => { const client = new Client({ node: 'http://192.168.33.12:9200', auth: { username: 'elastic', password: 'fFwiUpFAgEnQULb1Kgim', }, }); const indexName = 'myindex'; await client .search<estypes.SearchResponse<Book>>({ index: indexName, q: '*', sort: ['price:asc'], }) .then((res) => { expect(res.body.hits.total).toStrictEqual({ relation: 'eq', value: 3 }); expect(res.body.hits.hits[0]._source?.title).toBe( 'Elasticsearch NEXT STEP' ); expect(res.body.hits.hits[1]._source?.title).toBe( 'Elastic Stack実践ガイド[Elasticsearch/Kibana編]' ); expect(res.body.hits.hits[2]._source?.title).toBe( 'Elasticsearch 7.0 Cookbook: Over 100 recipes for fast, scalable, and reliable search for your enterprise, 4th Edition' ); }); });
search
の場合は、SearchReponse
とドキュメントの型定義を組み合わせて使うと、_source
の型情報が解決できるように
なります。
await client .search<estypes.SearchResponse<Book>>({
インデックスを削除する
最後に、インデックスを削除してみます。
test('delete index', async () => { const client = new Client({ node: 'http://192.168.33.12:9200', auth: { username: 'elastic', password: 'fFwiUpFAgEnQULb1Kgim', }, }); const indexName = 'myindex'; await client.indices.delete({ index: indexName, }); await client.indices .exists({ index: indexName }) .then((res) => expect(res.statusCode).toBe(404)); });
削除して
await client.indices.delete({ index: indexName, });
削除されたことを確認。
await client.indices .exists({ index: indexName }) .then((res) => expect(res.statusCode).toBe(404));
いったん、こんなところでしょうか。
まとめ
ElasticsearchのJavaScriptクライアントを、TypeScriptを使って試してみました。
TypeScriptに慣れていないのも合わせって、だいぶてこずるのですが…こういうのを繰り返して慣れていこうと思います…。