Blog

Como migrar seu aplicativo Ruby do OpenSearch para o Elasticsearch

Um guia para migrar um código-fonte Ruby do cliente OpenSearch para o cliente Elasticsearch.

O cliente Ruby do OpenSearch foi derivado do cliente Ruby do Elasticsearch na versão 7.x, portanto, as bases de código são relativamente semelhantes. Isso significa que, ao migrar um código-fonte Ruby do OpenSearch para o Elasticsearch, o código das respectivas bibliotecas de cliente parecerá muito familiar. Neste post do blog, vou mostrar um exemplo de aplicativo Ruby que usa o OpenSearch e os passos para migrar esse código para o Elasticsearch.

Ambos os clientes são distribuídos sob a popular Licença Apache 2.0, sendo, portanto, software livre e de código aberto. A licença do Elasticsearch foi atualizada recentemente e o núcleo do Elasticsearch e do Kibana são publicados sob a licença de código aberto AGPL, aprovada pela OSI, desde a versão 8.16.

Considerando a versão do Elasticsearch ao migrar um aplicativo Ruby.

Um aspecto a considerar na migração é qual versão do Elasticsearch será utilizada. Recomendamos usar a versão estável mais recente, que no momento em que este texto foi escrito é 8.17.0. As versões secundárias do cliente Ruby do Elasticsearch seguem as versões secundárias do Elasticsearch. Então, para o Elasticsearch 8.17.x, você pode usar a versão 8.17.x da gem Ruby.

O OpenSearch foi derivado do Elasticsearch 7.10.2. Assim, as APIs podem ter mudado e recursos diferentes podem ser usados em qualquer uma delas. Mas isso está fora do escopo desta postagem, e vou analisar apenas as operações mais comuns em um aplicativo de exemplo.

Para Ruby on Rails, você pode usar o cliente oficial do Elasticsearch ou as bibliotecas de integração do Rails. Recomendamos a migração para a versão estável mais recente do Elasticsearch e do cliente, respectivamente. A versão elasticsearch-rails da gem 8.0.0 suporta Rails 6.1, 7.0 e 7.1 e Elasticsearch 8.x.

O código

Neste exemplo, segui os passos para instalar o OpenSearch a partir de um arquivo tar. Após baixar e extrair o arquivo tar, precisei definir uma senha de administrador inicial, que usarei posteriormente para instanciar o cliente.

Criei um diretório com um Gemfile que se parece com isto:

source 'https://rubygems.org'

gem 'opensearch-ruby'

Após executar bundle install, a gem é instalada para o meu projeto. Esta versão do opensearch-ruby instalada é 3.4.0 e a versão do OpenSearch que estou executando é 2.18.0. Eu escrevi o código em um arquivo example_code.rb no mesmo diretório. O código inicial neste arquivo é a instanciação de um cliente OpenSearch:

require 'opensearch'

client = OpenSearch::Client.new(
  host: 'https://localhost:9200',
  user: 'admin',
  password: ENV['OPENSEARCH_INITIAL_ADMIN_PASSWORD'],
  transport_options: { ssl: { verify: false } }
)

O parâmetro de opção de transporte ssl: { verify: false} está sendo passado conforme o guia do usuário para facilitar os testes. Em produção, isso deve ser configurado dependendo da implantação do OpenSearch.

A partir da versão 2.12.0 do OpenSearch, a variável de ambiente OPENSEARCH_INITIAL_ADMIN_PASSWORD deve ser definida com uma senha forte ao executar o script de instalação. Seguindo os passos para instalar o OpenSearch a partir de um arquivo tar.gz, exportei a variável no meu console e agora ela está disponível para o meu script Ruby.

Uma API simples para garantir que o cliente esteja se conectando ao OpenSearch é usar a API cluster.health :

puts 'HEALTH:'
pp client.cluster.health

E de fato funciona:

$ be ruby example_code.rb
HEALTH:
{"cluster_name"=>"opensearch",
"status"=>"yellow",
 "timed_out"=>false,
 "number_of_nodes"=>1,
 "number_of_data_nodes"=>1,

Testei alguns dos exemplos comuns que temos na documentação do cliente Ruby do Elasticsearch e eles funcionaram conforme o esperado:

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}

