slingshot-rb 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +1 -0
  2. data/README.markdown +276 -50
  3. data/examples/rails-application-template.rb +144 -0
  4. data/examples/slingshot-dsl.rb +272 -102
  5. data/lib/slingshot.rb +13 -0
  6. data/lib/slingshot/client.rb +10 -1
  7. data/lib/slingshot/dsl.rb +17 -1
  8. data/lib/slingshot/index.rb +109 -7
  9. data/lib/slingshot/model/callbacks.rb +23 -0
  10. data/lib/slingshot/model/import.rb +18 -0
  11. data/lib/slingshot/model/indexing.rb +50 -0
  12. data/lib/slingshot/model/naming.rb +30 -0
  13. data/lib/slingshot/model/persistence.rb +34 -0
  14. data/lib/slingshot/model/persistence/attributes.rb +60 -0
  15. data/lib/slingshot/model/persistence/finders.rb +61 -0
  16. data/lib/slingshot/model/persistence/storage.rb +75 -0
  17. data/lib/slingshot/model/search.rb +97 -0
  18. data/lib/slingshot/results/collection.rb +35 -10
  19. data/lib/slingshot/results/item.rb +10 -7
  20. data/lib/slingshot/results/pagination.rb +30 -0
  21. data/lib/slingshot/rubyext/symbol.rb +11 -0
  22. data/lib/slingshot/search.rb +3 -2
  23. data/lib/slingshot/search/facet.rb +8 -6
  24. data/lib/slingshot/search/filter.rb +7 -8
  25. data/lib/slingshot/search/highlight.rb +1 -3
  26. data/lib/slingshot/search/query.rb +4 -0
  27. data/lib/slingshot/search/sort.rb +5 -0
  28. data/lib/slingshot/tasks.rb +88 -0
  29. data/lib/slingshot/version.rb +1 -1
  30. data/slingshot.gemspec +17 -4
  31. data/test/integration/active_model_searchable_test.rb +80 -0
  32. data/test/integration/active_record_searchable_test.rb +193 -0
  33. data/test/integration/highlight_test.rb +1 -1
  34. data/test/integration/index_mapping_test.rb +1 -1
  35. data/test/integration/index_store_test.rb +27 -0
  36. data/test/integration/persistent_model_test.rb +35 -0
  37. data/test/integration/query_string_test.rb +3 -3
  38. data/test/integration/sort_test.rb +2 -2
  39. data/test/models/active_model_article.rb +31 -0
  40. data/test/models/active_model_article_with_callbacks.rb +49 -0
  41. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  42. data/test/models/active_record_article.rb +12 -0
  43. data/test/models/persistent_article.rb +11 -0
  44. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  45. data/test/models/supermodel_article.rb +22 -0
  46. data/test/models/validated_model.rb +11 -0
  47. data/test/test_helper.rb +4 -0
  48. data/test/unit/active_model_lint_test.rb +17 -0
  49. data/test/unit/client_test.rb +4 -0
  50. data/test/unit/configuration_test.rb +4 -0
  51. data/test/unit/index_test.rb +240 -17
  52. data/test/unit/model_callbacks_test.rb +90 -0
  53. data/test/unit/model_import_test.rb +71 -0
  54. data/test/unit/model_persistence_test.rb +400 -0
  55. data/test/unit/model_search_test.rb +289 -0
  56. data/test/unit/results_collection_test.rb +69 -7
  57. data/test/unit/results_item_test.rb +8 -14
  58. data/test/unit/rubyext_hash_test.rb +19 -0
  59. data/test/unit/search_facet_test.rb +25 -7
  60. data/test/unit/search_filter_test.rb +3 -0
  61. data/test/unit/search_query_test.rb +11 -0
  62. data/test/unit/search_sort_test.rb +8 -0
  63. data/test/unit/search_test.rb +14 -0
  64. data/test/unit/slingshot_test.rb +38 -0
  65. metadata +133 -26
