stretchy-model 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +66 -10
- data/Rakefile +92 -0
- data/lib/stretchy/attributes/transformers/keyword_transformer.rb +85 -0
- data/lib/stretchy/attributes/type/keyword.rb +11 -0
- data/lib/stretchy/attributes.rb +10 -0
- data/lib/stretchy/open_search_compatibility.rb +86 -0
- data/lib/stretchy/querying.rb +6 -5
- data/lib/stretchy/relation.rb +3 -3
- data/lib/stretchy/relations/aggregation_methods.rb +758 -0
- data/lib/stretchy/relations/finder_methods.rb +21 -3
- data/lib/stretchy/relations/merger.rb +6 -6
- data/lib/stretchy/relations/query_builder.rb +23 -9
- data/lib/stretchy/relations/query_methods.rb +10 -36
- data/lib/stretchy/utils.rb +21 -0
- data/lib/stretchy/version.rb +1 -3
- data/lib/stretchy.rb +37 -8
- metadata +50 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7dff610329ad21128c58429c6f9d08b62138a7fad9adb0f610984bd744db1cf
|
4
|
+
data.tar.gz: 915256c1b413dd34d4777097b878f0c2c7886342b5d8dcfedc2159a405b3bdf2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66f98b878a8e78d9d79b53f05edf89a8d4a61d4fd3a5b98d93ef349491c7c87f36d4cadb857b8be5e9e992308609c22d1abc45703eb39b95e78240d87c6ae081
|
7
|
+
data.tar.gz: 0eabc3b84d0aecb0f8051bf53238ec6da5d431c7ff5d0f7c3be20888a20252504e3324c7b558fe455d90c2cf0db827b1d0440e6d03d211569ed8ed7d60dade01
|
data/README.md
CHANGED
@@ -98,16 +98,6 @@ Model.bulk_in_batches(records, size: 100) do |batch|
|
|
98
98
|
end
|
99
99
|
```
|
100
100
|
|
101
|
-
|
102
|
-
## Instrumentation
|
103
|
-
```ruby
|
104
|
-
Blanket.first
|
105
|
-
```
|
106
|
-
|
107
|
-
```sh
|
108
|
-
Blanket (6.322ms) curl -X GET 'http://localhost:9200/blankets/_search?size=1' -d '{"sort":{"date":"desc"}}'
|
109
|
-
```
|
110
|
-
|
111
101
|
## Installation
|
112
102
|
|
113
103
|
Install the gem and add to the application's Gemfile by executing:
|
@@ -118,6 +108,38 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
118
108
|
|
119
109
|
$ gem install stretchy-model
|
120
110
|
|
111
|
+
<details>
|
112
|
+
<summary>Rails Configuration</summary>
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
```sh
|
117
|
+
rails credentials:edit
|
118
|
+
```
|
119
|
+
|
120
|
+
#### Add elasticsearch credentials
|
121
|
+
```yaml
|
122
|
+
elasticsearch:
|
123
|
+
url: localhost:9200
|
124
|
+
|
125
|
+
# or opensearch
|
126
|
+
# opensearch:
|
127
|
+
# host: https://localhost:9200
|
128
|
+
# user: admin
|
129
|
+
# password: admin
|
130
|
+
```
|
131
|
+
|
132
|
+
#### Create an initializer
|
133
|
+
<p><sub><em>config/initializers/stretchy.rb</em></sub></p>
|
134
|
+
|
135
|
+
```ruby {file=config/initializers/stretchy.rb}
|
136
|
+
Stretchy.configure do |config|
|
137
|
+
config.client = Elasticsearch::Client.new url: Rails.application.credentials.elasticsearch.url, log: true
|
138
|
+
end
|
139
|
+
```
|
140
|
+
</details>
|
141
|
+
|
142
|
+
|
121
143
|
## Development
|
122
144
|
|
123
145
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -128,6 +150,25 @@ After checking out the repo, run `bin/setup` to install dependencies. You can al
|
|
128
150
|
> Full documentation on [Elasticsearch Query DSL and Aggregation options](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence)
|
129
151
|
|
130
152
|
## Testing
|
153
|
+
<details>
|
154
|
+
<summary>Act</summary>
|
155
|
+
|
156
|
+
Run github action workflow locally
|
157
|
+
|
158
|
+
```sh
|
159
|
+
brew install act --HEAD
|
160
|
+
```
|
161
|
+
|
162
|
+
```sh
|
163
|
+
act -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:runner-latest
|
164
|
+
```
|
165
|
+
|
166
|
+
</details>
|
167
|
+
|
168
|
+
<details>
|
169
|
+
<summary>Elasticsearch</summary>
|
170
|
+
|
171
|
+
|
131
172
|
```
|
132
173
|
docker-compose up elasticsearch
|
133
174
|
```
|
@@ -136,6 +177,21 @@ docker-compose up elasticsearch
|
|
136
177
|
bundle exec rspec
|
137
178
|
```
|
138
179
|
|
180
|
+
</details>
|
181
|
+
|
182
|
+
<details>
|
183
|
+
<summary>Opensearch</summary>
|
184
|
+
|
185
|
+
|
186
|
+
```
|
187
|
+
docker-compose up opensearch
|
188
|
+
```
|
189
|
+
|
190
|
+
```
|
191
|
+
ENV['BACKEND']=opensearch bundle rspec
|
192
|
+
```
|
193
|
+
</details>
|
194
|
+
|
139
195
|
## Contributing
|
140
196
|
|
141
197
|
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).
|
data/Rakefile
CHANGED
@@ -2,3 +2,95 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
task default: %i[]
|
5
|
+
|
6
|
+
require 'octokit'
|
7
|
+
require 'versionomy'
|
8
|
+
require 'rainbow'
|
9
|
+
|
10
|
+
def determine_current_version
|
11
|
+
# Load current version
|
12
|
+
load 'lib/stretchy/version.rb'
|
13
|
+
current_version = Versionomy.parse(Stretchy::VERSION)
|
14
|
+
end
|
15
|
+
|
16
|
+
def determine_new_version(version)
|
17
|
+
# Load current version
|
18
|
+
current_version = determine_current_version
|
19
|
+
|
20
|
+
# Determine new version
|
21
|
+
case version.to_sym
|
22
|
+
when :major
|
23
|
+
current_version.bump(:major)
|
24
|
+
when :minor
|
25
|
+
current_version.bump(:minor)
|
26
|
+
when :patch
|
27
|
+
current_version.bump(:tiny)
|
28
|
+
else
|
29
|
+
version =~ /\Av?\d+\.\d+\.\d+\z/ ? Versionomy.parse(version).to_s.gsub(/v/,'') : current_version
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_release_branch(new_version, base_branch)
|
34
|
+
system("git stash save 'Changes before creating release branch'")
|
35
|
+
system("git fetch origin #{base_branch}")
|
36
|
+
branch_name = "release/v#{new_version}"
|
37
|
+
system("git checkout -b #{branch_name} #{base_branch}")
|
38
|
+
branch_name
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_version_file(new_version)
|
42
|
+
# Update lib/stretchy/version.rb
|
43
|
+
File.open('lib/stretchy/version.rb', 'w') do |file|
|
44
|
+
file.puts "module Stretchy\n VERSION = '#{new_version}'\nend"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def commit_and_push_changes(new_version, branch_name)
|
49
|
+
system("git add lib/stretchy/version.rb")
|
50
|
+
system("git commit -m 'Bump version to v#{new_version}'")
|
51
|
+
system("git tag v#{new_version}")
|
52
|
+
system("git push origin #{branch_name} --tags -f")
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_pull_request(new_version, base_branch, branch_name)
|
56
|
+
# Create a pull request
|
57
|
+
client = Octokit::Client.new(access_token: ENV['GH_TOKEN'])
|
58
|
+
client.create_pull_request('theablefew/stretchy', base_branch, branch_name, "Release v#{new_version}")
|
59
|
+
end
|
60
|
+
|
61
|
+
namespace :publish do
|
62
|
+
desc "Create a release"
|
63
|
+
task :release, [:version, :base_branch] do |t, args|
|
64
|
+
args.with_defaults(version: :patch, base_branch: 'main')
|
65
|
+
version = args[:version]
|
66
|
+
base_branch = args[:base_branch]
|
67
|
+
|
68
|
+
old_version = determine_current_version
|
69
|
+
new_version = determine_new_version(version)
|
70
|
+
puts Rainbow("Bumping version from #{old_version} to #{new_version}").green
|
71
|
+
branch_name = create_release_branch(new_version, base_branch)
|
72
|
+
begin
|
73
|
+
update_version_file(new_version)
|
74
|
+
commit_and_push_changes(new_version, branch_name)
|
75
|
+
create_pull_request(new_version, base_branch, branch_name)
|
76
|
+
rescue => e
|
77
|
+
puts "Error: #{e.message}"
|
78
|
+
puts "Rolling back changes"
|
79
|
+
system("git tag -d v#{new_version}")
|
80
|
+
system("git checkout #{base_branch}")
|
81
|
+
system("git branch -D #{branch_name}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
task :major do
|
86
|
+
Rake::Task['publish:release'].invoke('major')
|
87
|
+
end
|
88
|
+
|
89
|
+
task :minor do
|
90
|
+
Rake::Task['publish:release'].invoke('minor')
|
91
|
+
end
|
92
|
+
|
93
|
+
task :patch do
|
94
|
+
Rake::Task['publish:release'].invoke('patch')
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Attributes
|
3
|
+
module Transformers
|
4
|
+
class KeywordTransformer
|
5
|
+
|
6
|
+
KEYWORD_AGGREGATION_KEYS = [:terms, :rare_terms, :significant_terms, :cardinality, :string_stats]
|
7
|
+
|
8
|
+
attr_reader :attribute_types
|
9
|
+
|
10
|
+
def initialize(attribute_types)
|
11
|
+
@attribute_types = attribute_types
|
12
|
+
end
|
13
|
+
|
14
|
+
def cast_value_keys
|
15
|
+
values.transform_values do |value|
|
16
|
+
case value
|
17
|
+
when Array
|
18
|
+
value.map { |item| transform_keys_for_item(item) }
|
19
|
+
when Hash
|
20
|
+
transform_keys_for_item(value)
|
21
|
+
else
|
22
|
+
value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def keyword?(arg)
|
28
|
+
attr = @attribute_types[arg.to_s]
|
29
|
+
return false unless attr
|
30
|
+
attr.is_a?(Stretchy::Attributes::Type::Keyword)
|
31
|
+
end
|
32
|
+
|
33
|
+
def protected?(arg)
|
34
|
+
return false if arg.nil?
|
35
|
+
Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS.include?(arg.to_sym)
|
36
|
+
end
|
37
|
+
|
38
|
+
def transform(item, *ignore)
|
39
|
+
item.each_with_object({}) do |(k, v), new_item|
|
40
|
+
if ignore && ignore.include?(k)
|
41
|
+
new_item[k] = v
|
42
|
+
next
|
43
|
+
end
|
44
|
+
new_key = (!protected?(k) && keyword?(k)) ? "#{k}.keyword" : k
|
45
|
+
|
46
|
+
new_value = v
|
47
|
+
|
48
|
+
if new_value.is_a?(Hash)
|
49
|
+
new_value = transform(new_value)
|
50
|
+
elsif new_value.is_a?(Array)
|
51
|
+
new_value = new_value.map { |i| i.is_a?(Hash) ? transform(i) : i }
|
52
|
+
elsif new_value.is_a?(String) || new_value.is_a?(Symbol)
|
53
|
+
new_value = "#{new_value}.keyword" if keyword?(new_value)
|
54
|
+
end
|
55
|
+
|
56
|
+
new_item[new_key] = new_value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# If terms are used, we assume that the field is a keyword field
|
61
|
+
# and append .keyword to the field name
|
62
|
+
# {terms: {field: 'gender'}}
|
63
|
+
# or nested aggs
|
64
|
+
# {terms: {field: 'gender'}, aggs: {name: {terms: {field: 'position.name'}}}}
|
65
|
+
# should be converted to
|
66
|
+
# {terms: {field: 'gender.keyword'}, aggs: {name: {terms: {field: 'position.name.keyword'}}}}
|
67
|
+
# {date_histogram: {field: 'created_at', interval: 'day'}}
|
68
|
+
# TODO: There may be cases where we don't want to add .keyword to the field and there should be a way to override this
|
69
|
+
def assume_keyword_field(args={}, parent_match=false)
|
70
|
+
if args.is_a?(Hash)
|
71
|
+
args.each do |k, v|
|
72
|
+
if v.is_a?(Hash)
|
73
|
+
assume_keyword_field(v, KEYWORD_AGGREGATION_FIELDS.include?(k))
|
74
|
+
else
|
75
|
+
next unless v.is_a?(String) || v.is_a?(Symbol)
|
76
|
+
args[k] = ([:field, :fields].include?(k.to_sym) && v !~ /\.keyword$/ && parent_match) ? "#{v}.keyword" : v.to_s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Attributes
|
3
|
+
|
4
|
+
def self.register!
|
5
|
+
ActiveModel::Type.register(:array, ActiveModel::Type::Array)
|
6
|
+
ActiveModel::Type.register(:hash, ActiveModel::Type::Hash)
|
7
|
+
ActiveModel::Type.register(:keyword, Stretchy::Attributes::Type::Keyword)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module OpenSearchCompatibility
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Patches the Elasticsearch::Persistence::Repository::Search module to remove the
|
6
|
+
# document type from the request for compatability with OpenSearch
|
7
|
+
def self.opensearch_patch!
|
8
|
+
patch = Module.new do
|
9
|
+
def search(query_or_definition, options={})
|
10
|
+
request = { index: index_name }
|
11
|
+
|
12
|
+
if query_or_definition.respond_to?(:to_hash)
|
13
|
+
request[:body] = query_or_definition.to_hash
|
14
|
+
elsif query_or_definition.is_a?(String)
|
15
|
+
request[:q] = query_or_definition
|
16
|
+
else
|
17
|
+
raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" +
|
18
|
+
" -- #{query_or_definition.class} given."
|
19
|
+
end
|
20
|
+
|
21
|
+
Elasticsearch::Persistence::Repository::Response::Results.new(self, client.search(request.merge(options)))
|
22
|
+
end
|
23
|
+
|
24
|
+
def count(query_or_definition=nil, options={})
|
25
|
+
query_or_definition ||= { query: { match_all: {} } }
|
26
|
+
request = { index: index_name}
|
27
|
+
|
28
|
+
if query_or_definition.respond_to?(:to_hash)
|
29
|
+
request[:body] = query_or_definition.to_hash
|
30
|
+
elsif query_or_definition.is_a?(String)
|
31
|
+
request[:q] = query_or_definition
|
32
|
+
else
|
33
|
+
raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" +
|
34
|
+
" -- #{query_or_definition.class} given."
|
35
|
+
end
|
36
|
+
|
37
|
+
client.count(request.merge(options))['count']
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
store = Module.new do
|
42
|
+
def save(document, options={})
|
43
|
+
serialized = serialize(document)
|
44
|
+
id = __get_id_from_document(serialized)
|
45
|
+
request = { index: index_name,
|
46
|
+
id: id,
|
47
|
+
body: serialized }
|
48
|
+
client.index(request.merge(options))
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def update(document_or_id, options = {})
|
53
|
+
if document_or_id.is_a?(String) || document_or_id.is_a?(Integer)
|
54
|
+
id = document_or_id
|
55
|
+
body = options
|
56
|
+
else
|
57
|
+
document = serialize(document_or_id)
|
58
|
+
id = __extract_id_from_document(document)
|
59
|
+
if options[:script]
|
60
|
+
body = options
|
61
|
+
else
|
62
|
+
body = { doc: document }.merge(options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
client.update(index: index_name, id: id, body: body)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(document_or_id, options = {})
|
69
|
+
if document_or_id.is_a?(String) || document_or_id.is_a?(Integer)
|
70
|
+
id = document_or_id
|
71
|
+
else
|
72
|
+
serialized = serialize(document_or_id)
|
73
|
+
id = __get_id_from_document(serialized)
|
74
|
+
end
|
75
|
+
client.delete({ index: index_name, id: id }.merge(options))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
::Elasticsearch::Persistence::Repository.send(:include, patch)
|
81
|
+
::Elasticsearch::Persistence::Repository.send(:include, store)
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
data/lib/stretchy/querying.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
module Stretchy
|
2
2
|
module Querying
|
3
3
|
delegate :first, :first!, :last, :last!, :exists?, :has_field, :any?, :many?, to: :all
|
4
|
-
delegate :order, :limit, :size, :sort, :
|
5
|
-
delegate :or_filter, :
|
6
|
-
delegate
|
7
|
-
|
8
|
-
delegate :
|
4
|
+
delegate :order, :limit, :size, :sort, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all
|
5
|
+
delegate :or_filter, :fields, :source, :highlight, to: :all
|
6
|
+
delegate *Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS, to: :all
|
7
|
+
|
8
|
+
delegate :skip_callbacks, :routing, :search_options, to: :all
|
9
|
+
delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, to: :all
|
9
10
|
|
10
11
|
def fetch_results(es)
|
11
12
|
unless es.count?
|
data/lib/stretchy/relation.rb
CHANGED
@@ -4,7 +4,7 @@ module Stretchy
|
|
4
4
|
class Relation
|
5
5
|
|
6
6
|
# These methods can accept multiple values.
|
7
|
-
MULTI_VALUE_METHODS = [:order, :where, :or_filter, :
|
7
|
+
MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter_query, :bind, :extending, :unscope, :skip_callbacks]
|
8
8
|
|
9
9
|
# These methods can accept a single value.
|
10
10
|
SINGLE_VALUE_METHODS = [:limit, :offset, :routing, :size]
|
@@ -16,7 +16,7 @@ module Stretchy
|
|
16
16
|
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
|
17
17
|
|
18
18
|
# Include modules.
|
19
|
-
include Relations::FinderMethods, Relations::SpawnMethods, Relations::QueryMethods, Relations::SearchOptionMethods, Delegation
|
19
|
+
include Relations::FinderMethods, Relations::SpawnMethods, Relations::QueryMethods, Relations::AggregationMethods, Relations::SearchOptionMethods, Delegation
|
20
20
|
|
21
21
|
# Getters.
|
22
22
|
attr_reader :klass, :loaded
|
@@ -162,7 +162,7 @@ module Stretchy
|
|
162
162
|
#
|
163
163
|
# @return [QueryBuilder] The query builder for the relation.
|
164
164
|
def query_builder
|
165
|
-
Relations::QueryBuilder.new(values)
|
165
|
+
Relations::QueryBuilder.new(values, klass.attribute_types)
|
166
166
|
end
|
167
167
|
|
168
168
|
end
|