waistband 0.4.2 → 0.7.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.
data/lib/waistband.rb CHANGED
@@ -9,7 +9,6 @@ module Waistband
9
9
  autoload :QueryResult, "waistband/query_result"
10
10
  autoload :QueryHelpers, "waistband/query_helpers"
11
11
  autoload :Query, "waistband/query"
12
- autoload :FreeQuery, "waistband/free_query"
13
12
  autoload :Index, "waistband/index"
14
13
  autoload :QuickError, "waistband/quick_error"
15
14
  autoload :Model, "waistband/model"
@@ -54,12 +54,8 @@ module Waistband
54
54
  connection.read @index, key
55
55
  end
56
56
 
57
- def query(term, options = {})
58
- ::Waistband::Query.new @index, term, options
59
- end
60
-
61
- def free_query(options = {})
62
- ::Waistband::FreeQuery.new @index, options
57
+ def query(options = {})
58
+ ::Waistband::Query.new @index, options
63
59
  end
64
60
 
65
61
  def search_url
@@ -1,7 +1,4 @@
1
- require 'json'
2
- require 'rest-client'
3
- require 'active_support/core_ext/object/blank'
4
- require 'kaminari/models/array_extension' if defined?(Kaminari)
1
+ require 'active_support/core_ext/hash/indifferent_access'
5
2
 
6
3
  module Waistband
7
4
  class Query
@@ -10,180 +7,26 @@ module Waistband
10
7
 
11
8
  attr_accessor :page, :page_size
12
9
 
13
- def initialize(index, search_term, options = {})
14
- @index = index
15
- @search_term = search_term
16
- @wildcards = []
17
- @fields = []
18
- @ranges = []
19
- @sorts = []
20
- @terms = {}
21
- @exclude_terms = {}
22
- @optional_terms = {}
23
- @page = (options[:page] || 1).to_i
24
- @page_size = (options[:page_size] || 20).to_i
10
+ def initialize(index, options = {})
11
+ @index = index
12
+ @page = (options[:page] || 1).to_i
13
+ @page_size = (options[:page_size] || 20).to_i
25
14
  end
26
15
 
27
- def add_match(field)
28
- @match = field
29
- end
30
-
31
- def add_fields(*fields)
32
- @fields |= fields
33
- @fields = @fields.compact.uniq
34
- end
35
- alias :add_field :add_fields
36
-
37
- def add_wildcard(wildcard, value)
38
- @wildcards << {
39
- wildcard: wildcard,
40
- value: value
41
- }
42
- end
43
-
44
- def add_range(field, min, max)
45
- @ranges << {
46
- field: field,
47
- min: min,
48
- max: max
49
- }
50
- end
51
-
52
- def add_terms(key, words)
53
- @terms[key] ||= {
54
- keywords: []
55
- }
56
- @terms[key][:keywords] += prep_words_uniquely(words)
57
- end
58
- alias :add_term :add_terms
59
-
60
- def add_exclude_terms(key, words)
61
- @exclude_terms[key] ||= {
62
- keywords: []
63
- }
64
- @exclude_terms[key][:keywords] += prep_words_uniquely(words)
65
- end
66
- alias :add_exclude_term :add_exclude_terms
67
-
68
- def add_optional_terms(key, words)
69
- @optional_terms[key] ||= {
70
- keywords: []
71
- }
72
- @optional_terms[key][:keywords] += prep_words_uniquely(words)
73
- end
74
- alias :add_optional_term :add_optional_terms
75
-
76
- def add_sort(key, ord)
77
- @sorts << {
78
- key: key,
79
- ord: ord
80
- }
81
- end
82
-
83
- def add_random_sort
84
- @random_sort = {
85
- _script: {
86
- script: "Math.random()",
87
- type: :number,
88
- params: {},
89
- order: :asc
90
- }
91
- }
16
+ def prepare(hash)
17
+ @hash = hash.with_indifferent_access
18
+ self
92
19
  end
93
20
 
94
21
  private
95
22
 
96
23
  def to_hash
97
- {
98
- query: {
99
- bool: {
100
- must: must_to_hash,
101
- must_not: must_not_to_hash,
102
- should: should_to_hash
103
- }
104
- },
105
- from: from,
106
- size: @page_size,
107
- sort: sort_to_hash
108
- }
109
- end
110
-
111
- def sort_to_hash
112
- return @random_sort if @random_sort
24
+ raise "No query has been prepared yet!" unless @hash
113
25
 
