search_flip 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+