Migrando um aplicativo Ruby para o Elasticsearch

O primeiro passo é adicionar elasticsearch-ruby no Gemfile. Após executar bundle install, a gem do cliente Ruby do Elasticsearch será instalada. Se você quiser testar seu código antes de migrar completamente, você pode inicialmente deixar a gem opensearch-ruby lá.

O próximo passo importante será a instanciação do cliente. Isso vai depender de como você está executando o Elasticsearch. Para manter uma abordagem semelhante nestes exemplos, estou seguindo os passos descritos em "Baixar o Elasticsearch e executá-lo localmente".

Ao executar bin/elasticsearch, o Elasticsearch será iniciado com recursos de segurança configurados automaticamente. Certifique-se de copiar a senha do usuário elastic (mas você pode redefini-la executando bin/elasticsearch-reset-password -u elastic). Se você estiver seguindo este exemplo, certifique-se de parar o OpenSearch antes de iniciar o Elasticsearch, pois ambos são executados na mesma porta.

No início de example_code.rb, comentei a instanciação do cliente OpenSearch e adicionei a instanciação para um cliente 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 } }
)

Como você pode ver, o código é quase idêntico neste cenário de teste. Isso varia de acordo com a implementação do Elasticsearch e a forma como você decide se conectar e autenticar com ele. O mesmo se aplica aqui ao OpenSearch em relação à segurança: a opção de não verificar o SSL destina-se apenas a fins de teste e não deve ser usada em produção.

Depois que o cliente estiver configurado, eu executo o código novamente com: bundle exec ruby example_code.rb. E tudo simplesmente funciona!

Depurando problemas de migração

Dependendo das APIs que seu aplicativo utiliza, existe a possibilidade de você receber um erro ao executar seu código no Elasticsearch se as APIs do OpenSearch divergirem. A documentação das APIs REST é uma referência essencial para informações detalhadas sobre como usar as APIs. Certifique-se de consultar a documentação da versão do Elasticsearch que você está usando. Você também pode consultar a referência Elasticsearch::API .

Alguns erros que você pode encontrar no Elasticsearch podem ser:

  • ArgumentError: Required argument '<ARGUMENT>' missing - Este é um erro do cliente e será gerado quando uma solicitação não contiver um parâmetro obrigatório.

  • Elastic::Transport::Transport::Errors::BadRequest: [400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"request [/example/_doc] contains unrecognized parameter: [test]"}]... Esse erro vem do Elasticsearch e significa que o código do cliente está usando um parâmetro que o Elasticsearch não reconhece para a API que está sendo usada.

O cliente Elasticsearch irá gerar erros do Elasticsearch com a mensagem de erro detalhada enviada pelo servidor. Assim, mesmo para parâmetros ou endpoints não suportados, o erro deve informar qual é a diferença.

Conclusão

Como demonstramos com este exemplo de código, a migração de um aplicativo Ruby do OpenSearch para o Elasticsearch não é muito complexa do ponto de vista do Ruby. É preciso estar ciente das diferenças de versão e de quaisquer APIs potencialmente divergentes entre os mecanismos de busca. Mas, para as ações mais comuns, a principal mudança na migração de clientes está na instanciação. Nesse aspecto, ambos são semelhantes, mas a forma como o host e as credenciais são definidos varia de acordo com a maneira como o Stack está sendo implantado. Após configurar o cliente e verificar se ele está se conectando ao Elasticsearch, você pode substituir o cliente OpenSearch pelo cliente Elasticsearch sem problemas.

Conteúdo relacionado

Scripting em Ruby no Logstash

Dai Sugimori

Apresentamos o construtor de consultas ES|QL para o cliente Ruby do Elasticsearch.

Fernando Briano

Pronto para criar buscas de última geração?

Uma pesquisa suficientemente avançada não se consegue apenas com o esforço de uma só pessoa. O Elasticsearch é impulsionado por cientistas de dados, especialistas em operações de aprendizado de máquina, engenheiros e muitos outros que são tão apaixonados por buscas quanto você. Vamos nos conectar e trabalhar juntos para construir a experiência de busca mágica que lhe trará os resultados desejados.

Experimente você mesmo(a)