@@ -0,0 +1,90 @@
1
+ require 'test_helper'
2
+
3
+ class ModelOne
4
+ include Slingshot::Model::Search
5
+ include Slingshot::Model::Callbacks
6
+
7
+ def save; false; end
8
+ def destroy; false; end
9
+ end
10
+
11
+ class ModelTwo
12
+ extend ActiveModel::Callbacks
13
+ define_model_callbacks :save, :destroy
14
+
15
+ include Slingshot::Model::Search
16
+ include Slingshot::Model::Callbacks
17
+
18
+ def save
19
+ _run_save_callbacks {}
20
+ end
21
+
22
+ def destroy
23
+ _run_destroy_callbacks { @destroyed = true }
24
+ end
25
+
26
+ def destroyed?; !!@destroyed; end
27
+ end
28
+
29
+ class ModelThree
30
+ extend ActiveModel::Callbacks
31
+ define_model_callbacks :save, :destroy
32
+
33
+ include Slingshot::Model::Search
34
+ include Slingshot::Model::Callbacks
35
+
36
+ def save
37
+ _run_save_callbacks {}
38
+ end
39
+
40
+ def destroy
41
+ _run_destroy_callbacks {}
42
+ end
43
+ end
44
+
45
+ module Slingshot
46
+ module Model
47
+
48
+ class ModelCallbacksTest < Test::Unit::TestCase
49
+
50
+ context "Model without ActiveModel callbacks" do
51
+
52
+ should "not execute any callbacks" do
53
+ ModelOne.any_instance.expects(:update_elastic_search_index).never
54
+
55
+ ModelOne.new.save
56
+ ModelOne.new.destroy
57
+ end
58
+
59
+ end
60
+
61
+ context "Model with ActiveModel callbacks and implemented destroyed? method" do
62
+
63
+ should "execute the callbacks" do
64
+ ModelTwo.any_instance.expects(:update_elastic_search_index).twice
65
+
66
+ ModelTwo.new.save
67
+ ModelTwo.new.destroy
68
+ end
69
+
70
+ end
71
+
72
+ context "Model with ActiveModel callbacks without destroyed? method implemented" do
73
+
74
+ should "have the destroyed? method added" do
75
+ assert_respond_to ModelThree.new, :destroyed?
76
+ end
77
+
78
+ should "execute the callbacks" do
79
+ ModelThree.any_instance.expects(:update_elastic_search_index).twice
80
+
81
+ ModelThree.new.save
82
+ ModelThree.new.destroy
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class ImportModel
4
+ extend ActiveModel::Naming
5
+ include Slingshot::Model::Search
6
+ include Slingshot::Model::Callbacks
7
+
8
+ DATA = (1..4).to_a
9
+
10
+ def self.paginate(options={})
11
+ options = {:page => 1, :per_page => 1000}.update options
12
+ DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] )
13
+ end
14
+
15
+ def self.all(options={})
16
+ DATA
17
+ end
18
+
19
+ def self.count
20
+ DATA.size
21
+ end
22
+ end
23
+
24
+ module Slingshot
25
+ module Model
26
+
27
+ class ImportTest < Test::Unit::TestCase
28
+
29
+ context "Model::Import" do
30
+
31
+ should "have the import method" do
32
+ assert_respond_to ImportModel, :import
33
+ end
34
+
35
+ should "paginate the results by default when importing" do
36
+ Slingshot::Index.any_instance.expects(:bulk_store).returns(true).times(2)
37
+
38
+ ImportModel.import :per_page => 2
39
+ end
40
+
41
+ should "call the passed block on every batch, and NOT manipulate the documents array" do
42
+ Slingshot::Index.any_instance.expects(:bulk_store).with([1, 2])
43
+ Slingshot::Index.any_instance.expects(:bulk_store).with([3, 4])
44
+
45
+ runs = 0
46
+ ImportModel.import :per_page => 2 do |documents|
47
+ runs += 1
48
+ # Don't forget to return the documents at the end of the block
49
+ documents
50
+ end
51
+
52
+ assert_equal 2, runs
53
+ end
54
+
55
+ should "manipulate the documents in passed block" do
56
+ Slingshot::Index.any_instance.expects(:bulk_store).with([2, 3])
57
+ Slingshot::Index.any_instance.expects(:bulk_store).with([4, 5])
58
+
59
+ ImportModel.import :per_page => 2 do |documents|
60
+ # Add 1 to every "document" and return them
61
+ documents.map { |d| d + 1 }
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,400 @@
1
+ require 'test_helper'
2
+
3
+ module Slingshot
4
+ module Model
5
+
6
+ class PersistenceTest < Test::Unit::TestCase
7
+
8
+ context "Model" do
9
+
10
+ should "have default index name" do
11
+ assert_equal 'persistent_articles', PersistentArticle.index_name
12
+ assert_equal 'persistent_articles', PersistentArticle.new(:title => 'Test').index_name
13
+ end
14
+
15
+ should "allow to set custom index name" do
16
+ assert_equal 'custom-index-name', PersistentArticleWithCustomIndexName.index_name
17
+
18
+ PersistentArticleWithCustomIndexName.index_name "another-index-name"
19
+ assert_equal 'another-index-name', PersistentArticleWithCustomIndexName.index_name
20
+ assert_equal 'another-index-name', PersistentArticleWithCustomIndexName.index.name
21
+ end
22
+
23
+ should "have document_type" do
24
+ assert_equal 'persistent_article', PersistentArticle.document_type
25
+ assert_equal 'persistent_article', PersistentArticle.new(:title => 'Test').document_type
26
+ end
27
+
28
+ should "allow to define property" do
29
+ assert_nothing_raised do
30
+ a = PersistentArticle.new
31
+ class << a
32
+ property :status
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ context "Finders" do
40
+
41
+ setup do
42
+ @first = { '_id' => 1, '_source' => { :title => 'First' } }
43
+ @second = { '_id' => 2, '_source' => { :title => 'Second' } }
44
+ @third = { '_id' => 3, '_source' => { :title => 'Third' } }
45
+ @find_all = { 'hits' => { 'hits' => [
46
+ @first,
47
+ @second,
48
+ @third
49
+ ] } }
50
+ @find_first = { 'hits' => { 'hits' => [ @first ] } }
51
+ @find_last_two = { 'hits' => { 'hits' => [ @second, @third ] } }
52
+ end
53
+
54
+ should "find document by numeric ID" do
55
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
56
+ document = PersistentArticle.find 1
57
+
58
+ assert_instance_of PersistentArticle, document
59
+ assert_equal 1, document.id
60
+ assert_equal 1, document.attributes['id']
61
+ assert_equal 'First', document.attributes['title']
62
+ assert_equal 'First', document.title
63
+ end
64
+
65
+ should "find document by string ID" do
66
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
67
+ document = PersistentArticle.find '1'
68
+
69
+ assert_instance_of PersistentArticle, document
70
+ assert_equal 1, document.id
71
+ assert_equal 1, document.attributes['id']
72
+ assert_equal 'First', document.attributes['title']
73
+ assert_equal 'First', document.title
74
+ end
75
+
76
+ should "find document by list of IDs" do
77
+ Configuration.client.expects(:post).returns(mock_response(@find_last_two.to_json))
78
+ documents = PersistentArticle.find 2, 3
79
+
80
+ assert_equal 2, documents.count
81
+ end
82
+
83
+ should "find document by array of IDs" do
84
+ Configuration.client.expects(:post).returns(mock_response(@find_last_two.to_json))
85
+ documents = PersistentArticle.find [2, 3]
86
+
87
+ assert_equal 2, documents.count
88
+ end
89
+
90
+ should "find all documents" do
91
+ Configuration.client.stubs(:post).returns(mock_response(@find_all.to_json))
92
+ documents = PersistentArticle.all
93
+
94
+ assert_equal 3, documents.count
95
+ assert_equal 'First', documents.first.attributes['title']
96
+ assert_equal PersistentArticle.find(:all).map { |e| e.id }, PersistentArticle.all.map { |e| e.id }
97
+ end
98
+
99
+ should "find first document" do
100
+ Configuration.client.expects(:post).returns(mock_response(@find_first.to_json))
101
+ document = PersistentArticle.first
102
+
103
+ assert_equal 'First', document.attributes['title']
104
+ end
105
+
106
+ should "raise error when passing incorrect argument" do
107
+ assert_raise(ArgumentError) do
108
+ PersistentArticle.find :name => 'Test'
109
+ end
110
+ end
111
+
112
+ should_eventually "raise error when document is not found" do
113
+ assert_raise(DocumentNotFound) do
114
+ PersistentArticle.find 'xyz001'
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ context "Persistent model" do
121
+
122
+ setup { @article = PersistentArticle.new :title => 'Test', :tags => [:one, :two] }
123
+
124
+ context "attribute methods" do
125
+
126
+ should "allow to set attributes on initialization" do
127
+ assert_not_nil @article.attributes
128
+ assert_equal 'Test', @article.attributes['title']
129
+ end
130
+
131
+ should "allow to leave attributes blank on initialization" do
132
+ assert_nothing_raised { PersistentArticle.new }
133
+ end
134
+
135
+ should "have getter methods for attributes" do
136
+ assert_not_nil @article.title
137
+ assert_equal 'Test', @article.title
138
+ assert_equal [:one, :two], @article.tags
139
+ end
140
+
141
+ should "have getter methods for attribute passed as a String" do
142
+ article = PersistentArticle.new 'title' => 'Tony Montana'
143
+ assert_not_nil article.title
144
+ assert_equal 'Tony Montana', article.title
145
+ end
146
+
147
+ should "raise error when getting unknown attribute" do
148
+ assert_raise(NoMethodError) do
149
+ @article.krapulitz
150
+ end
151
+ end
152
+
153
+ should "not raise error when getting unset attribute" do
154
+ article = PersistentArticle.new :title => 'Test'
155
+
156
+ assert_nothing_raised { article.published_on }
157
+ assert_nil article.published_on
158
+ end
159
+
160
+ should_eventually "return default value for attribute" do
161
+ article = PersistentArticle.new :title => 'Test'
162
+ article.class_eval do
163
+ property :title
164
+ property :tags, :default => []
165
+ end
166
+
167
+ assert_nothing_raised { article.tags }
168
+ assert_equal [], article.tags
169
+ end
170
+
171
+ should "have query method for attribute" do
172
+ assert_equal true, @article.title?
173
+ end
174
+
175
+ should "raise error when querying for unknown attribute" do
176
+ assert_raise(NoMethodError) do
177
+ @article.krapulitz?
178
+ end
179
+ end
180
+
181
+ should "not raise error when querying for unset attribute" do
182
+ article = PersistentArticle.new :title => 'Test'
183
+
184
+ assert_nothing_raised { article.published_on? }
185
+ assert ! article.published_on?
186
+ end
187
+
188
+ should "return true for respond_to? calls for set attributes" do
189
+ article = PersistentArticle.new :title => 'Test'
190
+ assert article.respond_to?(:title)
191
+ end
192
+
193
+ should "return false for respond_to? calls for unknown attributes" do
194
+ article = PersistentArticle.new :title => 'Test'
195
+ assert ! article.respond_to?(:krapulitz)
196
+ end
197
+
198
+ should "return true for respond_to? calls for defined but unset attributes" do
199
+ article = PersistentArticle.new :title => 'Test'
200
+
201
+ assert article.respond_to?(:published_on)
202
+ end
203
+
204
+ should "have attribute names" do
205
+ article = PersistentArticle.new :title => 'Test', :tags => ['one', 'two']
206
+ assert_equal ['published_on', 'tags', 'title'], article.attribute_names
207
+ end
208
+
209
+ should "have setter method for attribute" do
210
+ @article.title = 'Updated'
211
+ assert_equal 'Updated', @article.title
212
+ assert_equal 'Updated', @article.attributes['title']
213
+ end
214
+
215
+ should_eventually "allow to set deeply nested attributes on initialization" do
216
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
217
+
218
+ assert_equal 'John', article.author.first_name
219
+ assert_equal 'Smith', article.author.last_name
220
+ assert_equal({ :first_name => 'John', :last_name => 'Smith' }, article.attributes['author'])
221
+ end
222
+
223
+ should_eventually "allow to set deeply nested attributes on update" do
224
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
225
+
226
+ article.author.first_name = 'Robert'
227
+ article.author.last_name = 'Carpenter'
228
+
229
+ assert_equal 'Robert', article.author.first_name
230
+ assert_equal 'Carpenter', article.author.last_name
231
+ assert_equal({ :first_name => 'Robert', :last_name => 'Carpenter' }, article.attributes['author'])
232
+ end
233
+
234
+ end
235
+
236
+ context "when creating" do
237
+
238
+ should "save the document with generated ID in the database" do
239
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
240
+ '{"title":"Test","tags":["one","two"],"published_on":null}').
241
+ returns(mock_response('{"ok":true,"_id":"abc123"}'))
242
+ article = PersistentArticle.create :title => 'Test', :tags => [:one, :two]
243
+
244
+ assert article.persisted?, "#{article.inspect} should be `persisted?`"
245
+ assert_equal 'abc123', article.id
246
+ end
247
+
248
+ should "save the document with custom ID in the database" do
249
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/r2d2",
250
+ '{"title":"Test","id":"r2d2","tags":null,"published_on":null}').
251
+ returns(mock_response('{"ok":true,"_id":"r2d2"}'))
252
+ article = PersistentArticle.create :id => 'r2d2', :title => 'Test'
253
+
254
+ assert_equal 'r2d2', article.id
255
+ end
256
+
257
+ should "perform model validations" do
258
+ Configuration.client.expects(:post).never
259
+
260
+ assert ! ValidatedModel.create(:name => nil)
261
+ end
262
+
263
+ end
264
+
265
+ context "when creating" do
266
+
267
+ should "set the id property" do
268
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
269
+ {:title => 'Test', :tags => nil, :published_on => nil}.to_json).
270
+ returns(mock_response('{"ok":true,"_id":"1"}'))
271
+
272
+ article = PersistentArticle.create :title => 'Test'
273
+ assert_equal '1', article.id
274
+ end
275
+
276
+ should "not set the id property if already set" do
277
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
278
+ '{"title":"Test","id":"123","tags":null,"published_on":null}').
279
+ returns(mock_response('{"ok":true, "_id":"XXX"}'))
280
+
281
+ article = PersistentArticle.create :id => '123', :title => 'Test'
282
+ assert_equal '123', article.id
283
+ end
284
+
285
+ end
286
+
287
+ context "when saving" do
288
+
289
+ should "save the document with updated attribute" do
290
+ article = PersistentArticle.new :id => 1, :title => 'Test'
291
+
292
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/1",
293
+ '{"title":"Test","id":1,"tags":null,"published_on":null}').
294
+ returns(mock_response('{"ok":true,"_id":"1"}'))
295
+ assert article.save
296
+
297
+ article.title = 'Updated'
298
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/1",
299
+ '{"title":"Updated","id":1,"tags":null,"published_on":null}').
300
+ returns(mock_response('{"ok":true,"_id":"1"}'))
301
+ assert article.save
302
+ end
303
+
304
+ should "perform validations" do
305
+ article = ValidatedModel.new :name => nil
306
+ assert ! article.save
307
+ end
308
+
309
+ should "set the id property" do
310
+ article = PersistentArticle.new
311
+ article.title = 'Test'
312
+
313
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
314
+ article.to_indexed_json).
315
+ returns(mock_response('{"ok":true,"_id":"1"}'))
316
+ assert article.save
317
+ assert_equal '1', article.id
318
+ end
319
+
320
+ should "not set the id property if already set" do
321
+ article = PersistentArticle.new
322
+ article.id = '123'
323
+ article.title = 'Test'
324
+
325
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
326
+ '{"title":"Test","id":"123","tags":null,"published_on":null}').
327
+ returns(mock_response('{"ok":true,"_id":"XXX"}'))
328
+ assert article.save
329
+ assert_equal '123', article.id
330
+ end
331
+
332
+ end
333
+
334
+ context "when destroying" do
335
+
336
+ should "delete the document from the database" do
337
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
338
+ '{"title":"Test","id":"123","tags":null,"published_on":null}').
339
+ returns(mock_response('{"ok":true,"_id":"123"}'))
340
+ Configuration.client.expects(:delete).with("#{Configuration.url}/persistent_articles/persistent_article/123")
341
+
342
+ article = PersistentArticle.new :id => '123', :title => 'Test'
343
+ article.save
344
+ article.destroy
345
+ end
346
+
347
+ end
348
+
349
+ context "when updating attributes" do
350
+
351
+ should "update single attribute" do
352
+ @article.expects(:save).returns(true)
353
+
354
+ @article.update_attribute :title, 'Updated'
355
+ assert_equal 'Updated', @article.title
356
+ end
357
+
358
+ should "update all attributes" do
359
+ @article.expects(:save).returns(true)
360
+
361
+ @article.update_attributes :title => 'Updated', :tags => ['three']
362
+ assert_equal 'Updated', @article.title
363
+ assert_equal ['three'], @article.tags
364
+ end
365
+
366
+ end
367
+
368
+ end
369
+
370
+ context "Persistent model with mapping definition" do
371
+
372
+ should "create the index with mapping" do
373
+ expected_mapping = {
374
+ :mappings => { :persistent_article_with_mapping => {
375
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
376
+ }}
377
+ }
378
+
379
+ Slingshot::Index.any_instance.expects(:create).with(expected_mapping)
380
+
381
+ class ::PersistentArticleWithMapping
382
+
383
+ include Slingshot::Model::Persistence
384
+ include Slingshot::Model::Search
385
+ include Slingshot::Model::Callbacks
386
+
387
+ mapping do
388
+ property :title, :type => 'string', :analyzer => 'snowball', :boost => 10
389
+ end
390
+
391
+ end
392
+
393
+ assert_equal 'snowball', PersistentArticleWithMapping.mapping[:title][:analyzer]
394
+ end
395
+
396
+ end
397
+
398
+ end
399
+ end
400
+ end