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