博客

如何将 Ruby 应用程序从 OpenSearch 迁移到 Elasticsearch

将 Ruby 代码库从 OpenSearch 客户端迁移到 Elasticsearch 客户端的指南。

OpenSearch Ruby 客户端是从版本7.x 的 Elasticsearch Ruby 客户端分叉而来,因此代码库相对相似。这意味着在将 Ruby 代码库从 OpenSearch 迁移到 Elasticsearch 时,各客户端库中的代码看起来会非常熟悉。在这篇博文中,我将展示一个使用 OpenSearch 的 Ruby 应用程序示例,以及将代码迁移到 Elasticsearch 的步骤。

这两个客户端都是根据流行的 Apache License 2.0 发布的,因此都是开源和免费软件。Elasticsearch 的许可证最近进行了更新,自 8.16 版起,Elasticsearch 和 Kibana 的核心内容均根据 OSI 批准的开源许可证 AGPL 发布。

迁移 Ruby 应用程序时考虑 Elasticsearch 版本

迁移时的一个考虑因素是将使用哪个版本的 Elasticsearch。我们建议使用最新的稳定版本,即8.17.0 。Elasticsearch Ruby 客户端的次版本与 Elasticsearch 的次版本一致。因此,对于 Elasticsearch8.17.x ,可以使用版本为8.17.x 的 Ruby gem。

OpenSearch 源自 Elasticsearch 7.10.2。因此,应用程序接口可能已经发生了变化,不同的功能可以在其中任何一个上使用。但这不在本篇文章的讨论范围内,我只想在一个示例应用程序中了解最常见的操作。

对于 Ruby on Rails,可以使用官方 Elasticsearch 客户端或Rails 集成库。我们建议分别迁移到 Elasticsearch 和客户端的最新稳定版本。elasticsearch-rails gem 版本8.0.0 支持 Rails6.17.07.1 以及 Elasticsearch8.x

代码

在本例中,我按照步骤从 tar 包中安装 OpenSearch。下载并解压 tar 包后,我需要设置一个初始管理员密码,稍后将使用该密码实例化客户端。

我创建了一个带有Gemfile 的目录,看起来像这样:

source 'https://rubygems.org'

gem 'opensearch-ruby'

运行bundle install 后,我的项目安装了 gem。这就安装了3.4.0 版本的 opensearch-ruby,而我运行的 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 环境变量设置为强密码。按照从压缩包中安装 OpenSearch 的步骤,我在控制台中导出了变量,现在我的 Ruby 脚本可以使用它了。

使用cluster.health API 是确保客户端连接 OpenSearch 的一个简单 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 会自动配置安全功能。确保复制了弹性用户的密码(但可以通过运行bin/elasticsearch-reset-password -u elastic 重置)。如果您遵循这个示例,请确保在启动 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 的部署以及您决定与之连接和验证的方式而有所不同。这里与 OpenSearch 的安全问题相同,不验证 ssl 的选项仅用于测试目的,不应在生产中使用。

设置好客户端后,我再次运行代码,使用:bundle exec ruby example_code.rb.一切都很顺利!

调试迁移问题

根据您的应用程序使用的 API,如果 OpenSearch 的 API 与 Elasticsearch 的不同,您在针对 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,这意味着客户端代码使用了 Elasticsearch 无法识别的 API 参数。

Elasticsearch 客户端会从 Elasticsearch 引发错误,并附带服务器发送的详细错误信息。因此,即使是不支持的参数或端点,错误也会告诉你有什么不同。

结论

正如我们在本示例代码中演示的那样,从 Ruby 端将 Ruby 应用程序从 OpenSearch 迁移到 Elasticsearch 并不复杂。您需要了解搜索引擎之间的版本和任何潜在的不同应用程序接口。但就最常见的操作而言,迁移客户端时的主要变化在于实例化。它们在这方面都很相似,但主机和凭证的定义方式因堆栈的部署方式而异。一旦设置好客户端,并验证它连接到 Elasticsearch,就可以用 Elasticsearch 客户端无缝替换 OpenSearch 客户端。

相关内容

在 Logstash 中使用 Ruby 脚本

Dai Sugimori

介绍用于 Elasticsearch Ruby 客户端的 ES|QL 查询生成器

Fernando Briano

准备好打造最先进的搜索体验了吗?

足够先进的搜索不是一个人的努力就能实现的。Elasticsearch 由数据科学家、ML 操作员、工程师以及更多和您一样对搜索充满热情的人提供支持。让我们联系起来,共同打造神奇的搜索体验,让您获得想要的结果。

亲自试用