springy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +34 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/springy/and_collector.rb +102 -0
- data/lib/springy/api.rb +237 -0
- data/lib/springy/errors.rb +6 -0
- data/lib/springy/factory.rb +197 -0
- data/lib/springy/node.rb +21 -0
- data/lib/springy/results.rb +103 -0
- data/lib/springy/scopes.rb +30 -0
- data/lib/springy/utils.rb +76 -0
- data/lib/springy/version.rb +3 -0
- data/lib/springy.rb +70 -0
- data/springy.gemspec +32 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 993710d7e496323dcfb22680fd7ed879e105c7b1
|
4
|
+
data.tar.gz: bcf3f7281b0cf7061251ab6ed4aec75bc67d4b9c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9f1bc21a4846df0bbcd13738204e9500ab20115d67457b550bf9d9e9a3ee04c9b6e0d1cd794b6119d94f5e9386f45c77436e57765740973a11b9799dec3d8f36
|
7
|
+
data.tar.gz: 8233643b6d3d6aac589acee9e1e7ae1b717fba201252e534e3255f4f29e4930f613195d807e09dcfddba757a9b81dbca0f0573a09718c8f845054964225f2854
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.2
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 eubelts
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Springy
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/springy`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'springy'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install springy
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
1. Fork it ( https://github.com/[my-github-username]/springy/fork )
|
36
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
37
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
38
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
39
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
begin
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
task default: :spec
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift File.expand_path('lib', __FILE__)
|
10
|
+
require 'springy'
|
11
|
+
|
12
|
+
namespace :fixtures do
|
13
|
+
task :gen do
|
14
|
+
require 'json'
|
15
|
+
|
16
|
+
q = Springy.query(index: 'springy_test', type: 'game_dev')
|
17
|
+
.match(_all: 'game')
|
18
|
+
.where(url_slug: [
|
19
|
+
'masahiro-sakurai',
|
20
|
+
'tetsuya-mizuguchi',
|
21
|
+
'suda-51'
|
22
|
+
])
|
23
|
+
.explain
|
24
|
+
.page(1, per_page: 20)
|
25
|
+
|
26
|
+
File.open('spec/fixtures/request_stub.json', 'w') do |f|
|
27
|
+
f.puts JSON.pretty_generate(q.request)
|
28
|
+
end
|
29
|
+
|
30
|
+
File.open('spec/fixtures/response_stub.json', 'w') do |f|
|
31
|
+
f.puts JSON.pretty_generate(q.response)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "springy"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Springy
|
2
|
+
class AndCollector
|
3
|
+
|
4
|
+
extend Forwardable
|
5
|
+
delegate [:json, :as_json] => :node
|
6
|
+
delegate [:each] => :nodes
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_reader :nodes, :context
|
10
|
+
|
11
|
+
def initialize(nodes, context = {})
|
12
|
+
@nodes = nodes
|
13
|
+
@context = context
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_context(new_context)
|
17
|
+
self.class.new nodes, new_context
|
18
|
+
end
|
19
|
+
|
20
|
+
def context?(*args)
|
21
|
+
args.all? {|c| !!context[c] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def node
|
25
|
+
@node ||= if function_score_node?
|
26
|
+
function_score_node
|
27
|
+
elsif query_nodes.any?
|
28
|
+
query_node
|
29
|
+
else
|
30
|
+
Node.new({match_all: {}}, context)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def query_nodes
|
35
|
+
@query_nodes ||= nodes.reject {|n| n.context? :boost }
|
36
|
+
end
|
37
|
+
|
38
|
+
def boost_nodes
|
39
|
+
@boost_nodes ||= nodes.select {|n| n.context? :boost }
|
40
|
+
end
|
41
|
+
|
42
|
+
def function_score_node?
|
43
|
+
boost_nodes.reject { |n| n.empty? }.any?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def query_node
|
49
|
+
if query_nodes.size > 1 || multicontext?(query_nodes)
|
50
|
+
compile_bool query_nodes
|
51
|
+
else
|
52
|
+
query_nodes.first
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def multicontext?(node_arr)
|
57
|
+
Array(node_arr).any? {|n| n.context?(:must_not) || n.context?(:should) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def compile_bool(bool_nodes)
|
61
|
+
split_nodes = split_nodes_for_bool(bool_nodes)
|
62
|
+
refined = bool_ctx.each_with_object(split_nodes) do |k, hash|
|
63
|
+
hash[k] = Array(compile_bool(hash[k])) if multicontext? hash[k]
|
64
|
+
end
|
65
|
+
bool_json = Hash[refined.map{|k,v| [k, v.map(&:as_json)] }]
|
66
|
+
Node.new(bool: bool_json)
|
67
|
+
end
|
68
|
+
|
69
|
+
def bool_ctx
|
70
|
+
[:filter, :must_not, :should]
|
71
|
+
end
|
72
|
+
|
73
|
+
def split_nodes_for_bool(bool_nodes)
|
74
|
+
bool_nodes.each_with_object({}) do |n, hash|
|
75
|
+
key = bool_ctx.find{|c| n.context? c } || :must
|
76
|
+
hash[key] ||= []
|
77
|
+
hash[key] << Node.new(n.json, n.context.merge(key => nil))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def compile_boost_functions
|
82
|
+
boost_nodes.map do |n|
|
83
|
+
next if n.empty?
|
84
|
+
n.json
|
85
|
+
end.compact
|
86
|
+
end
|
87
|
+
|
88
|
+
def compile_function_score_options
|
89
|
+
boost_nodes.reduce({}) do |options, node|
|
90
|
+
options.merge(node.context[:fn_score] || {})
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def function_score_node
|
95
|
+
function_score_json = compile_function_score_options
|
96
|
+
function_score_json[:functions] = compile_boost_functions
|
97
|
+
function_score_json[:query] = query_node.json if query_nodes.any?
|
98
|
+
|
99
|
+
Node.new({function_score: function_score_json}, context)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/springy/api.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'springy/utils'
|
2
|
+
|
3
|
+
module Springy
|
4
|
+
class API
|
5
|
+
DEFAULT_BOOST = 2.0
|
6
|
+
DEFAULT_PER_PAGE = 10
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
include Enumerable
|
10
|
+
include Utils::Methods
|
11
|
+
|
12
|
+
attr_reader :collector, :opts, :root, :body, :context
|
13
|
+
|
14
|
+
delegate [:with_context, :json, :as_json] => :collector
|
15
|
+
|
16
|
+
delegate [
|
17
|
+
:total, :total_count, :length, :size, :total_pages, :results, :hits,
|
18
|
+
:to_a, :ids, :scores, :explanations, :aggregations, :each
|
19
|
+
] => :results_obj
|
20
|
+
|
21
|
+
def initialize(opts = {})
|
22
|
+
@opts = opts
|
23
|
+
@collector = AndCollector.new(opts[:nodes] || [], query: true)
|
24
|
+
@root = opts[:root] || {}
|
25
|
+
@body = opts[:body] || {}
|
26
|
+
@context = opts[:context] || {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# def context?(*args)
|
30
|
+
# (args - context.keys).empty?
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
def limit(size = nil)
|
34
|
+
return @root[:size] || DEFAULT_PER_PAGE unless size
|
35
|
+
add_root size: size.to_i
|
36
|
+
end
|
37
|
+
alias :limit_value :limit
|
38
|
+
|
39
|
+
def offset(from = nil)
|
40
|
+
return @root[:from] || 0 unless from
|
41
|
+
add_root from: from.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
# page 1 = from: 0, size: per_page
|
45
|
+
# page 2 = from: per_page, size: per_page
|
46
|
+
def page(num = nil, params = {})
|
47
|
+
return current_page if num.nil?
|
48
|
+
per = params[:limit] || params[:per_page] || limit
|
49
|
+
per = per.to_i > 0 ? per.to_i : 1
|
50
|
+
start = [num.to_i - 1, 0].max
|
51
|
+
add_root from: start * per, size: per
|
52
|
+
end
|
53
|
+
|
54
|
+
def per(num = nil)
|
55
|
+
return limit if num.nil?
|
56
|
+
add_root size: [num.to_i, 1].max
|
57
|
+
end
|
58
|
+
alias :per_page :per
|
59
|
+
|
60
|
+
def current_page
|
61
|
+
Utils.current_page(offset, limit)
|
62
|
+
end
|
63
|
+
|
64
|
+
def explain
|
65
|
+
add_root explain: true
|
66
|
+
end
|
67
|
+
|
68
|
+
def fields(*list)
|
69
|
+
add_root _source: list
|
70
|
+
end
|
71
|
+
|
72
|
+
alias :source :fields
|
73
|
+
|
74
|
+
def aggs(params = {})
|
75
|
+
add_body aggs: params
|
76
|
+
end
|
77
|
+
|
78
|
+
def highlight(params = {})
|
79
|
+
add_body highlight: params
|
80
|
+
end
|
81
|
+
|
82
|
+
def where(params = {})
|
83
|
+
subcontext = {filter: true}
|
84
|
+
subcontext[:nested] = params.delete(:nested) if params[:nested]
|
85
|
+
add_params params, subcontext, :context_nodes
|
86
|
+
end
|
87
|
+
|
88
|
+
def match(params = {})
|
89
|
+
if params.is_a? Hash
|
90
|
+
subcontext = {query: true}
|
91
|
+
subcontext[:nested] = true if params.delete(:nested)
|
92
|
+
add_params params, subcontext, :context_nodes
|
93
|
+
else
|
94
|
+
add_params Hash[_all: params], :query, :context_nodes
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def more_like(params = {})
|
99
|
+
params[:ids] = Array(params[:ids]) if params[:ids]
|
100
|
+
add_params params, :query, :more_like_node
|
101
|
+
end
|
102
|
+
|
103
|
+
def fulltext(params = '')
|
104
|
+
unless params.is_a?(String)
|
105
|
+
raise Errors::InvalidParamsError.new('.fulltext only takes a string')
|
106
|
+
end
|
107
|
+
add_nodes Factory.fulltext_nodes_from_string(params, context)
|
108
|
+
end
|
109
|
+
|
110
|
+
def query(params = {})
|
111
|
+
add_params params, :query, :raw_node
|
112
|
+
end
|
113
|
+
|
114
|
+
def filter(params = {})
|
115
|
+
add_params params, :filter, :raw_node
|
116
|
+
end
|
117
|
+
|
118
|
+
def should(params = {})
|
119
|
+
add_params params, :should, :context_nodes
|
120
|
+
end
|
121
|
+
|
122
|
+
def not(params = {})
|
123
|
+
add_params params, :must_not, :context_nodes
|
124
|
+
end
|
125
|
+
|
126
|
+
def range(params = {})
|
127
|
+
add_params params, nil, :range_node
|
128
|
+
end
|
129
|
+
|
130
|
+
def geo_distance(params = {})
|
131
|
+
add_params params, nil, :geo_distance_node
|
132
|
+
end
|
133
|
+
|
134
|
+
def boost(params = {}, options = {})
|
135
|
+
return add_context(:boost) if Utils.is_empty? params
|
136
|
+
|
137
|
+
subcontext = context.merge(boost: true)
|
138
|
+
if params.is_a? self.class
|
139
|
+
boost_json = options.merge(filter: params.json)
|
140
|
+
add_nodes Node.new(boost_json, subcontext)
|
141
|
+
else
|
142
|
+
add_nodes Factory.raw_boost_node(params, subcontext)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def field_value(params = {})
|
147
|
+
add_params params, :boost, :field_value_function_node
|
148
|
+
end
|
149
|
+
|
150
|
+
def random(params)
|
151
|
+
if params.is_a? Hash
|
152
|
+
add_params params, :boost, :random_score_function_node
|
153
|
+
else
|
154
|
+
add_params Hash[seed: params], :boost, :random_score_function_node
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def near(params = {})
|
159
|
+
add_params params, :boost, :decay_function_node
|
160
|
+
end
|
161
|
+
|
162
|
+
def request
|
163
|
+
@request ||= begin
|
164
|
+
root.merge(body: body.merge(query: collector.as_json))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def response
|
169
|
+
@response ||= Springy.search(request)
|
170
|
+
end
|
171
|
+
|
172
|
+
def results_obj
|
173
|
+
@results ||= Results.new request, response
|
174
|
+
end
|
175
|
+
#
|
176
|
+
# def count
|
177
|
+
# results_obj.ids.count
|
178
|
+
# end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def args_to_context(*args)
|
183
|
+
args.reduce({}) do |ctx, item|
|
184
|
+
next ctx if item.nil?
|
185
|
+
item.is_a?(Hash) ? ctx.merge(item) : ctx.merge({item => true})
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_params(params = {}, new_context, factory_method)
|
190
|
+
return add_context(new_context) if is_empty?(params)
|
191
|
+
subcontext = context.merge(args_to_context(new_context))
|
192
|
+
|
193
|
+
if params.is_a? self.class
|
194
|
+
add_nodes params.with_context(subcontext)
|
195
|
+
else
|
196
|
+
add_nodes Factory.send(factory_method, params, subcontext)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_nodes(additional)
|
201
|
+
self.class.new(opts.merge(
|
202
|
+
nodes: collector.nodes + Array(additional),
|
203
|
+
root: root,
|
204
|
+
body: body,
|
205
|
+
context: {}
|
206
|
+
))
|
207
|
+
end
|
208
|
+
|
209
|
+
def add_root(options = {})
|
210
|
+
self.class.new(opts.merge(
|
211
|
+
nodes: collector.nodes,
|
212
|
+
root: root.merge(options),
|
213
|
+
body: body,
|
214
|
+
context: context
|
215
|
+
))
|
216
|
+
end
|
217
|
+
|
218
|
+
def add_body(options = {})
|
219
|
+
self.class.new(opts.merge(
|
220
|
+
nodes: collector.nodes,
|
221
|
+
root: root,
|
222
|
+
body: body.merge(options),
|
223
|
+
context: context
|
224
|
+
))
|
225
|
+
end
|
226
|
+
|
227
|
+
def add_context(*args)
|
228
|
+
self.class.new(opts.merge(
|
229
|
+
nodes: collector.nodes,
|
230
|
+
root: root,
|
231
|
+
body: body,
|
232
|
+
context: context.merge(args_to_context(*args))
|
233
|
+
))
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Springy
|
2
|
+
module Factory
|
3
|
+
|
4
|
+
DEFAULT_WEIGHT = 1.5
|
5
|
+
DEFAULT_SLOP = 50
|
6
|
+
BOOST_OPTIONS = [
|
7
|
+
:filter,
|
8
|
+
:function,
|
9
|
+
:weight
|
10
|
+
]
|
11
|
+
FUNCTION_SCORE_OPTIONS = [
|
12
|
+
:boost,
|
13
|
+
:max_boost,
|
14
|
+
:score_mode,
|
15
|
+
:boost_mode,
|
16
|
+
:min_score
|
17
|
+
]
|
18
|
+
|
19
|
+
module_function
|
20
|
+
|
21
|
+
def default_context
|
22
|
+
{}
|
23
|
+
end
|
24
|
+
|
25
|
+
def extract_boost_params!(params)
|
26
|
+
Utils.extract_options!(params, BOOST_OPTIONS)
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_function_score_options!(params)
|
30
|
+
Utils.extract_options!(params, FUNCTION_SCORE_OPTIONS)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dotify_params(params, context)
|
34
|
+
if context[:nested]
|
35
|
+
Utils.nestify(params)
|
36
|
+
else
|
37
|
+
Utils.dotify(params)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def raw_node(params, context)
|
42
|
+
if context[:boost]
|
43
|
+
raw_boost_node(params, context)
|
44
|
+
else
|
45
|
+
Node.new(params, context)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def raw_boost_node(params, context)
|
50
|
+
boost_params = extract_boost_params!(params)
|
51
|
+
context[:fn_score] = extract_function_score_options!(params)
|
52
|
+
context[:boost] = true
|
53
|
+
context[:filter] = true
|
54
|
+
boost_params.merge!(filter: params) unless Utils.is_empty? params
|
55
|
+
Node.new(boost_params, context)
|
56
|
+
end
|
57
|
+
|
58
|
+
def context_nodes(params, context = default_context)
|
59
|
+
if context[:boost]
|
60
|
+
params_to_boost(params, context)
|
61
|
+
elsif context[:filter] && !context[:query]
|
62
|
+
params_to_filters(dotify_params(params, context), context)
|
63
|
+
else
|
64
|
+
params_to_queries(dotify_params(params, context), context)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def params_to_boost(params, context = default_context)
|
69
|
+
boost_params = extract_boost_params!(params)
|
70
|
+
context[:fn_score] = extract_function_score_options!(params)
|
71
|
+
subcontext = context.merge(boost: nil)
|
72
|
+
nodes = context_nodes(params, subcontext)
|
73
|
+
collector = AndCollector.new(nodes, subcontext)
|
74
|
+
|
75
|
+
boost_params.merge!(filter: collector.json) if collector.any?
|
76
|
+
if boost_params.count == 1 && boost_params.key?(:filter)
|
77
|
+
boost_params[:weight] = DEFAULT_WEIGHT
|
78
|
+
end
|
79
|
+
|
80
|
+
Node.new(boost_params, context)
|
81
|
+
end
|
82
|
+
|
83
|
+
def params_to_queries(params, context = default_context)
|
84
|
+
params.map do |field, val|
|
85
|
+
case val
|
86
|
+
when Array
|
87
|
+
Node.new({match: {
|
88
|
+
field => {query: val.join(' '), :operator => :or}
|
89
|
+
}}, context)
|
90
|
+
when Range
|
91
|
+
Node.new({range: {
|
92
|
+
field => {gte: val.min, lte: val.max}
|
93
|
+
}}, context)
|
94
|
+
when Hash
|
95
|
+
nested(val, field, context)
|
96
|
+
else
|
97
|
+
Node.new({match: {field => val}}, context)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def params_to_filters(params, context = default_context)
|
103
|
+
params.map do |field, val|
|
104
|
+
case val
|
105
|
+
when Range
|
106
|
+
Node.new({range: {field => {gte: val.min, lte: val.max}}}, context)
|
107
|
+
when nil
|
108
|
+
nil_ctx = context.merge(must_not: true)
|
109
|
+
Node.new({exists: {field: field}}, nil_ctx)
|
110
|
+
when Hash
|
111
|
+
nested(val, field, context)
|
112
|
+
else
|
113
|
+
Node.new({terms: {field => Array(val)}}, context)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def nested(params, path, context = default_context)
|
119
|
+
nodes = if context[:filter] && !context[:query]
|
120
|
+
params_to_filters(params, context)
|
121
|
+
else
|
122
|
+
params_to_queries(params, context)
|
123
|
+
end
|
124
|
+
json = AndCollector.new(nodes, context).json
|
125
|
+
|
126
|
+
Node.new({nested: {
|
127
|
+
path: path,
|
128
|
+
query: json
|
129
|
+
}}, context)
|
130
|
+
end
|
131
|
+
|
132
|
+
# https://www.elastic.co/guide/en/elasticsearch/guide/current/proximity-relevance.html
|
133
|
+
def fulltext_nodes_from_string(params, context = default_context)
|
134
|
+
subcontext = context.merge(query: true)
|
135
|
+
nodes = [raw_node({
|
136
|
+
match: {
|
137
|
+
_all: {
|
138
|
+
query: params,
|
139
|
+
minimum_should_match: 1
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}, subcontext)]
|
143
|
+
|
144
|
+
subcontext = subcontext.merge(should: true)
|
145
|
+
nodes << Factory.raw_node({
|
146
|
+
match_phrase: {
|
147
|
+
_all: {
|
148
|
+
query: params,
|
149
|
+
slop: DEFAULT_SLOP
|
150
|
+
}
|
151
|
+
}
|
152
|
+
}, subcontext)
|
153
|
+
|
154
|
+
nodes
|
155
|
+
end
|
156
|
+
|
157
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-mlt-query.html
|
158
|
+
def more_like_node(params = {}, context = default_context)
|
159
|
+
Node.new({more_like_this: params}, context)
|
160
|
+
end
|
161
|
+
|
162
|
+
# query and filter use the same syntax
|
163
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html
|
164
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-filter.html
|
165
|
+
def range_node(params = {}, context = default_context)
|
166
|
+
Node.new({range: params}, context)
|
167
|
+
end
|
168
|
+
|
169
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-filter.html
|
170
|
+
def geo_distance_node(params = {}, context = default_context)
|
171
|
+
Node.new({geo_distance: params}, context)
|
172
|
+
end
|
173
|
+
|
174
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/querydslfunctionscorequery.html#functionfieldvaluefactor
|
175
|
+
def field_value_function_node(params = {}, context = default_context)
|
176
|
+
context[:fn_score] = extract_function_score_options!(params)
|
177
|
+
boost_params = extract_boost_params!(params)
|
178
|
+
Node.new(boost_params.merge(field_value_factor: params), context)
|
179
|
+
end
|
180
|
+
|
181
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/querydslfunctionscorequery.html#functionrandom
|
182
|
+
def random_score_function_node(params, context = default_context)
|
183
|
+
json = {random_score: {seed: params[:seed]}}
|
184
|
+
json[:weight] = params[:weight] if params[:weight]
|
185
|
+
Node.new(json, context)
|
186
|
+
end
|
187
|
+
|
188
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/querydslfunctionscorequery.html#functiondecay
|
189
|
+
def decay_function_node(params = {}, context = default_context)
|
190
|
+
boost_params = extract_boost_params!(params)
|
191
|
+
context[:fn_score] = extract_function_score_options!(params)
|
192
|
+
decay_fn = params.delete(:decay_function)
|
193
|
+
field = params.delete(:field)
|
194
|
+
Node.new({decay_fn => { field => params}}.merge(boost_params), context)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
data/lib/springy/node.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Springy
|
2
|
+
class Node
|
3
|
+
|
4
|
+
attr_reader :json, :context
|
5
|
+
alias :as_json :json
|
6
|
+
|
7
|
+
def initialize(json, context = {})
|
8
|
+
@json = json
|
9
|
+
@context = context
|
10
|
+
end
|
11
|
+
|
12
|
+
def empty?
|
13
|
+
!@json.any?
|
14
|
+
end
|
15
|
+
|
16
|
+
def context?(*args)
|
17
|
+
args.all? {|c| !!context[c] }
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Springy
|
2
|
+
class Results
|
3
|
+
|
4
|
+
extend Forwardable
|
5
|
+
include Enumerable
|
6
|
+
include Utils::Methods
|
7
|
+
delegate [:first, :last, :each] => :results
|
8
|
+
|
9
|
+
attr_reader :request, :response
|
10
|
+
|
11
|
+
# implements null object pattern
|
12
|
+
def self.fake
|
13
|
+
self.new(
|
14
|
+
{size: API::DEFAULT_PER_PAGE, fake: true},
|
15
|
+
{'hits' => {'total' => 0, 'hits' => [], 'aggregations' => {}}}
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(request, response)
|
20
|
+
@request = request
|
21
|
+
@response = response
|
22
|
+
end
|
23
|
+
|
24
|
+
def fake?
|
25
|
+
!!request[:fake]
|
26
|
+
end
|
27
|
+
|
28
|
+
def limit
|
29
|
+
(request['size'] || request[:size] || API::DEFAULT_PER_PAGE).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
alias :size :limit
|
33
|
+
alias :limit_value :limit
|
34
|
+
alias :per_page :limit
|
35
|
+
|
36
|
+
def offset
|
37
|
+
(request['from'] || request[:from] || 0).to_i
|
38
|
+
end
|
39
|
+
alias :from :offset
|
40
|
+
alias :offset_value :offset
|
41
|
+
|
42
|
+
def current_page
|
43
|
+
Utils.current_page(offset, limit)
|
44
|
+
end
|
45
|
+
|
46
|
+
def total
|
47
|
+
response['hits']['total']
|
48
|
+
end
|
49
|
+
alias :total_count :total
|
50
|
+
alias :count :total
|
51
|
+
alias :length :total
|
52
|
+
alias :size :total
|
53
|
+
|
54
|
+
def total_pages
|
55
|
+
(total.to_f / limit_value).ceil
|
56
|
+
end
|
57
|
+
|
58
|
+
def results
|
59
|
+
@results ||= response['hits']['hits'].map do |r|
|
60
|
+
fields = r.reject {|k, _| k == '_source' || k == 'fields'}
|
61
|
+
fields['_id'] = coerce_id(fields['_id']) if fields['_id']
|
62
|
+
source = r['_source'] || {}
|
63
|
+
|
64
|
+
# Elasticsearch always returns array values when specific
|
65
|
+
# fields are selected. Undesirable for single values, so
|
66
|
+
# coerce to single values when appropriate
|
67
|
+
selected = r['fields'] || {}
|
68
|
+
selected = Hash[selected.map do |k,v|
|
69
|
+
v.is_a?(Array) && v.count == 1 ? [k,v.first] : [k,v]
|
70
|
+
end]
|
71
|
+
|
72
|
+
source.merge(selected).merge(fields)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
alias :hits :results
|
77
|
+
alias :to_a :results
|
78
|
+
|
79
|
+
def ids
|
80
|
+
@ids ||= response['hits']['hits'].map {|r| coerce_id r['_id'] }
|
81
|
+
end
|
82
|
+
|
83
|
+
def scores
|
84
|
+
@scores ||= Hash[results.map {|r| [coerce_id(r['_id']), r['_score']]}]
|
85
|
+
end
|
86
|
+
|
87
|
+
def explanations
|
88
|
+
@explanations ||= Hash[results.map {|r|
|
89
|
+
[coerce_id(r['_id']), r['_explanation']]
|
90
|
+
}]
|
91
|
+
end
|
92
|
+
|
93
|
+
def aggregations(*args)
|
94
|
+
key = args.map(&:to_s).join('.')
|
95
|
+
@aggregations ||= {}
|
96
|
+
@aggregations[key] ||= begin
|
97
|
+
args.reduce(response['aggregations']) do |agg, name|
|
98
|
+
agg = agg[name.to_s] unless agg.nil?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Springy
|
2
|
+
module Scopes
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def stretchify(options = {})
|
10
|
+
@stretchy_options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def search(options = {})
|
14
|
+
stretchy_scope.new stretchy_options.merge(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def stretchy_options
|
18
|
+
Hash(@stretchy_options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def stretchy_scope
|
22
|
+
@scopes_class ||= Class.new Springy::API
|
23
|
+
end
|
24
|
+
|
25
|
+
def stretch(name, block)
|
26
|
+
stretchy_scope.send(:define_method, name, &block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Springy
|
2
|
+
module Utils
|
3
|
+
module Methods
|
4
|
+
|
5
|
+
# detects empty string, empty array, empty hash, nil
|
6
|
+
def is_empty?(arg = nil)
|
7
|
+
return true if arg.nil?
|
8
|
+
if arg.respond_to?(:collector)
|
9
|
+
!arg.collector.any?
|
10
|
+
elsif arg.respond_to?(:any?)
|
11
|
+
!arg.any? {|a| !is_empty?(a) }
|
12
|
+
elsif arg.respond_to?(:empty?)
|
13
|
+
arg.empty?
|
14
|
+
else
|
15
|
+
!arg
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# raises error if required parameters are missing
|
20
|
+
def require_params!(method, params, *fields)
|
21
|
+
raise Errors::InvalidParamsError.new(
|
22
|
+
"#{method} requires at least #{fields.join(' and ')} params, but " +
|
23
|
+
"found: #{params}"
|
24
|
+
) if fields.any? {|f| is_empty? params[f] }
|
25
|
+
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
# generates a hash of specified options,
|
30
|
+
# removing them from the original hash
|
31
|
+
def extract_options!(params, list)
|
32
|
+
output = Hash[list.map do |opt|
|
33
|
+
[opt, params.delete(opt)]
|
34
|
+
end].keep_if {|k,v| !is_empty?(v)}
|
35
|
+
end
|
36
|
+
|
37
|
+
# must be shared between api & results
|
38
|
+
def current_page(offset, limit)
|
39
|
+
((offset + 1.0) / limit).ceil
|
40
|
+
end
|
41
|
+
|
42
|
+
# coerces ids to integers, unless they contain non-integers
|
43
|
+
def coerce_id(id)
|
44
|
+
id =~ /^\d+$/ ? id.to_i : id
|
45
|
+
end
|
46
|
+
|
47
|
+
def dotify(hash, prefixes = [])
|
48
|
+
hash.reduce({}) do |memo, kv|
|
49
|
+
key, val = kv
|
50
|
+
subprefixes = (prefixes + [key])
|
51
|
+
prefix = subprefixes.join('.')
|
52
|
+
if val.is_a? Hash
|
53
|
+
memo.merge(dotify(val, subprefixes))
|
54
|
+
else
|
55
|
+
memo.merge(prefix => val)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def nestify(hash, prefixes = [])
|
61
|
+
hash.reduce({}) do |memo, kv|
|
62
|
+
key, val = kv
|
63
|
+
subprefixes = (prefixes + [key])
|
64
|
+
prefix = subprefixes.join('.')
|
65
|
+
if val.is_a? Hash
|
66
|
+
memo.merge(prefix => nestify(val, subprefixes))
|
67
|
+
else
|
68
|
+
memo.merge(prefix => val)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
extend Methods
|
75
|
+
end
|
76
|
+
end
|
data/lib/springy.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'logger'
|
3
|
+
require 'forwardable'
|
4
|
+
require 'elasticsearch'
|
5
|
+
|
6
|
+
require "springy/version"
|
7
|
+
require 'springy/api'
|
8
|
+
require 'springy/utils'
|
9
|
+
require 'springy/and_collector'
|
10
|
+
require 'springy/factory'
|
11
|
+
require 'springy/node'
|
12
|
+
require 'springy/results'
|
13
|
+
require 'springy/scopes'
|
14
|
+
require 'springy/errors'
|
15
|
+
|
16
|
+
module Springy
|
17
|
+
|
18
|
+
module_function
|
19
|
+
|
20
|
+
def client
|
21
|
+
@client ||= Elasticsearch::Client.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def client=(client)
|
25
|
+
@client = client
|
26
|
+
end
|
27
|
+
|
28
|
+
def search(options = {})
|
29
|
+
client.search(options)
|
30
|
+
rescue Elasticsearch::Transport::Transport::Errors::BadRequest => bre
|
31
|
+
msg = bre.message[-150..-1]
|
32
|
+
msg << "\n\n"
|
33
|
+
msg << JSON.pretty_generate(options)
|
34
|
+
raise msg
|
35
|
+
end
|
36
|
+
|
37
|
+
def index_exists?(name)
|
38
|
+
client.indices.exists? index: name
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete_index(name)
|
42
|
+
client.indices.delete(index: name) if index_exists? name
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_index(name, params = {})
|
46
|
+
client.indices.create({index: name}.merge(params)) unless index_exists? name
|
47
|
+
end
|
48
|
+
|
49
|
+
def refresh_index(name, params = {})
|
50
|
+
client.indices.refresh({index: name}.merge(params)) if index_exists? name
|
51
|
+
end
|
52
|
+
|
53
|
+
def count_index(name, params = {})
|
54
|
+
client.count({index: name}.merge(params))['count']
|
55
|
+
end
|
56
|
+
|
57
|
+
def index_document(params = {})
|
58
|
+
Utils.require_params!(:index_document, params, :index, :type, :body)
|
59
|
+
|
60
|
+
raise IndexDoesNotExistError.new(
|
61
|
+
"index #{params[:index]} does not exist"
|
62
|
+
) unless index_exists? params[:index]
|
63
|
+
|
64
|
+
client.index(params)
|
65
|
+
end
|
66
|
+
|
67
|
+
def query(options = {})
|
68
|
+
API.new(root: options)
|
69
|
+
end
|
70
|
+
end
|
data/springy.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'springy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "springy"
|
8
|
+
spec.version = Springy::VERSION
|
9
|
+
spec.authors = ["eubelts"]
|
10
|
+
spec.email = ["eunicesausbeltran@yahoo.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Query builder for Elasticsearch}
|
13
|
+
spec.description = %q{ActiveRecord-like query builder for Elasticsearch}
|
14
|
+
# spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
|
25
|
+
spec.add_dependency "elasticsearch", "~> 5.0"
|
26
|
+
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
28
|
+
spec.add_development_dependency "fuubar", "~> 2.0"
|
29
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
30
|
+
spec.add_development_dependency "awesome_print", "~> 1.6"
|
31
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: springy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- eubelts
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: elasticsearch
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.2'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: fuubar
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.10'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.10'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: awesome_print
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.6'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.6'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.9'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.9'
|
125
|
+
description: ActiveRecord-like query builder for Elasticsearch
|
126
|
+
email:
|
127
|
+
- eunicesausbeltran@yahoo.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".ruby-version"
|
134
|
+
- CODE_OF_CONDUCT.md
|
135
|
+
- Gemfile
|
136
|
+
- LICENSE.txt
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- bin/console
|
140
|
+
- bin/setup
|
141
|
+
- lib/springy.rb
|
142
|
+
- lib/springy/and_collector.rb
|
143
|
+
- lib/springy/api.rb
|
144
|
+
- lib/springy/errors.rb
|
145
|
+
- lib/springy/factory.rb
|
146
|
+
- lib/springy/node.rb
|
147
|
+
- lib/springy/results.rb
|
148
|
+
- lib/springy/scopes.rb
|
149
|
+
- lib/springy/utils.rb
|
150
|
+
- lib/springy/version.rb
|
151
|
+
- springy.gemspec
|
152
|
+
homepage:
|
153
|
+
licenses:
|
154
|
+
- MIT
|
155
|
+
metadata: {}
|
156
|
+
post_install_message:
|
157
|
+
rdoc_options: []
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - ">="
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 2.6.13
|
173
|
+
signing_key:
|
174
|
+
specification_version: 4
|
175
|
+
summary: Query builder for Elasticsearch
|
176
|
+
test_files: []
|