The OpenSearch Ruby client was forked from the Elasticsearch Ruby Client in version 7.x
, so the codebases are relatively similar. This means when migrating a Ruby codebase from OpenSearch to Elasticsearch, the code from the respective client libraries will look very familiar. In this blog post, I27;m going to show an example Ruby app that uses OpenSearch and the steps to migrate this code to Elasticsearch.
Both clients are released under the popular Apache License 2.0, so they27;re open source and free software. Elasticsearch27;s license was recently updated and the core of Elasticsearch and Kibana are published under the OSI approved Open Source license AGPL since version 8.16.
Versions
One consideration when migrating is which version of Elasticsearch is going to be used. We recommend using the latest stable release, which at the time of writing this is 8.17.0
. The Elasticsearch Ruby Client minor versions follow the Elasticsearch minor versions. So for Elasticsearch 8.17.x
, you can use version 8.17.x
of the Ruby gem.
OpenSearch was forked from Elasticsearch 7.10.2. So the APIs may have changed and different features could be used on either. But that27;s out of scope for this post, and I27;m only going to look into the most common operations in an example app.
For Ruby on Rails, you can use the official Elasticsearch client, or the Rails integration libraries. We recommend migrating to the latest stable version of Elasticsearch and client respectively. The elasticsearch-rails
gem version 8.0.0
support Rails 6.1
, 7.0
and 7.1
and Elasticsearch 8.x
.
The code
For this example, I followed the steps to install OpenSearch from a tarball. After downloading and extracting the tarball, I needed to set an initial admin password which I27;m going to use later to instantiate the client.
I created a directory with a Gemfile
that looks like this:
source 27;https://rubygems.org27;
gem 27;opensearch-ruby27;
After running bundle install
, the gem is installed for my project. This installed opensearch-ruby version 3.4.0
and the version of OpenSearch I27;m running is 2.18.0
. I wrote the code in an example_code.rb
file in the same directory. The initial code in this file is the instantiation of an OpenSearch client:
require 27;opensearch27;
client = OpenSearch::Client.new(
host: 27;https://localhost:920027;,
user: 27;admin27;,
password: ENV[27;OPENSEARCH_INITIAL_ADMIN_PASSWORD27;],
transport_options: { ssl: { verify: false } }
)
The transport option ssl: { verify: false}
parameter is being passed as per the user guide to make things easier for testing. In production, this should be set up depending on the deployment of OpenSearch.
Since version 2.12.0 of OpenSearch, the OPENSEARCH_INITIAL_ADMIN_PASSWORD
environment variable must be set to a strong password when running the install script. Following the steps to install OpenSearch from a tarball, I exported the variable in my console and now it27;s available for my Ruby script.
A simple API to make sure the client is connecting to OpenSearch is using the cluster.health
API:
puts 27;HEALTH:27;
pp client.cluster.health
And indeed it works:
$ be ruby example_code.rb
HEALTH:
{"cluster_name"=>"opensearch",
"status"=>"yellow",
"timed_out"=>false,
"number_of_nodes"=>1,
"number_of_data_nodes"=>1,
I tested some of the common examples we have on the Elasticsearch Ruby client documentation, and they work as expected:
index = 27;books27;
puts 27;Creating index27;
response = client.indices.create(index: index)
puts response
# Creating index
# {"acknowledged"=>true, "shards_acknowledged"=>true, "index"=>"books"}
puts 27;Indexing a document27;
document = { title: 27;The Time Machine27;, author: 27;H. G. Wells27;, 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[27;_id27;]
puts 27;Getting document27;
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: 27;imaginary_index27;)
# Does an index exist?
# false
puts 27;Processing Bulk request27;
body = [
{ index: { _index: 27;books27;, data: { name: 27;Leviathan Wakes27;, author: 27;James S.A. Corey27;, release_date: 27;2011-06-0227;, page_count: 561 } } },
{ index: { _index: 27;books27;, data: { name: 27;Hyperion27;, author: 27;Dan Simmons27;, release_date: 27;1989-05-2627;, page_count: 482 } } },
{ index: { _index: 27;books27;, data: { name: 27;Dune27;, author: 27;Frank Herbert27;, release_date: 27;1965-06-0127;, page_count: 604 } } },
{ index: { _index: 27;books27;, data: { name: 27;Dune Messiah27;, author: 27;Frank Herbert27;, release_date: 27;1969-10-1527;, page_count: 331 } } },
{ index: { _index: 27;books27;, data: { name: 27;Children of Dune27;, author: 27;Frank Herbert27;, release_date: 27;1976-04-2127;, page_count: 408 } } },
{ index: { _index: 27;books27;, data: { name: 27;God Emperor of Dune27;, author: 27;Frank Herbert27;, release_date: 27;1981-05-2827;, page_count: 454 } } },
{ index: { _index: 27;books27;, data: { name: 27;Consider Phlebas27;, author: 27;Iain M. Banks27;, release_date: 27;1987-04-2327;, page_count: 471 } } },
{ index: { _index: 27;books27;, data: { name: 27;Pandora\27;s Star27;, author: 27;Peter F. Hamilton27;, release_date: 27;2004-03-0227;, page_count: 768 } } },
{ index: { _index: 27;books27;, data: { name: 27;Revelation Space27;, author: 27;Alastair Reynolds27;, release_date: 27;2000-03-1527;, page_count: 585 } } },
{ index: { _index: 27;books27;, data: { name: 27;A Fire Upon the Deep27;, author: 27;Vernor Vinge27;, release_date: 27;1992-06-0127;, page_count: 613 } } },
{ index: { _index: 27;books27;, data: { name: 27;Ender\27;s Game27;, author: 27;Orson Scott Card27;, release_date: 27;1985-06-0127;, page_count: 324 } } },
{ index: { _index: 27;books27;, data: { name: 27;198427;, author: 27;George Orwell27;, release_date: 27;1985-06-0127;, page_count: 328 } } },
{ index: { _index: 27;books27;, data: { name: 27;Fahrenheit 45127;, author: 27;Ray Bradbury27;, release_date: 27;1953-10-1527;, page_count: 227 } } },
{ index: { _index: 27;books27;, data: { name: 27;Brave New World27;, author: 27;Aldous Huxley27;, release_date: 27;1932-06-0127;, page_count: 268 } } },
{ index: { _index: 27;books27;, data: { name: 27;Foundation27;, author: 27;Isaac Asimov27;, release_date: 27;1951-06-0127;, page_count: 224 } } },
{ index: { _index: 27;books27;, data: { name: 27;The Giver27;, author: 27;Lois Lowry27;, release_date: 27;1993-04-2627;, page_count: 208 } } },
{ index: { _index: 27;books27;, data: { name: 27;Slaughterhouse-Five27;, author: 27;Kurt Vonnegut27;, release_date: 27;1969-06-0127;, page_count: 275 } } },
{ index: { _index: 27;books27;, data: { name: 27;The Hitchhiker\27;s Guide to the Galaxy27;, author: 27;Douglas Adams27;, release_date: 27;1979-10-1227;, page_count: 180 } } },
{ index: { _index: 27;books27;, data: { name: 27;Snow Crash27;, author: 27;Neal Stephenson27;, release_date: 27;1992-06-0127;, page_count: 470 } } },
{ index: { _index: 27;books27;, data: { name: 27;Neuromancer27;, author: 27;William Gibson27;, release_date: 27;1984-07-0127;, page_count: 271 } } },
{ index: { _index: 27;books27;, data: { name: 27;The Handmaid\27;s Tale27;, author: 27;Margaret Atwood27;, release_date: 27;1985-06-0127;, page_count: 311 } } },
{ index: { _index: 27;books27;, data: { name: 27;Starship Troopers27;, author: 27;Robert A. Heinlein27;, release_date: 27;1959-12-0127;, page_count: 335 } } },
{ index: { _index: 27;books27;, data: { name: 27;The Left Hand of Darkness27;, author: 27;Ursula K. Le Guin27;, release_date: 27;1969-06-0127;, page_count: 304 } } },
{ index: { _index: 27;books27;, data: { name: 27;The Moon is a Harsh Mistress27;, author: 27;Robert A. Heinlein27;, release_date: 27;1966-04-0127;, 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: 27;dune27;, fields: [27;name27;] } } }
puts 27;Search results27;
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 27;Updating document27;
document = { title: 27;Walkaway27;, author: 27;Cory Doctorow27;, release_date: 27;201727; }
response = client.index(index: index, body: document, refresh: true)
id = response[27;_id27;]
response = client.update(index: index, id: id, body: { doc: { release_date: 27;2017-04-2627; } })
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 27;Retrieveing multiple documents27;
response = client.search(index: index, body: { query: { match_all: {} }, size: 3, stored_fields: 27;_id27; })
ids = response[27;hits27;][27;hits27;]
ids.map { |a| a.delete(27;_score27;) }
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)[27;count27;]}"
puts 27;Deleting by query27;
response = client.delete_by_query(index: index, body: { query: { match: { author: 27;Robert A. Heinlein27; } } }, refresh: true)
puts response
puts "Count #{client.count(index: index)[27;count27;]}"
# 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 27;Deleting document27;
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 27;Deleting index27;
response = client.indices.delete(index: index)
puts response
# Deleting index
# {"acknowledged"=>true}
Migating to Elasticsearch
The first step is to add elasticsearch-ruby
in the Gemfile. After running bundle install
, the Elasticsearch Ruby client gem will be installed. If you want to test your code before fully migrating, you can initially leave the opensearch-ruby
gem there.
The next important step is going to be the client instantiation. This is going to depend on how you27;re running Elasticsearch. To keep a similar approach for these examples, I am following the steps in Download Elasticsearch and running it locally.
When running bin/elasticsearch
, Elasticsearch will start with security features automatically configured. Make sure you copy the password for the elastic user (but you can reset it by running bin/elasticsearch-reset-password -u elastic
). If you27;re following this example, make sure you stop OpenSearch before starting Elasticsearch, since they run on the same port.
At the beginning of example_code.rb
, I commented out the OpenSearch client instantiation and added the instantiation for an Elasticsearch client:
# require 27;opensearch27;
# client = OpenSearch::Client.new(
# host: 27;https://localhost:920027;,
# user: 27;admin27;,
# password: ENV[27;OPENSEARCH_INITIAL_ADMIN_PASSWORD27;]
# transport_options: { ssl: { verify: false } }
# )
require 27;elasticsearch27;
client = Elasticsearch::Client.new(
host: 27;https://localhost:920027;,
user: ENV[27;ELASTICSEARCH_USER27;],
password: ENV[27;ELASTICSEARCH_PASSWORD27;],
transport_options: { ssl: { verify: false } }
)
As you can see, the code is almost identical in this testing scenario. It will differ according to the deployment of Elasticsearch and how you decide to connect and authenticate with it. The same applies here as in OpenSearch regarding security, the option to not verify ssl is just for testing purposes and should not be used in production.
Once the client is set up, I run the code again with: bundle exec ruby example_code.rb
. And everything just works!
Debugging
Depending on the APIs your application is using, there is a possibility that you receive an error when running your code against Elasticsearch if the APIs from OpenSearch diverge. The REST APIs documentation is an essential reference for detailed information on how to use the APIs. Make sure to check the documentation for the version of Elasticsearch that you27;re using. You can also refer to the Elasticsearch::API
reference.
Some errors you may encounter from Elasticsearch could be:
ArgumentError: Required argument 27;<ARGUMENT>27; missing
- This is a Client error and it will be raised when a request is missing a required parameter.Elastic::Transport::Transport::Errors::BadRequest: [400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"request [/example/_doc] contains unrecognized parameter: [test]"}]...
This error comes from Elasticsearch and it means the client code is using a parameter that Elasticsearch doesn27;t recognize for the API being used.
The Elasticsearch client will raise errors from Elasticsearch with the detailed error message sent by the server. So for unsupported parameters or endpoints even, the error should inform you what is different.
Conclusion
As we demonstrated with this example code, the migration of a Ruby app from OpenSearch to Elasticsearch is not too complex from the Ruby side of things. You need to be aware of the versioning and any potential divergent APIs between the search engines. But for the most common actions, the main change when migrating clients is in the instantiation. They27;re both similar in that respect, but the way the host and credentials are defined varies in relation to how the Stack is being deployed. Once the client is set up, and you verify it27;s connecting to Elasticsearch, you can replace the OpenSearch client seamlessly with the Elasticsearch client.
Want to get Elastic certified? Find out when the next Elasticsearch Engineer training is running!
Elasticsearch is packed with new features to help you build the best search solutions for your use case. Dive into our sample notebooks to learn more, start a free cloud trial, or try Elastic on your local machine now.