RubyアプリをOpenSearchからElasticsearchに移行する方法
Ruby コードベースを OpenSearch クライアントから Elasticsearch クライアントに移行するためのガイド。
OpenSearch Ruby クライアントは、バージョン7.xの Elasticsearch Ruby クライアントからフォークされたため、コードベースは比較的似ています。つまり、Ruby コードベースを OpenSearch から Elasticsearch に移行すると、それぞれのクライアント ライブラリのコードは非常に馴染みのあるものになります。このブログ記事では、OpenSearch を使用する Ruby アプリの例と、このコードを Elasticsearch に移行する手順を紹介します。
どちらのクライアントも、一般的な Apache License 2.0 に基づいてリリースされているため、オープンソースかつフリーソフトウェアです。Elasticsearch のライセンスは最近更新され、Elasticsearch と Kibana のコアはバージョン 8.16 以降、OSI 承認のオープン ソース ライセンス AGPL に基づいて公開されています。
Rubyアプリの移行時にElasticsearchのバージョンを考慮する
移行時に考慮すべき点の 1 つは、どのバージョンの Elasticsearch を使用するかということです。最新の安定リリース (この記事の執筆時点では8.17.0を使用することをお勧めします。Elasticsearch Ruby クライアントのマイナー バージョンは、Elasticsearch のマイナー バージョンに従います。したがって、Elasticsearch 8.17.xでは、Ruby gem のバージョン8.17.xを使用できます。
OpenSearch は Elasticsearch 7.10.2 からフォークされました。そのため、API が変更され、どちらでも異なる機能が使用される可能性があります。しかし、それはこの記事の範囲外なので、サンプル アプリで最も一般的な操作についてのみ説明します。
Ruby on Rails の場合、公式の Elasticsearch クライアント、またはRails 統合ライブラリを使用できます。Elasticsearch とクライアントの最新の安定バージョンに移行することをお勧めします。elasticsearch-rails gem バージョン8.0.0 Rails 6.1 、 7.0 、 7.1 、および Elasticsearch 8.xをサポートします。
コード
この例では、 tarball から OpenSearch をインストールする手順に従いました。tarball をダウンロードして解凍した後、後でクライアントをインスタンス化するために使用する初期管理者パスワードを設定する必要がありました。
次のようなGemfileを持つディレクトリを作成しました:
source 'https://rubygems.org'
gem 'opensearch-ruby'bundle installを実行すると、プロジェクトに gem がインストールされます。インストールされた opensearch-ruby バージョン3.4.0と、実行している OpenSearch のバージョンは2.18.0です。同じディレクトリ内のexample_code.rbファイルにコードを記述しました。このファイルの初期コードは、OpenSearch クライアントのインスタンス化です。
require 'opensearch'
client = OpenSearch::Client.new(
host: 'https://localhost:9200',
user: 'admin',
password: ENV['OPENSEARCH_INITIAL_ADMIN_PASSWORD'],
transport_options: { ssl: { verify: false } }
)テストを容易にするために、ユーザー ガイドに従ってトランスポート オプションssl: { verify: false}パラメータが渡されます。本番環境では、OpenSearch の展開に応じてこれを設定する必要があります。
OpenSearch バージョン 2.12.0 以降では、インストール スクリプトを実行するときに、 OPENSEARCH_INITIAL_ADMIN_PASSWORD環境変数を強力なパスワードに設定する必要があります。tarball から OpenSearch をインストールする手順に従って、コンソールで変数をエクスポートし、Ruby スクリプトで使用できるようになりました。
クライアントが OpenSearch に接続していることを確認するための簡単な API は、 cluster.health API を使用しています。
puts 'HEALTH:'
pp client.cluster.healthそして実際にそれは機能します:
$ be ruby example_code.rb
HEALTH:
{"cluster_name"=>"opensearch",
"status"=>"yellow",
"timed_out"=>false,
"number_of_nodes"=>1,
"number_of_data_nodes"=>1,Elasticsearch Ruby クライアントのドキュメントにある一般的な例をいくつかテストしたところ、期待どおりに動作しました。
index = 'books'
puts 'Creating index'
response = client.indices.create(index: index)
puts response
# Creating index
# {"acknowledged"=>true, "shards_acknowledged"=>true, "index"=>"books"}
puts 'Indexing a document'
document = { title: 'The Time Machine', author: 'H. G. Wells', year: 1895 }
response = client.index(index: index, body: document, refresh: true)
puts response
# Indexing document
# {"_index"=>"books", "_id"=>"esalT5MB4vnuJz5TtqOc", "_version"=>1, "result"=>"created", "forced_refresh"=>true, "_shards"=>{"total"=>2, "successful"=>1, "failed"=>0}, "_seq_no"=>0, "_primary_term"=>1}
id = response['_id']
puts 'Getting document'
response = client.get(index: index, id: id)
puts response
# Getting document
# {"_index"=>"books", "_id"=>"esalT5MB4vnuJz5TtqOc", "_version"=>1, "_seq_no"=>0, "_primary_term"=>1, "found"=>true, "_source"=>{"title"= >"The Time Machine", "author"=>"H. G. Wells", "year"=>1895}}
puts "Does an index exist?"
puts client.indices.exists(index: 'imaginary_index')
# Does an index exist?
# false
puts 'Processing Bulk request'
body = [
{ index: { _index: 'books', data: { name: 'Leviathan Wakes', author: 'James S.A. Corey', release_date: '2011-06-02', page_count: 561 } } },
{ index: { _index: 'books', data: { name: 'Hyperion', author: 'Dan Simmons', release_date: '1989-05-26', page_count: 482 } } },
{ index: { _index: 'books', data: { name: 'Dune', author: 'Frank Herbert', release_date: '1965-06-01', page_count: 604 } } },
{ index: { _index: 'books', data: { name: 'Dune Messiah', author: 'Frank Herbert', release_date: '1969-10-15', page_count: 331 } } },
{ index: { _index: 'books', data: { name: 'Children of Dune', author: 'Frank Herbert', release_date: '1976-04-21', page_count: 408 } } },
{ index: { _index: 'books', data: { name: 'God Emperor of Dune', author: 'Frank Herbert', release_date: '1981-05-28', page_count: 454 } } },
{ index: { _index: 'books', data: { name: 'Consider Phlebas', author: 'Iain M. Banks', release_date: '1987-04-23', page_count: 471 } } },
{ index: { _index: 'books', data: { name: 'Pandora\'s Star', author: 'Peter F. Hamilton', release_date: '2004-03-02', page_count: 768 } } },
{ index: { _index: 'books', data: { name: 'Revelation Space', author: 'Alastair Reynolds', release_date: '2000-03-15', page_count: 585 } } },
{ index: { _index: 'books', data: { name: 'A Fire Upon the Deep', author: 'Vernor Vinge', release_date: '1992-06-01', page_count: 613 } } },
{ index: { _index: 'books', data: { name: 'Ender\'s Game', author: 'Orson Scott Card', release_date: '1985-06-01', page_count: 324 } } },
{ index: { _index: 'books', data: { name: '1984', author: 'George Orwell', release_date: '1985-06-01', page_count: 328 } } },
{ index: { _index: 'books', data: { name: 'Fahrenheit 451', author: 'Ray Bradbury', release_date: '1953-10-15', page_count: 227 } } },
{ index: { _index: 'books', data: { name: 'Brave New World', author: 'Aldous Huxley', release_date: '1932-06-01', page_count: 268 } } },
{ index: { _index: 'books', data: { name: 'Foundation', author: 'Isaac Asimov', release_date: '1951-06-01', page_count: 224 } } },
{ index: { _index: 'books', data: { name: 'The Giver', author: 'Lois Lowry', release_date: '1993-04-26', page_count: 208 } } },
{ index: { _index: 'books', data: { name: 'Slaughterhouse-Five', author: 'Kurt Vonnegut', release_date: '1969-06-01', page_count: 275 } } },
{ index: { _index: 'books', data: { name: 'The Hitchhiker\'s Guide to the Galaxy', author: 'Douglas Adams', release_date: '1979-10-12', page_count: 180 } } },
{ index: { _index: 'books', data: { name: 'Snow Crash', author: 'Neal Stephenson', release_date: '1992-06-01', page_count: 470 } } },
{ index: { _index: 'books', data: { name: 'Neuromancer', author: 'William Gibson', release_date: '1984-07-01', page_count: 271 } } },
{ index: { _index: 'books', data: { name: 'The Handmaid\'s Tale', author: 'Margaret Atwood', release_date: '1985-06-01', page_count: 311 } } },
{ index: { _index: 'books', data: { name: 'Starship Troopers', author: 'Robert A. Heinlein', release_date: '1959-12-01', page_count: 335 } } },
{ index: { _index: 'books', data: { name: 'The Left Hand of Darkness', author: 'Ursula K. Le Guin', release_date: '1969-06-01', page_count: 304 } } },
{ index: { _index: 'books', data: { name: 'The Moon is a Harsh Mistress', author: 'Robert A. Heinlein', release_date: '1966-04-01', page_count: 288 } } }
]
puts client.bulk(body: body, refresh: true)
# Processing Bulk request
# {"took"=>38, "errors"=>false, "items"=>[{"index"=>{"_index"=>"books", "_id"=>" ...
query = { query: { multi_match: { query: 'dune', fields: ['name'] } } }
puts 'Search results'
response = client.search(index: index, body: query)
puts response
# Search results
# {"_index"=>"books", "_id"=>"oEawT5MBOXHuGXdEu5Wu", "_score"=>2.2886353, "_source"=>{"name"=>"Dune", "author"=>"Frank Herbert", "release_date"=>"1965-06-01", "page_count"=>604}}
# {"_index"=>"books", "_id"=>"oUawT5MBOXHuGXdEu5Wu", "_score"=>1.8893257, "_source"=>{"name"=>"Dune Messiah", "author"=>"Frank Herbert", "release_date"=>"1969-10-15", "page_count"=>331}}
# {"_index"=>"books", "_id"=>"okawT5MBOXHuGXdEu5Wu", "_score"=>1.6086557, "_source"=>{"name"=>"Children of Dune", "author"=>"Frank Herbert", "release_date"=>"1976-04-21", "page_count"=>408}}
# {"_index"=>"books", "_id"=>"o0awT5MBOXHuGXdEu5Wu", "_score"=>1.40059, "_source"=>{"name"=>"God Emperor of Dune", "author"=>"Frank Herbert", "release_date"=>"1981-05-28", "page_count"=>454}}
puts 'Updating document'
document = { title: 'Walkaway', author: 'Cory Doctorow', release_date: '2017' }
response = client.index(index: index, body: document, refresh: true)
id = response['_id']
response = client.update(index: index, id: id, body: { doc: { release_date: '2017-04-26' } })
puts response
# Updating document
# {"_index"=>"books", "_id"=>"degnZJMBIGr4X0Yim55L", "_version"=>2, "result"=>"updated", "_shards"=>{"total"=>2, "successful"=>1, "failed"=>0}, "_seq_no"=>26, "_primary_term"=>1}
puts 'Retrieveing multiple documents'
response = client.search(index: index, body: { query: { match_all: {} }, size: 3, stored_fields: '_id' })
ids = response['hits']['hits']
ids.map { |a| a.delete('_score') }
response = client.mget(body: { docs: [{ _index: index, _id: ids }] })
puts response
# Retrieveing multiple documents
# {"docs"=>[{"_index"=>"books", "_id"=>"qeg2ZJMBIGr4X0YiiqD2", "_version"=>1, "_seq_no"=>0, "_primary_term"=>1, "found"=>true, "_source"=>{"title"=>"The Time Machine", "author"=>"H. G. Wells", "year"=>1895}}, {"_index"=>"books", "_id"=>"q-g2ZJMBIGr4X0Yii6Ah", "_version"=>1, "_seq_no"=>1, "_primary_term"=>1, "found"=>true, "_source"=>{"name"=>"Leviathan Wakes", "author"=>"James S.A. Corey", "release_date"=>"2011-06-02", "page_count"=>561}}, {"_index"=>"books", "_id"=>"rOg2ZJMBIGr4X0Yii6Ah", "_version"=>1, "_seq_no"=>2, "_primary_term"=>1, "found"=>true, "_source"=>{"name"=>"Hyperion", "author"=>"Dan Simmons", "release_date"=>"1989-05-26", "page_count"=>482}}]}
puts "Count #{client.count(index: index)['count']}"
puts 'Deleting by query'
response = client.delete_by_query(index: index, body: { query: { match: { author: 'Robert A. Heinlein' } } }, refresh: true)
puts response
puts "Count #{client.count(index: index)['count']}"
# Count 26
# Deleting by query
# {"took"=>16, "timed_out"=>false, "total"=>2, "deleted"=>2, "batches"=>1, "version_conflicts"=>0, "noops"=>0, "retries"=>{"bulk"=>0, "search"=>0}, "throttled_millis"=>0, "requests_per_second"=>-1.0, "throttled_until_millis"=>0, "failures"=>[]}
# Count 24
puts 'Deleting document'
response = client.delete(index: index, id: id)
puts response
# Deleting document
# {"_index"=>"books", "_id"=>"nEawT5MBOXHuGXdEu5WA", "_version"=>2, "result"=>"deleted", "_shards"=>{"total"=>2, "successful"=>1, "failed"=>0}, "_seq_no"=>25, "_primary_term"=>1}
puts 'Deleting index'
response = client.indices.delete(index: index)
puts response
# Deleting index
# {"acknowledged"=>true}RubyアプリをElasticsearchに移行する
最初のステップは、Gemfile にelasticsearch-rubyを追加することです。bundle installを実行すると、Elasticsearch Ruby クライアント gem がインストールされます。完全に移行する前にコードをテストしたい場合は、最初はopensearch-ruby gem をそこに残しておくことができます。
次の重要なステップは、クライアントのインスタンス化です。これは、Elasticsearch をどのように実行しているかによって異なります。これらの例に対して同様のアプローチを維持するために、 「Elasticsearch をダウンロードしてローカルで実行する」の手順に従います。
bin/elasticsearchを実行すると、Elasticsearch は自動的に構成されたセキュリティ機能で起動します。必ず elastic ユーザーのパスワードをコピーしてください (ただし、 bin/elasticsearch-reset-password -u elasticを実行してリセットできます)。この例に従う場合は、OpenSearch と Elasticsearch は同じポートで実行されるため、Elasticsearch を起動する前に必ず OpenSearch を停止してください。
example_code.rbの始めで、OpenSearch クライアントのインスタンス化をコメント アウトし、Elasticsearch クライアントのインスタンス化を追加しました。
# require 'opensearch'
# client = OpenSearch::Client.new(
# host: 'https://localhost:9200',
# user: 'admin',
# password: ENV['OPENSEARCH_INITIAL_ADMIN_PASSWORD']
# transport_options: { ssl: { verify: false } }
# )
require 'elasticsearch'
client = Elasticsearch::Client.new(
host: 'https://localhost:9200',
user: ENV['ELASTICSEARCH_USER'],
password: ENV['ELASTICSEARCH_PASSWORD'],
transport_options: { ssl: { verify: false } }
)ご覧のとおり、このテスト シナリオではコードはほぼ同じです。これは、Elasticsearch のデプロイメントと、Elasticsearch との接続および認証の方法によって異なります。セキュリティに関しては OpenSearch と同じことが当てはまります。SSL を検証しないオプションはテスト目的のみであり、本番環境では使用しないでください。
クライアントがセットアップされたら、 bundle exec ruby example_code.rbを使用してコードを再度実行します。そして、すべてがうまく機能します!
移行問題のデバッグ
アプリケーションが使用している API によっては、OpenSearch の API が異なる場合、Elasticsearch に対してコードを実行するとエラーが発生する可能性があります。REST API ドキュメントは、API の使用方法に関する詳細な情報を得るための重要なリファレンスです。使用している Elasticsearch のバージョンのドキュメントを必ず確認してください。Elasticsearch::API参照も参照できます。
Elasticsearch で発生する可能性のあるエラーには次のようなものがあります:
ArgumentError: Required argument '<ARGUMENT>' missing- これはクライアント エラーであり、リクエストに必要なパラメータがない場合に発生します。Elastic::Transport::Transport::Errors::BadRequest: [400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"request [/example/_doc] contains unrecognized parameter: [test]"}]...このエラーは Elasticsearch から発生し、クライアント コードが、使用されている API に対して Elasticsearch が認識しないパラメータを使用していることを意味します。
Elasticsearch クライアントは、サーバーから送信された詳細なエラー メッセージとともに、Elasticsearch からのエラーを発生させます。したがって、サポートされていないパラメータやエンドポイントの場合でも、エラーによって何が異なっているかが通知されるはずです。
まとめ
このサンプル コードで示したように、Ruby アプリを OpenSearch から Elasticsearch に移行することは、Ruby 側から見るとそれほど複雑ではありません。バージョン管理と、検索エンジン間で異なる可能性のある API に注意する必要があります。しかし、最も一般的なアクションでは、クライアントを移行する際の主な変更はインスタンス化にあります。その点ではどちらも似ていますが、ホストと資格情報の定義方法は、スタックの展開方法によって異なります。クライアントがセットアップされ、Elasticsearch に接続していることを確認したら、OpenSearch クライアントを Elasticsearch クライアントにシームレスに置き換えることができます。

