diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index f6daa55..fb8e38f 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -4,7 +4,7 @@ on: pull_request: jobs: - test: + elasticsearch: runs-on: ${{matrix.os}}-latest @@ -37,3 +37,47 @@ jobs: run: bundle install - name: Run tests run: bundle exec rspec + + opensearch: + + runs-on: ${{matrix.os}}-latest + # services: + # opensearch: + # image: opensearchproject/opensearch:2 + # ports: + # - 9200:9200 + # env: + # discovery.type: single-node + # OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m" + # DISABLE_INSTALL_DEMO_CONFIG: true + # DISABLE_SECURITY_PLUGIN: true + # bootstrap.memory_lock: true + + strategy: + matrix: + os: ['ubuntu'] + ruby: ['3.1'] + opensearch: ['2.12.0'] + + env: + BACKEND: opensearch + OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m" + DISABLE_INSTALL_DEMO_CONFIG: true + + steps: + - uses: actions/checkout@v4 + - name: Set up OpenSearch ${{ matrix.opensearch }} + uses: theablefew/opensearch-github-actions/opensearch@main + with: + version: ${{ matrix.opensearch }} + security-disabled: true + + - name: Set up Ruby ${{ matrix.ruby }} + uses: ruby/setup-ruby@ec02537da5712d66d4d50a0f33b7eb52773b5ed1 + with: + ruby-version: ${{ matrix.ruby }} + + - name: Install dependencies + run: bundle install + - name: Run tests + run: bundle exec rspec \ No newline at end of file diff --git a/.gitignore b/.gitignore index 307f973..96e6bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ /tmp/ PLAN.md .irb-history -Gemfile.lock \ No newline at end of file +Gemfile.lock +.gem diff --git a/README.md b/README.md index cea0873..980b04b 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,10 @@ After checking out the repo, run `bin/setup` to install dependencies. You can al > Full documentation on [Elasticsearch Query DSL and Aggregation options](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence) ## Testing +
+Elasticsearch + + ``` docker-compose up elasticsearch ``` @@ -162,6 +166,21 @@ docker-compose up elasticsearch bundle exec rspec ``` +
+ +
+Opensearch + + +``` +docker-compose up opensearch +``` + +``` +ENV['BACKEND']=opensearch bundle rspec +``` +
+ ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/theablefew/stretchy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/theablefew/stretchy/blob/master/CODE_OF_CONDUCT.md). diff --git a/lib/stretchy.rb b/lib/stretchy.rb index 8254707..d7528e1 100644 --- a/lib/stretchy.rb +++ b/lib/stretchy.rb @@ -24,9 +24,25 @@ class QueryOptionMissing < StandardError; end class Configuration attr_accessor :client + attr_accessor :opensearch def initialize @client = Elasticsearch::Client.new url: 'http://localhost:9200' + @opensearch = false + end + + def client=(client) + @client = client + self.opensearch = true if @client.class.name =~ /OpenSearch/ + end + + def opensearch=(bool) + @opensearch = bool + OpenSearchCompatibility.opensearch_patch! if bool + end + + def opensearch? + @opensearch end end @@ -45,9 +61,10 @@ def configure end end - end + + loader = Zeitwerk::Loader.new loader.tag = File.basename(__FILE__, ".rb") loader.inflector = Zeitwerk::GemInflector.new(__FILE__) diff --git a/lib/stretchy/open_search_compatibility.rb b/lib/stretchy/open_search_compatibility.rb new file mode 100644 index 0000000..2abc689 --- /dev/null +++ b/lib/stretchy/open_search_compatibility.rb @@ -0,0 +1,86 @@ +module Stretchy + module OpenSearchCompatibility + extend ActiveSupport::Concern + + # Patches the Elasticsearch::Persistence::Repository::Search module to remove the + # document type from the request for compatability with OpenSearch + def self.opensearch_patch! + patch = Module.new do + def search(query_or_definition, options={}) + request = { index: index_name } + + if query_or_definition.respond_to?(:to_hash) + request[:body] = query_or_definition.to_hash + elsif query_or_definition.is_a?(String) + request[:q] = query_or_definition + else + raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" + + " -- #{query_or_definition.class} given." + end + + Elasticsearch::Persistence::Repository::Response::Results.new(self, client.search(request.merge(options))) + end + + def count(query_or_definition=nil, options={}) + query_or_definition ||= { query: { match_all: {} } } + request = { index: index_name} + + if query_or_definition.respond_to?(:to_hash) + request[:body] = query_or_definition.to_hash + elsif query_or_definition.is_a?(String) + request[:q] = query_or_definition + else + raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" + + " -- #{query_or_definition.class} given." + end + + client.count(request.merge(options))['count'] + end + end + + store = Module.new do + def save(document, options={}) + serialized = serialize(document) + id = __get_id_from_document(serialized) + request = { index: index_name, + id: id, + body: serialized } + client.index(request.merge(options)) + end + + + def update(document_or_id, options = {}) + if document_or_id.is_a?(String) || document_or_id.is_a?(Integer) + id = document_or_id + body = options + else + document = serialize(document_or_id) + id = __extract_id_from_document(document) + if options[:script] + body = options + else + body = { doc: document }.merge(options) + end + end + client.update(index: index_name, id: id, body: body) + end + + def delete(document_or_id, options = {}) + if document_or_id.is_a?(String) || document_or_id.is_a?(Integer) + id = document_or_id + else + serialized = serialize(document_or_id) + id = __get_id_from_document(serialized) + end + client.delete({ index: index_name, id: id }.merge(options)) + end + end + + + ::Elasticsearch::Persistence::Repository.send(:include, patch) + ::Elasticsearch::Persistence::Repository.send(:include, store) + end + + + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9c4db8b..f994e49 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,10 +19,30 @@ end require 'stretchy' require 'active_support/core_ext' -# require 'elasticsearch/persistence' + +backend = ENV['BACKEND'] || 'elasticsearch' + +if backend == 'opensearch' + require 'opensearch' + Stretchy.configure do |config| + config.client = OpenSearch::Client.new( + host: 'http://localhost:9200', + user: 'admin', + password: 'admin', + transport_options: { ssl: { verify: false } } # For testing only. Use certificate for validation. + ) + end +else + # Configure for Elasticsearch +end + RSpec.configure do |config| + if ENV['BACKEND'] == 'opensearch' + config.filter_run_excluding opensearch_incompatible: true + end + # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. diff --git a/spec/stretchy/aggregations_spec.rb b/spec/stretchy/aggregations_spec.rb index 0831f0f..f704042 100644 --- a/spec/stretchy/aggregations_spec.rb +++ b/spec/stretchy/aggregations_spec.rb @@ -314,7 +314,7 @@ end context 'boxplot' do - it 'returns boxplot statistics' do + it 'returns boxplot statistics', opensearch_incompatible: true do boxplot_agg = described_class.size(0).aggregation(:age_stats, boxplot: {field: :age}) expect(boxplot_agg.aggregations.age_stats).to be_a(Hash) expect(boxplot_agg.aggregations.age_stats).to include(:q1, :q3, :min, :max) @@ -452,7 +452,7 @@ end context 'rate' do - it 'returns rate' do + it 'returns rate', opensearch_incompatible: true do rate_agg = described_class.size(0).aggregation(:by_date, date_histogram: { field: :created_at, @@ -502,7 +502,7 @@ end context 'string_stats' do - it 'returns string statistics' do + it 'returns string statistics', opensearch_incompatible: true do string_stats_agg = described_class.size(0).aggregation(:position_stats, string_stats: {field: 'position.name'}) expect(string_stats_agg.aggregations.position_stats).to be_a(Hash) expect(string_stats_agg.aggregations.position_stats).to include(:count, :min_length, :max_length, :avg_length, :entropy) @@ -520,7 +520,7 @@ end context 't_test' do - it 'returns t-test statistics' do + it 'returns t-test statistics', opensearch_incompatible: true do t_test_agg = described_class.size(0).aggregation(:t_test, t_test: { a: {field: :income}, @@ -555,7 +555,7 @@ end context 'top_metrics' do - it 'returns top metrics' do + it 'returns top metrics', opensearch_incompatible: true do top_metrics_agg = described_class.size(0).aggregation(:top_metrics, top_metrics: { metrics: [{field: :income}, {field: :age}], diff --git a/spec/stretchy/instrumentation_spec.rb b/spec/stretchy/instrumentation_spec.rb index 468aec6..abacc5c 100644 --- a/spec/stretchy/instrumentation_spec.rb +++ b/spec/stretchy/instrumentation_spec.rb @@ -10,13 +10,13 @@ "(#{(finish.to_time - start.to_time).round(2)}ms)", Stretchy::Utils.to_curl(payload[:klass].constantize, payload[:search]) ].join(" ") - expect(message).to eq("Post(0.0ms) curl -XGET 'http://localhost:9200/posts/_search' -d '{\"query\":{\"term\":{\"title\":\"hello\"}}}'") + expect(message).to match(/Post\(0.0ms\) curl -XGET 'https?:\/\/localhost:9200\/posts\/_search' -d '\{\"query\":\{\"term\":\{\"title\":\"hello\"\}\}\}'/) end ActiveSupport::Notifications.unsubscribe(subscription) end it 'converts the payload to a curl command' do curl = Stretchy::Utils.to_curl(Post, {index: Post.index_name, body: {query: {term: {title: "hello"}}}}) - expect(curl).to eq("curl -XGET 'http://localhost:9200/posts/_search' -d '{\"query\":{\"term\":{\"title\":\"hello\"}}}'\n") + expect(curl).to match(%r{curl -XGET 'https?://localhost:9200/posts/_search' -d '\{"query":\{"term":\{"title":"hello"\}\}\}'\n}) end end \ No newline at end of file diff --git a/stretchy-model.gemspec b/stretchy-model.gemspec index dd9afa1..fd26457 100644 --- a/stretchy-model.gemspec +++ b/stretchy-model.gemspec @@ -44,6 +44,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec", "~> 3.9" spec.add_development_dependency "simplecov", "~> 0.21.2" spec.add_development_dependency "yard", "~> 0.9.36" + spec.add_development_dependency "opensearch-ruby", "~> 3.0" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html end