search_flip 1.0.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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +34 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +606 -0
- data/Rakefile +9 -0
- data/irb.rb +7 -0
- data/lib/search_flip/aggregatable.rb +69 -0
- data/lib/search_flip/aggregation.rb +57 -0
- data/lib/search_flip/bulk.rb +152 -0
- data/lib/search_flip/config.rb +21 -0
- data/lib/search_flip/criteria.rb +737 -0
- data/lib/search_flip/filterable.rb +240 -0
- data/lib/search_flip/http_client.rb +49 -0
- data/lib/search_flip/index.rb +545 -0
- data/lib/search_flip/json.rb +18 -0
- data/lib/search_flip/model.rb +21 -0
- data/lib/search_flip/post_filterable.rb +252 -0
- data/lib/search_flip/response.rb +319 -0
- data/lib/search_flip/result.rb +12 -0
- data/lib/search_flip/to_json.rb +31 -0
- data/lib/search_flip/version.rb +5 -0
- data/lib/search_flip.rb +82 -0
- data/search_flip.gemspec +35 -0
- data/test/database.yml +4 -0
- data/test/search_flip/aggregation_test.rb +212 -0
- data/test/search_flip/bulk_test.rb +55 -0
- data/test/search_flip/criteria_test.rb +825 -0
- data/test/search_flip/http_client_test.rb +35 -0
- data/test/search_flip/index_test.rb +350 -0
- data/test/search_flip/model_test.rb +39 -0
- data/test/search_flip/response_test.rb +136 -0
- data/test/search_flip/to_json_test.rb +30 -0
- data/test/search_flip_test.rb +26 -0
- data/test/test_helper.rb +243 -0
- metadata +258 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# The SearchFlip::Filterable mixin provides chainable methods like
|
4
|
+
# #where, #exists, #range, etc to add search filters to a criteria.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# CommentIndex.where(public: true)
|
8
|
+
# CommentIndex.exists(:user_id)
|
9
|
+
# CommentIndex.range(:created_at, gt: Date.today - 7)
|
10
|
+
|
11
|
+
module Filterable
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
attr_accessor :search_values, :must_values, :must_not_values, :should_values, :filter_values
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds a query string query to the criteria while using AND as the default
|
19
|
+
# operator unless otherwise specified. Check out the ElasticSearch docs
|
20
|
+
# for further details.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# CommentIndex.search("message:hello OR message:worl*")
|
24
|
+
#
|
25
|
+
# @param q [String] The query string query
|
26
|
+
#
|
27
|
+
# @param options [Hash] Additional options for the query string query, like
|
28
|
+
# eg default_operator, default_field, etc.
|
29
|
+
#
|
30
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
31
|
+
|
32
|
+
def search(q, options = {})
|
33
|
+
fresh.tap do |criteria|
|
34
|
+
criteria.search_values = (search_values || []) + [query_string: { query: q, :default_operator => :AND }.merge(options)] if q.to_s.strip.length > 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Adds filters to your criteria for the supplied hash composed of
|
39
|
+
# field-to-filter mappings which specify terms, term or range filters,
|
40
|
+
# depending on the type of the respective hash value, namely array, range
|
41
|
+
# or scalar type like Fixnum, String, etc.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# CommentIndex.where(id: [1, 2, 3], state: ["approved", "declined"])
|
45
|
+
# CommentIndex.where(id: 1 .. 100)
|
46
|
+
# CommentIndex.where(created_at: Time.parse("2016-01-01") .. Time.parse("2017-01-01"))
|
47
|
+
# CommentIndex.where(id: 1, message: "hello")
|
48
|
+
# CommentIndex.where(state: nil)
|
49
|
+
#
|
50
|
+
# @param hash [Hash] A field-to-filter mapping specifying filter values for
|
51
|
+
# the respective fields
|
52
|
+
#
|
53
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
54
|
+
|
55
|
+
def where(hash)
|
56
|
+
hash.inject(fresh) do |memo, (key, value)|
|
57
|
+
if value.is_a?(Array)
|
58
|
+
memo.filter terms: { key => value }
|
59
|
+
elsif value.is_a?(Range)
|
60
|
+
memo.filter range: { key => { gte: value.min, lte: value.max } }
|
61
|
+
elsif value.nil?
|
62
|
+
memo.exists_not key
|
63
|
+
else
|
64
|
+
memo.filter term: { key => value }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Adds filters to exclude documents in accordance to the supplied hash
|
70
|
+
# composed of field-to-filter mappings. Check out #where for further
|
71
|
+
# details.
|
72
|
+
#
|
73
|
+
# @see #where See #where for further details
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
# CommentIndex.where_not(state: "approved")
|
77
|
+
# CommentIndex.where_not(created_at: Time.parse("2016-01-01") .. Time.parse("2017-01-01"))
|
78
|
+
# CommentIndex.where_not(id: [1, 2, 3], state: "new")
|
79
|
+
# CommentIndex.where_not(state: nil)
|
80
|
+
#
|
81
|
+
# @param hash [Hash] A field-to-filter mapping specifying filter values for the
|
82
|
+
# respective fields
|
83
|
+
#
|
84
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
85
|
+
|
86
|
+
def where_not(hash)
|
87
|
+
hash.inject(fresh) do |memo, (key, value)|
|
88
|
+
if value.is_a?(Array)
|
89
|
+
memo.must_not terms: { key => value }
|
90
|
+
elsif value.is_a?(Range)
|
91
|
+
memo.must_not range: { key => { gte: value.min, lte: value.max } }
|
92
|
+
elsif value.nil?
|
93
|
+
memo.exists key
|
94
|
+
else
|
95
|
+
memo.must_not term: { key => value }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Adds raw filter queries to the criteria.
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# CommentIndex.filter(term: { state: "new" })
|
104
|
+
# CommentIndex.filter(range: { created_at: { gte: Time.parse("2016-01-01") }})
|
105
|
+
#
|
106
|
+
# @param args [Array, Hash] The raw filter query arguments
|
107
|
+
#
|
108
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
109
|
+
|
110
|
+
def filter(*args)
|
111
|
+
fresh.tap do |criteria|
|
112
|
+
criteria.filter_values = (filter_values || []) + args
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Adds raw must queries to the criteria.
|
117
|
+
#
|
118
|
+
# @example
|
119
|
+
# CommentIndex.must(term: { state: "new" })
|
120
|
+
# CommentIndex.must(range: { created_at: { gt: Time.parse("2016-01-01") }})
|
121
|
+
#
|
122
|
+
# @param args [Array, Hash] The raw must query arguments
|
123
|
+
#
|
124
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
125
|
+
|
126
|
+
def must(*args)
|
127
|
+
fresh.tap do |criteria|
|
128
|
+
criteria.must_values = (must_values || []) + args
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Adds raw must_not queries to the criteria.
|
133
|
+
#
|
134
|
+
# @example
|
135
|
+
# CommentIndex.must_not(term: { state: "new" })
|
136
|
+
# CommentIndex.must_not(range: { created_at: { gt: Time.parse"2016-01-01") }})
|
137
|
+
#
|
138
|
+
# @param args [Array, Hash] The raw must_not query arguments
|
139
|
+
#
|
140
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
141
|
+
|
142
|
+
def must_not(*args)
|
143
|
+
fresh.tap do |criteria|
|
144
|
+
criteria.must_not_values = (must_not_values || []) + args
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Adds raw should queries to the criteria.
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# CommentIndex.should(term: { state: "new" })
|
152
|
+
# CommentIndex.should(range: { created_at: { gt: Time.parse"2016-01-01") }})
|
153
|
+
#
|
154
|
+
# @param args [Array, Hash] The raw should query arguments
|
155
|
+
#
|
156
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
157
|
+
|
158
|
+
def should(*args)
|
159
|
+
fresh.tap do |criteria|
|
160
|
+
criteria.should_values = (should_values || []) + args
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Adds a range filter to the criteria without being forced to specify the
|
165
|
+
# left and right end of the range, such that you can eg simply specify lt,
|
166
|
+
# lte, gt and gte. For fully specified ranges, you can as well use #where,
|
167
|
+
# etc. Check out the ElasticSearch docs for further details regarding the
|
168
|
+
# range filter.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# CommentIndex.range(:created_at, gte: Time.parse("2016-01-01"))
|
172
|
+
# CommentIndex.range(:likes_count, gt: 10, lt: 100)
|
173
|
+
#
|
174
|
+
# @param field [Symbol, String] The field name to specify the range for
|
175
|
+
# @param options [Hash] The range filter specification, like lt, lte, etc
|
176
|
+
#
|
177
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
178
|
+
|
179
|
+
def range(field, options = {})
|
180
|
+
filter range: { field => options }
|
181
|
+
end
|
182
|
+
|
183
|
+
# Adds a match all filter/query to the criteria, which simply matches all
|
184
|
+
# documents. This can be eg be used within filter aggregations or for
|
185
|
+
# filter chaining. Check out the ElasticSearch docs for further details.
|
186
|
+
#
|
187
|
+
# @example Basic usage
|
188
|
+
# CommentIndex.match_all
|
189
|
+
#
|
190
|
+
# @example Filter chaining
|
191
|
+
# query = CommentIndex.match_all
|
192
|
+
# query = query.where(public: true) unless current_user.admin?
|
193
|
+
#
|
194
|
+
# @example Filter aggregation
|
195
|
+
# query = CommentIndex.aggregate(filtered_tags: {}) do |aggregation|
|
196
|
+
# aggregation = aggregation.match_all
|
197
|
+
# aggregation = aggregation.where(user_id: current_user.id) if current_user
|
198
|
+
# aggregation = aggregation.aggregate(:tags)
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
# query.aggregations(:filtered_tags).tags.buckets.each { ... }
|
202
|
+
#
|
203
|
+
# @param options [Hash] Options for the match_all filter, like eg boost
|
204
|
+
#
|
205
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
206
|
+
|
207
|
+
def match_all(options = {})
|
208
|
+
filter match_all: options
|
209
|
+
end
|
210
|
+
|
211
|
+
# Adds an exists filter to the criteria, which selects all documents for
|
212
|
+
# which the specified field has a non-null value.
|
213
|
+
#
|
214
|
+
# @example
|
215
|
+
# CommentIndex.exists(:notified_at)
|
216
|
+
#
|
217
|
+
# @param field [Symbol, String] The field that should have a non-null value
|
218
|
+
#
|
219
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
220
|
+
|
221
|
+
def exists(field)
|
222
|
+
filter exists: { field: field }
|
223
|
+
end
|
224
|
+
|
225
|
+
# Adds an exists not filter to the criteria, which selects all documents
|
226
|
+
# for which the specified field's value is null.
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
# CommentIndex.exists_not(:notified_at)
|
230
|
+
#
|
231
|
+
# @param field [Symbol, String] The field that should have a null value
|
232
|
+
#
|
233
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
234
|
+
|
235
|
+
def exists_not(field)
|
236
|
+
must_not exists: { field: field }
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# @api private
|
4
|
+
#
|
5
|
+
# The SearchFlip::HTTPClient class wraps the http gem, is for internal use
|
6
|
+
# and responsible for the http request/response handling, ie communicating
|
7
|
+
# with ElasticSearch.
|
8
|
+
|
9
|
+
class HTTPClient
|
10
|
+
class Request
|
11
|
+
attr_accessor :headers_hash
|
12
|
+
|
13
|
+
def headers(hash = {})
|
14
|
+
dup.tap do |request|
|
15
|
+
request.headers_hash = (request.headers_hash || {}).merge(hash)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
[:get, :post, :put, :delete, :head].each do |method|
|
20
|
+
define_method method do |*args|
|
21
|
+
execute(method, *args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def execute(method, *args)
|
28
|
+
response = HTTP.headers(headers_hash || {}).send(method, *args)
|
29
|
+
|
30
|
+
raise SearchFlip::ResponseError.new(code: response.code, body: response.body.to_s) unless response.status.success?
|
31
|
+
|
32
|
+
response
|
33
|
+
rescue HTTP::ConnectionError => e
|
34
|
+
raise SearchFlip::ConnectionError, e.message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.request
|
39
|
+
Request.new
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
extend Forwardable
|
44
|
+
|
45
|
+
def_delegators :request, :headers, :get, :post, :put, :delete, :head
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|