waistband 0.4.2 → 0.7.0

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