114
- sort = []
115
-
116
- @sorts.each do |s|
117
- sort << {s[:key] => s[:ord]}
118
- end
119
-
120
- sort << '_score'
121
-
122
- sort
123
- end
124
-
125
- def must_to_hash
126
- must = []
127
-
128
- must << {
129
- multi_match: {
130
- query: @search_term,
131
- fields: @fields
132
- }
133
- } if @fields.any?
134
-
135
- @wildcards.each do |wc|
136
- must << {
137
- wildcard: {
138
- wc[:wildcard] => wc[:value]
139
- }
140
- }
141
- end
142
-
143
- @ranges.each do |range|
144
- must << {
145
- range: {
146
- range[:field] => {
147
- lte: range[:max],
148
- gte: range[:min],
149
- }
150
- }
151
- }
152
- end
153
-
154
- must << {
155
- match: {
156
- @match => @search_term
157
- }
158
- } if @match.present?
159
-
160
- prep_term_hash(@terms).each do |term|
161
- must << term
162
- end
163
-
164
- must
165
- end
166
-
167
- def must_not_to_hash
168
- prep_term_hash(@exclude_terms).map { |term| term }
169
- end
170
-
171
- def should_to_hash
172
- prep_term_hash(@optional_terms).map { |term| term }
173
- end
174
-
175
- def prep_term_hash(terms)
176
- terms.map do |key, term|
177
- {
178
- terms: {
179
- key.to_sym => term[:keywords]
180
- }
181
- }
182
- end
183
- end
26
+ @hash[:from] = from unless @hash[:from]
27
+ @hash[:size] = @page_size unless @hash[:size]
184
28
 
185
- def prep_words_uniquely(val)
186
- val.to_s.gsub(/ +/, ' ').strip.split(' ').uniq
29
+ @hash
187
30
  end
188
31
 
189
32
  # /private
@@ -1,3 +1,3 @@
1
1
  module Waistband
2
- VERSION = "0.4.2"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -32,11 +32,7 @@ describe Waistband::Index do
32
32
  end
33
33
 
34
34
  it "proxies to a query" do
35
- index.query('shopping').should be_a Waistband::Query
36
- end
37
-
38
- it "proxies to a free query" do
39
- index.free_query.should be_a Waistband::FreeQuery
35
+ index.query.should be_a Waistband::Query
40
36
  end
41
37
 
42
38
  describe "storing" do
@@ -1,373 +1,161 @@
1
1
  require 'spec_helper'
2
- require 'active_support/core_ext/kernel/reporting'
3
-
4
- Kernel.silence_stderr do
5
- require 'kaminari'
6
- end
7
2
 
8
3
  describe Waistband::Query do
9
4
 
