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.
@@ -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
+