10
- let(:index) { Waistband::Index.new('search') }
11
- let(:query) { index.query('shopping ikea') }
12
-
13
- describe '#execute!' do
14
-
15
- before { add_result! }
16
-
17
- it "gets results from elastic search" do
18
- json = query.send(:execute!)
19
-
20
- json['hits'].should be_a Hash
21
- json['hits']['total'].should > 0
22
- json['hits']['hits'].size.should eql 2
23
-
24
- hit = json['hits']['hits'].first
25
-
26
- hit['_id'].should match(/^task_.*/)
27
- hit['_source'].should be_a Hash
28
- hit['_source']['id'].should eql 123123
29
- hit['_source']['name'].should eql 'some shopping in ikea'
30
- hit['_source']['user_id'].should eql 999
31
- hit['_source']['description'].should eql 'i need you to pick up some stuff in ikea'
32
- end
33
-
34
- it "boosts results with optional terms" do
35
- query = index.query('shopping ikea')
36
- query.add_field('name')
37
- query.add_optional_term('internal', 'true')
38
-
39
- json = query.send(:execute!)
40
-
41
- json['hits']['hits'].size.should eql 2
42
-
43
- hit = json['hits']['hits'].first
44
-
45
- hit['_id'].should match(/^task_.*/)
46
- hit['_source'].should be_a Hash
47
- hit['_source']['id'].should eql 234234
48
- hit['_source']['name'].should eql "some shopping in ikea and trader joe's"
49
- hit['_source']['user_id'].should eql 987
50
- hit['_source']['description'].should eql 'pick me up some eggs'
51
- end
52
-
53
- it "excludes results" do
54
- query = index.query('shopping ikea')
55
- query.add_field('name')
56
- query.add_exclude_term('internal', 'true')
57
-
58
- json = query.send(:execute!)
59
-
60
- json['hits']['hits'].size.should eql 1
61
-
62
- hit = json['hits']['hits'].first
63
-
64
- hit['_id'].should match(/^task_.*/)
65
- hit['_source'].should be_a Hash
66
- hit['_source']['id'].should eql 123123
67
- hit['_source']['name'].should eql 'some shopping in ikea'
68
- hit['_source']['user_id'].should eql 999
69
- hit['_source']['description'].should eql 'i need you to pick up some stuff in ikea'
70
- hit['_source']['internal'].should be_false
71
- end
72
-
73
- end
74
-
75
- describe '#add_sort' do
76
-
77
- it "adds sort field" do
78
- query.add_sort('created_at', 'asc')
79
- query.instance_variable_get('@sorts').should eql [{key: 'created_at', ord: 'asc'}]
80
-
81
- query.add_sort('updated_at', 'desc')
82
- query.instance_variable_get('@sorts').should eql [{key: 'created_at', ord: 'asc'}, {key: 'updated_at', ord: 'desc'}]
83
- end
84
-
85
- end
86
-
87
- describe '#add_random_sort' do
88
-
89
- it "sets the correct instance variables" do
90
- query.add_random_sort
91
-
92
- query.instance_variable_get('@random_sort').should eql({
93
- _script: {
94
- script: "Math.random()",
95
- type: :number,
96
- params: {},
97
- order: :asc
98
- }
99
- })
100
-
101
- query.send(:sort_to_hash).should eql({
102
- _script: {
103
- script: "Math.random()",
104
- type: :number,
105
- params: {},
106
- order: :asc
5
+ let(:index) { Waistband::Index.new('search') }
6
+ let(:geo_index) { Waistband::Index.new('geo') }
7
+
8
+ let(:query) { Waistband::Query.new('search') }
9
+ let(:geo_query) { Waistband::Query.new('geo') }
10
+
11
+ let(:attrs) do
12
+ {
13
+ 'query' => {
14
+ 'bool' => {
15
+ 'must' => [
16
+ {
17
+ 'multi_match' => {
18
+ 'query' => "shopping ikea",
19
+ 'fields' => ['name']
20
+ },
21
+ }
22
+ ]
107
23
  }
108
- })
109
- end
110
-
111
- it "sorts restuls randomly" do
112
- add_result!
113
-
114
- query = index.query('shopping ikea')
115
-
116
- query.add_field('name')
117
- query.add_random_sort
118
-
119
- query.results.size.should eql 2
120
- end
121
-
122
- end
123
-
124
- describe '#add_range' do
125
-
126
- it "adds a range" do
127
- query.add_range('task_id', 5, 10)
128
- query.instance_variable_get('@ranges').should eql [{field: 'task_id', min: 5, max: 10}]
129
- end
130
-
131
- end
132
-
133
- describe '#add_field' do
134
-
135
- it "adds field" do
136
- query.add_field('name')
137
- query.instance_variable_get('@fields').should eql ['name']
138
- end
139
-
140
- it "adds multiple fields at once" do
141
- query.add_field('name', 'description')
142
- query.instance_variable_get('@fields').should eql ['name', 'description']
143
- end
144
-
24
+ }
25
+ }
145
26
  end
146
27
 
147
- describe '#add_term' do
148
-
149
- it "adds the term on the key" do
150
- query.add_term('metro', 'boston')
151
- query.instance_variable_get('@terms')['metro'][:keywords].should eql ['boston']
152
- end
153
-
154
- it "adds several terms on multiple words" do
155
- query.add_term('metro', 'sf bay area')
156
- query.instance_variable_get('@terms')['metro'][:keywords].should eql ['sf', 'bay', 'area']
157
- end
28
+ it "correclty forms a query hash" do
29
+ add_result!
30
+ query.prepare(attrs)
158
31
 
32
+ expect(query.instance_variable_get('@hash')).to eql(attrs)
159
33
  end
160
34
 
161
- describe '#add_exclude_term' do
35
+ it "finds results for a query" do
36
+ add_result!
37
+ query.prepare(attrs)
38
+ json = query.send(:execute!)
162
39
 
163
- it "adds the term on the key" do
164
- query.add_exclude_term('metro', 'boston')
165
- query.instance_variable_get('@exclude_terms')['metro'][:keywords].should eql ['boston']
166
- end
40
+ json['hits'].should be_a Hash
41
+ json['hits']['total'].should > 0
42
+ json['hits']['hits'].size.should eql 2
167
43
 
168
- it "adds several terms on multiple words" do
169
- query.add_exclude_term('metro', 'sf bay area')
170
- query.instance_variable_get('@exclude_terms')['metro'][:keywords].should eql ['sf', 'bay', 'area']
171
- end
44
+ hit = json['hits']['hits'].first
172
45
 
46
+ hit['_id'].should match(/^task_.*/)
47
+ hit['_source'].should be_a Hash
48
+ hit['_source']['id'].should eql 123123
49
+ hit['_source']['name'].should eql 'some shopping in ikea'
50
+ hit['_source']['user_id'].should eql 999
51
+ hit['_source']['description'].should eql 'i need you to pick up some stuff in ikea'
173
52
  end
174
53
 
175
- describe '#add_optional_term' do
176
-
177
- it "adds the term on the key" do
178
- query.add_optional_term('metro', 'boston')
179
- query.instance_variable_get('@optional_terms')['metro'][:keywords].should eql ['boston']
180
- end
181
-
182
- it "adds several terms on multiple words" do
183
- query.add_optional_term('metro', 'sf bay area')
184
- query.instance_variable_get('@optional_terms')['metro'][:keywords].should eql ['sf', 'bay', 'area']
185
- end
186
-
187
- end
188
-
189
- describe '#must_to_hash' do
190
-
191
- it "creates an array of the must of the query" do
192
- query.add_term('metro', 'sf bay area')
193
- query.add_field('name')
194
- query.send(:must_to_hash).should eql([
195
- {
196
- multi_match: {
197
- query: "shopping ikea",
198
- fields: ['name']
199
- }
200
- },
201
- {
202
- terms: {
203
- metro: ["sf", "bay", "area"]
54
+ it "permits storing and fetching geo results" do
55
+ add_geo_results!
56
+ geo_query.prepare({
57
+ "query" => {
58
+ "filtered" => {
59
+ "query" => { "match_all" => {} },
60
+ "filter" => {
61
+ "geo_shape" => {
62
+ "work_area" => {
63
+ "relation" => "intersects",
64
+ "shape" => {
65
+ "type" => "Point",
66
+ "coordinates" => [-122.39455,37.7841]
67
+ }
68
+ }
69
+ }
204
70
  }
205
71
  }
206
- ])
207
- end
208
-
209
- end
210
-
211
- describe '#must_not_to_hash' do
212
-
213
- it "creates an array of the must of the query" do
214
- query.add_exclude_term('metro', 'sf bay area')
215
-
216
- query.send(:must_not_to_hash).should eql([
217
- {
218
- terms: {
219
- metro: ["sf", "bay", "area"]
72
+ }
73
+ })
74
+
75
+ json = geo_query.send(:execute!)
76
+
77
+ json['hits'].should be_a Hash
78
+ json['hits']['total'].should eql 3
79
+ json['hits']['hits'].size.should eql 3
80
+
81
+ geo_query.prepare({
82
+ "query" => {
83
+ "filtered" => {
84
+ "query" => { "match_all" => {} },
85
+ "filter" => {
86
+ "geo_shape" => {
87
+ "work_area" => {
88
+ "relation" => "intersects",
89
+ "shape" => {
90
+ "type" => "Point",
91
+ "coordinates" => [-122.3859222222222,37.78292222222222]
92
+ }
93
+ }
94
+ }
220
95
  }
221
96
  }
222
- ])
223
- end
224
-
225
- end
226
-
227
- describe '#from' do
228
-
229
- it "returns 0 when page is 1" do
230
- Waistband::Query.new('search', 'shopping ikea', page: 1, page_size: 20).send(:from).should eql 0
231
- end
232
-
233
- it "returns 20 when page is 2 and page_size is 20" do
234
- Waistband::Query.new('search', 'shopping ikea', page: 2, page_size: 20).send(:from).should eql 20
235
- end
236
-
237
- it "returns 30 when page is 4 and page_size is 10" do
238
- Waistband::Query.new('search', 'shopping ikea', page: 4, page_size: 10).send(:from).should eql 30
239
- end
240
-
241
- it "returns 10 when page is 2 and page_size is 10" do
242
- Waistband::Query.new('search', 'shopping ikea', page: 2, page_size: 10).send(:from).should eql 10
243
- end
97
+ }
98
+ })
244
99
 
245
- end
246
-
247
- describe '#to_hash' do
248
-
249
- it "constructs the query's json" do
250
- query.add_term('metro', 'sf bay area')
251
- query.add_field('name')
252
- query.add_sort('created_at', 'desc')
253
- query.add_range('task_id', 1, 10)
254
-
255
- query.send(:to_hash).should eql({
256
- query: {
257
- bool: {
258
- must: [
259
- {
260
- multi_match: {
261
- query: "shopping ikea",
262
- fields: ['name']
263
- },
264
- },
265
- {
266
- range: {
267
- 'task_id' => {
268
- lte: 10,
269
- gte: 1
270
- }
271
- }
272
- },
273
- {
274
- terms: {
275
- metro: ["sf","bay","area"]
276
- }
277
- }
278
- ],
279
- must_not: [],
280
- should: []
281
- }
282
- },
283
- from: 0,
284
- size: 20,
285
- sort: [
286
- {'created_at' => 'desc'},
287
- '_score'
288
- ]
289
- })
290
- end
291
-
292
- it 'constructs the query with several terms' do
293
- query.add_term('metro', 'sf bay area')
294
- query.add_term('geography', 'San Francisco')
295
- query.add_optional_term('internal', 'true')
296
- query.add_exclude_term('user_id', '999')
297
- query.add_field('name')
298
- query.add_field('description')
299
-
300
- query.send(:to_hash).should eql({
301
- query: {
302
- bool: {
303
- must: [
304
- {
305
- multi_match: {
306
- query: "shopping ikea",
307
- fields: ['name', 'description']
308
- }
309
- },
310
- {
311
- terms: {
312
- metro: ["sf", "bay", "area"]
313
- }
314
- },
315
- {
316
- terms: {
317
- geography: ["San", "Francisco"]
318
- }
319
- }
320
- ],
321
- must_not: [
322
- {
323
- terms: {
324
- user_id: ['999']
325
- }
326
- }
327
- ],
328
- should: [
329
- {
330
- terms: {
331
- internal: ['true']
332
- }
333
- }
334
- ]
335
- }
336
- },
337
- from: 0,
338
- size: 20,
339
- sort: ['_score']
340
- })
341
- end
342
-
343
- end
344
-
345
- describe '#results' do
346
-
347
- it "returns a QueryResult array" do
348
- add_result!
349
-
350
- query.results.first.should be_a Waistband::QueryResult
351
- end
352
-
353
- end
354
-
355
- describe '#paginated_results' do
356
-
357
- it "returns a kaminari paginated array" do
358
- add_result!
359
-
360
- query.paginated_results.should be_an Array
361
- end
100
+ json = geo_query.send(:execute!)
362
101
 
102
+ json['hits'].should be_a Hash
103
+ json['hits']['total'].should eql 1
104
+ json['hits']['hits'].size.should eql 1
363
105
  end
364
106
 
365
107
  def add_result!
366
108
  index.store!("task_123123", {id: 123123, name: 'some shopping in ikea', user_id: 999, description: 'i need you to pick up some stuff in ikea', internal: false})
367
109
  index.store!("task_234234", {id: 234234, name: "some shopping in ikea and trader joe's", user_id: 987, description: 'pick me up some eggs', internal: true})
368
110
  index.refresh
111
+ end
369
112
 
370
- query.add_field('name')
113
+ def add_geo_results!
114
+ geo_index.store!("rabbit_1", {
115
+ id: "rabbit_1",
116
+ work_area: {
117
+ type: 'polygon',
118
+ coordinates: [[
119
+ [-122.4119,37.78211],
120
+ [-122.39285,37.79649],
121
+ [-122.37997,37.78415],
122
+ [-122.39817,37.77248],
123
+ [-122.40932,37.77302],
124
+ [-122.4119,37.78211]
125
+ ]]
126
+ }
127
+ })
128
+
129
+ geo_index.store!("rabbit_2", {
130
+ id: "rabbit_2",
131
+ work_area: {
132
+ type: 'polygon',
133
+ coordinates: [[
134
+ [-122.41087,37.78822],
135
+ [-122.43174,37.78578],
136
+ [-122.42816,37.75938],
137
+ [-122.38787,37.76882],
138
+ [-122.39199,37.78584],
139
+ [-122.41087,37.78822]
140
+ ]]
141
+ }
142
+ })
143
+
144
+ geo_index.store!("rabbit_3", {
145
+ id: "rabbit_3",
146
+ work_area: {
147
+ type: 'polygon',
148
+ coordinates: [[
149
+ [-122.41997,37.79744],
150
+ [-122.39576,37.79975],
151
+ [-122.38461,37.78469],
152
+ [-122.40294,37.77738],
153
+ [-122.41997,37.79744]
154
+ ]]
155
+ }
156
+ })
157
+
158
+ geo_index.refresh
371
159
  end
372
160
 
373
161
  end