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
@@ -21,7 +21,7 @@ module Slingshot
21
21
  assert doc.highlight.title.to_s.include?('<em>'), "Highlight does not include default highlight tag"
22
22
  end
23
23
 
24
- should "return entire content with highlighted fragments in ElasticSearch >= 0.16.0" do
24
+ should "return entire content with highlighted fragments" do
25
25
  # Slingshot::Configuration.logger STDERR, :level => 'debug'
26
26
 
27
27
  content = "A Fox one day fell into a deep well and could find no means of escape. A Goat, overcome with thirst, came to the same well, and seeing the Fox, inquired if the water was good. Concealing his sad plight under a merry guise, the Fox indulged in a lavish praise of the water, saying it was excellent beyond measure, and encouraging him to descend. The Goat, mindful only of his thirst, thoughtlessly jumped down, but just as he drank, the Fox informed him of the difficulty they were both in and suggested a scheme for their common escape. \"If,\" said he, \"you will place your forefeet upon the wall and bend your head, I will run up your back and escape, and will help you out afterwards.\" The Goat readily assented and the Fox leaped upon his back. Steadying himself with the Goat horns, he safely reached the mouth of the well and made off as fast as he could. When the Goat upbraided him for breaking his promise, he turned around and cried out, \"You foolish old fellow! If you had as many brains in your head as you have hairs in your beard, you would never have gone down before you had inspected the way up, nor have exposed yourself to dangers from which you had no means of escape.\" Look before you leap."
@@ -15,7 +15,7 @@ module Slingshot
15
15
  store :article, :title => 'One'
16
16
  refresh
17
17
  end
18
- sleep 1
18
+ sleep 1.5
19
19
 
20
20
  assert_equal 'string', index.mapping['article']['properties']['title']['type'], index.mapping.inspect
21
21
  assert_nil index.mapping['article']['properties']['title']['boost'], index.mapping.inspect
@@ -36,6 +36,33 @@ module Slingshot
36
36
 
37
37
  end
38
38
 
39
+ context "Removing documents from the index" do
40
+
41
+ teardown { Slingshot.index('articles-test-remove').delete }
42
+
43
+ setup do
44
+ Slingshot.index 'articles-test-remove' do
45
+ delete
46
+ create
47
+ store :id => 1, :title => 'One'
48
+ store :id => 2, :title => 'Two'
49
+ refresh
50
+ end
51
+ end
52
+
53
+ should "remove document from the index" do
54
+
55
+ assert_equal 2, Slingshot.search('articles-test-remove') { query { string '*' } }.results.count
56
+
57
+ assert_nothing_raised do
58
+ assert Slingshot.index('articles-test-remove').remove 1
59
+ assert ! Slingshot.index('articles-test-remove').remove(1)
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
39
66
  end
40
67
 
41
68
  end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ module Slingshot
4
+
5
+ class PersistentModelIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ def setup
9
+ super
10
+ PersistentArticle.index.delete
11
+ end
12
+
13
+ def teardown
14
+ super
15
+ PersistentArticle.index.delete
16
+ end
17
+
18
+ context "PersistentModel" do
19
+
20
+ should "save documents into index and find them by IDs" do
21
+ one = PersistentArticle.create :id => 1, :title => 'One'
22
+ two = PersistentArticle.create :id => 2, :title => 'Two'
23
+
24
+ PersistentArticle.index.refresh
25
+ sleep(1.5)
26
+
27
+ results = PersistentArticle.find [1, 2]
28
+
29
+ assert_equal 2, results.size
30
+
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -10,19 +10,19 @@ module Slingshot
10
10
  should "find article by title" do
11
11
  q = 'title:one'
12
12
  assert_equal 1, search(q).results.count
13
- assert_equal 'One', search(q).results.first['_source']['title']
13
+ assert_equal 'One', search(q).results.first[:title]
14
14
  end
15
15
 
16
16
  should "find articles by title with boosting" do
17
17
  q = 'title:one^100 OR title:two'
18
18
  assert_equal 2, search(q).results.count
19
- assert_equal 'One', search(q).results.first['_source']['title']
19
+ assert_equal 'One', search(q).results.first[:title]
20
20
  end
21
21
 
22
22
  should "find articles by tags" do
23
23
  q = 'tags:ruby AND tags:python'
24
24
  assert_equal 1, search(q).results.count
25
- assert_equal 'Two', search(q).results.first['_source']['title']
25
+ assert_equal 'Two', search(q).results.first[:title]
26
26
  end
27
27
 
28
28
  should "find any article with tags" do
@@ -15,7 +15,7 @@ module Slingshot
15
15
  end
16
16
 
17
17
  assert_equal 5, s.results.count
18
- assert_equal 'Five', s.results.first['_source']['title']
18
+ assert_equal 'Five', s.results.first[:title]
19
19
  end
20
20
 
21
21
  should "sort by title, descending" do
@@ -26,7 +26,7 @@ module Slingshot
26
26
  end
27
27
 
28
28
  assert_equal 5, s.results.count
29
- assert_equal 'Two', s.results.first['_source']['title']
29
+ assert_equal 'Two', s.results.first[:title]
30
30
  end
31
31
 
32
32
  end
@@ -0,0 +1,31 @@
1
+ # Example ActiveModel class
2
+
3
+ require 'rubygems'
4
+ require 'active_model'
5
+
6
+ class ActiveModelArticle
7
+
8
+ extend ActiveModel::Naming
9
+ include ActiveModel::AttributeMethods
10
+ include ActiveModel::Serialization
11
+ include ActiveModel::Serializers::JSON
12
+
13
+ include Slingshot::Model::Search
14
+
15
+ attr_reader :attributes
16
+
17
+ def initialize(attributes = {})
18
+ @attributes = attributes
19
+ end
20
+
21
+ def id; attributes['id'] || attributes['_id']; end
22
+ def id=(value); attributes['id'] = value; end
23
+
24
+ def method_missing(name, *args, &block)
25
+ attributes[name.to_sym] || attributes[name.to_s] || super
26
+ end
27
+
28
+ def persisted?; true; end
29
+ def save; true; end
30
+
31
+ end
@@ -0,0 +1,49 @@
1
+ # Example ActiveModel class with callbacks
2
+
3
+ require 'rubygems'
4
+ require 'active_model'
5
+
6
+ class ActiveModelArticleWithCallbacks
7
+
8
+ include ActiveModel::AttributeMethods
9
+ include ActiveModel::Validations
10
+ include ActiveModel::Serialization
11
+ include ActiveModel::Serializers::JSON
12
+ include ActiveModel::Naming
13
+
14
+ extend ActiveModel::Callbacks
15
+ define_model_callbacks :save, :destroy
16
+
17
+ include Slingshot::Model::Search
18
+ include Slingshot::Model::Callbacks
19
+
20
+ attr_reader :attributes
21
+
22
+ def initialize(attributes = {})
23
+ @attributes = attributes
24
+ end
25
+
26
+ def method_missing(id, *args, &block)
27
+ attributes[id.to_sym] || attributes[id.to_s] || super
28
+ end
29
+
30
+ def persisted?
31
+ true
32
+ end
33
+
34
+ def save
35
+ _run_save_callbacks do
36
+ STDERR.puts "[Saving ...]"
37
+ end
38
+ end
39
+
40
+ def destroy
41
+ _run_destroy_callbacks do
42
+ STDERR.puts "[Destroying ...]"
43
+ @destroyed = true
44
+ end
45
+ end
46
+
47
+ def destroyed?; !!@destroyed; end
48
+
49
+ end
@@ -0,0 +1,5 @@
1
+ # Example ActiveModel class with custom index name
2
+
3
+ class ActiveModelArticleWithCustomIndexName < ActiveModelArticle
4
+ index_name 'custom-index-name'
5
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+
4
+ class ActiveRecordArticle < ActiveRecord::Base
5
+ include Slingshot::Model::Search
6
+ include Slingshot::Model::Callbacks
7
+
8
+ mapping do
9
+ indexes :title, :type => 'string', :boost => 10, :analyzer => 'snowball'
10
+ indexes :created_at, :type => 'date'
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # Example class with ElasticSearch persistence
2
+
3
+ class PersistentArticle
4
+
5
+ include Slingshot::Model::Persistence
6
+
7
+ property :title
8
+ property :published_on
9
+ property :tags
10
+
11
+ end
@@ -0,0 +1,10 @@
1
+ # Example class with ElasticSearch persistence and custom index name
2
+
3
+ class PersistentArticleWithCustomIndexName
4
+
5
+ include Slingshot::Model::Persistence
6
+
7
+ property :title
8
+
9
+ index_name 'custom-index-name'
10
+ end
@@ -0,0 +1,22 @@
1
+ # Example ActiveModel class for testing :searchable mode
2
+
3
+ require 'rubygems'
4
+ require 'supermodel'
5
+
6
+ class SupermodelArticle < SuperModel::Base
7
+ include SuperModel::RandomID
8
+
9
+ include Slingshot::Model::Search
10
+ include Slingshot::Model::Callbacks
11
+
12
+ mapping do
13
+ indexes :title, :type => 'string', :boost => 15, :analyzer => 'czech'
14
+ end
15
+
16
+ alias :persisted? :exists?
17
+
18
+ def destroyed?
19
+ !self.class.find(self.id) rescue true
20
+ end
21
+
22
+ end
@@ -0,0 +1,11 @@
1
+ # Example ActiveModel with validations
2
+
3
+ class ValidatedModel
4
+
5
+ include Slingshot::Model::Persistence
6
+
7
+ property :name
8
+
9
+ validates_presence_of :name
10
+
11
+ end
data/test/test_helper.rb CHANGED
@@ -11,6 +11,10 @@ Dir[File.dirname(__FILE__) + '/models/**/*.rb'].each { |m| require m }
11
11
 
12
12
  class Test::Unit::TestCase
13
13
 
14
+ def mock_response(body, code=200)
15
+ stub(:body => body, :code => code)
16
+ end
17
+
14
18
  def fixtures_path
15
19
  Pathname( File.expand_path( 'fixtures', File.dirname(__FILE__) ) )
16
20
  end
@@ -0,0 +1,17 @@
1
+ require 'test_helper'
2
+
3
+ module Slingshot
4
+ module Model
5
+
6
+ class ActiveModelLintTest < Test::Unit::TestCase
7
+
8
+ include ActiveModel::Lint::Tests
9
+
10
+ def setup
11
+ @model = PersistentArticle.new :title => 'Test'
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -15,6 +15,9 @@ module Slingshot
15
15
  assert_raise(ArgumentError) { @http.post 'URL' }
16
16
  assert_raise(NoMethodError) { @http.post 'URL', 'DATA' }
17
17
 
18
+ assert_raise(ArgumentError) { @http.put }
19
+ assert_raise(ArgumentError) { @http.put 'URL' }
20
+
18
21
  assert_raise(ArgumentError) { @http.delete }
19
22
  assert_raise(NoMethodError) { @http.delete 'URL' }
20
23
  end
@@ -29,6 +32,7 @@ module Slingshot
29
32
  should "respond to HTTP methods" do
30
33
  assert_respond_to Client::RestClient, :get
31
34
  assert_respond_to Client::RestClient, :post
35
+ assert_respond_to Client::RestClient, :put
32
36
  assert_respond_to Client::RestClient, :delete
33
37
  end
34
38
 
@@ -4,6 +4,10 @@ module Slingshot
4
4
 
5
5
  class ConfigurationTest < Test::Unit::TestCase
6
6
 
7
+ def teardown
8
+ Slingshot::Configuration.reset
9
+ end
10
+
7
11
  context "Configuration" do
8
12
  setup do
9
13
  Configuration.instance_variable_set(:@url, nil)
@@ -10,8 +10,22 @@ module Slingshot
10
10
  @index = Slingshot::Index.new 'dummy'
11
11
  end
12
12
 
13
+ should "have a name" do
14
+ assert_equal 'dummy', @index.name
15
+ end
16
+
17
+ should "return true when exists" do
18
+ Configuration.client.expects(:get).returns(mock_response('{"dummy":{"document":{"properties":{}}}}'))
19
+ assert @index.exists?
20
+ end
21
+
22
+ should "return false when does not exist" do
23
+ Configuration.client.expects(:get).raises(RestClient::ResourceNotFound)
24
+ assert ! @index.exists?
25
+ end
26
+
13
27
  should "create new index" do
14
- Configuration.client.expects(:post).returns('{"ok":true,"acknowledged":true}')
28
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
15
29
  assert @index.create
16
30
  end
17
31
 
@@ -21,26 +35,26 @@ module Slingshot
21
35
  end
22
36
 
23
37
  should "delete index" do
24
- Configuration.client.expects(:delete).returns('{"ok":true,"acknowledged":true}')
38
+ Configuration.client.expects(:delete).returns(mock_response('{"ok":true,"acknowledged":true}'))
25
39
  assert @index.delete
26
40
  end
27
41
 
28
42
  should "not raise exception and just return false when deleting non-existing index" do
29
- Configuration.client.expects(:delete).returns('{"error":"[articles] missing"}')
43
+ Configuration.client.expects(:delete).returns(mock_response('{"error":"[articles] missing"}'))
30
44
  assert_nothing_raised { assert ! @index.delete }
31
45
  Configuration.client.expects(:delete).raises(RestClient::BadRequest)
32
46
  assert_nothing_raised { assert ! @index.delete }
33
47
  end
34
48
 
35
49
  should "refresh the index" do
36
- Configuration.client.expects(:post).returns('{"ok":true,"_shards":{}}')
50
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_shards":{}}'))
37
51
  assert_nothing_raised { assert @index.refresh }
38
52
  end
39
53
 
40
54
  context "mapping" do
41
55
 
42
56
  should "create index with mapping" do
43
- Configuration.client.expects(:post).returns('{"ok":true,"acknowledged":true}')
57
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
44
58
 
45
59
  assert @index.create :settings => { :number_of_shards => 1 },
46
60
  :mappings => { :article => {
@@ -67,7 +81,7 @@ module Slingshot
67
81
  }
68
82
  }
69
83
  JSON
70
- Configuration.client.stubs(:get).returns(json)
84
+ Configuration.client.stubs(:get).returns(mock_response(json))
71
85
 
72
86
  assert_equal 'string', @index.mapping['article']['properties']['title']['type']
73
87
  assert_equal 2.0, @index.mapping['article']['properties']['title']['boost']
@@ -78,19 +92,19 @@ module Slingshot
78
92
  context "when storing" do
79
93
 
80
94
  should "properly set type from args" do
81
- Configuration.client.expects(:post).with("#{Configuration.url}/dummy/article/", '{"title":"Test"}').returns('{"ok":true,"_id":"test"}').twice
95
+ Configuration.client.expects(:post).with("#{Configuration.url}/dummy/article/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}')).twice
82
96
  @index.store 'article', :title => 'Test'
83
97
  @index.store :article, :title => 'Test'
84
98
  end
85
99
 
86
100
  should "set default type" do
87
- Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/", '{"title":"Test"}').returns('{"ok":true,"_id":"test"}')
101
+ Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
88
102
  @index.store :title => 'Test'
89
103
  end
90
104
 
91
105
  should "call #to_indexed_json on non-String documents" do
92
106
  document = { :title => 'Test' }
93
- Configuration.client.expects(:post).returns('{"ok":true,"_id":"test"}')
107
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
94
108
  document.expects(:to_indexed_json)
95
109
  @index.store document
96
110
  end
@@ -104,15 +118,15 @@ module Slingshot
104
118
 
105
119
  should "store Hash it under its ID property" do
106
120
  Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/123",
107
- {:id => 123, :title => 'Test'}.to_json).
108
- returns('{"ok":true,"_id":"123"}')
121
+ Yajl::Encoder.encode({:id => 123, :title => 'Test'})).
122
+ returns(mock_response('{"ok":true,"_id":"123"}'))
109
123
  @index.store :id => 123, :title => 'Test'
110
124
  end
111
125
 
112
126
  should "store a custom class under its ID property" do
113
127
  Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/123",
114
128
  {:id => 123, :title => 'Test', :body => 'Lorem'}.to_json).
115
- returns('{"ok":true,"_id":"123"}')
129
+ returns(mock_response('{"ok":true,"_id":"123"}'))
116
130
  @index.store Article.new(:id => 123, :title => 'Test', :body => 'Lorem')
117
131
  end
118
132
 
@@ -126,24 +140,24 @@ module Slingshot
126
140
  Configuration.reset :wrapper
127
141
 
128
142
  Configuration.client.stubs(:post).with("#{Configuration.url}/dummy/article/", '{"title":"Test"}').
129
- returns('{"ok":true,"_id":"id-1"}')
143
+ returns(mock_response('{"ok":true,"_id":"id-1"}'))
130
144
  @index.store :article, :title => 'Test'
131
145
  end
132
146
 
133
147
  should "return document in default wrapper" do
134
148
  Configuration.client.expects(:get).with("#{Configuration.url}/dummy/article/id-1").
135
- returns('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}')
149
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
136
150
  article = @index.retrieve :article, 'id-1'
137
151
  assert_instance_of Results::Item, article
138
- assert_equal 'Test', article['_source']['title']
139
152
  assert_equal 'Test', article.title
153
+ assert_equal 'Test', article[:title]
140
154
  end
141
155
 
142
156
  should "return document as a hash" do
143
157
  Configuration.wrapper Hash
144
158
 
145
159
  Configuration.client.expects(:get).with("#{Configuration.url}/dummy/article/id-1").
146
- returns('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}')
160
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
147
161
  article = @index.retrieve :article, 'id-1'
148
162
  assert_instance_of Hash, article
149
163
  end
@@ -152,7 +166,7 @@ module Slingshot
152
166
  Configuration.wrapper Article
153
167
 
154
168
  Configuration.client.expects(:get).with("#{Configuration.url}/dummy/article/id-1").
155
- returns('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}')
169
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
156
170
  article = @index.retrieve :article, 'id-1'
157
171
  assert_instance_of Article, article
158
172
  assert_equal 'Test', article.title
@@ -160,6 +174,215 @@ module Slingshot
160
174
 
161
175
  end
162
176
 
177
+ context "when removing" do
178
+
179
+ should "properly set type from args" do
180
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/article/").
181
+ returns('{"ok":true,"_id":"test"}').twice
182
+ @index.remove 'article', :title => 'Test'
183
+ @index.remove :article, :title => 'Test'
184
+ end
185
+
186
+ should "set default type" do
187
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/document/").
188
+ returns('{"ok":true,"_id":"test"}')
189
+ @index.remove :title => 'Test'
190
+ end
191
+
192
+ should "get ID from hash" do
193
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/document/1").
194
+ returns('{"ok":true,"_id":"1"}')
195
+ @index.remove :id => 1
196
+ end
197
+
198
+ should "get ID from method" do
199
+ document = stub('document', :id => 1)
200
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/document/1").
201
+ returns('{"ok":true,"_id":"1"}')
202
+ @index.remove document
203
+ end
204
+
205
+ should "get ID from arguments" do
206
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/document/1").
207
+ returns('{"ok":true,"_id":"1"}')
208
+ @index.remove :document, 1
209
+ end
210
+
211
+ end
212
+
213
+ context "when storing in bulk" do
214
+ # The expected JSON looks like this:
215
+ #
216
+ # {"index":{"_index":"dummy","_type":"document","_id":"1"}}
217
+ # {"id":"1","title":"One"}
218
+ # {"index":{"_index":"dummy","_type":"document","_id":"2"}}
219
+ # {"id":"2","title":"Two"}
220
+ #
221
+ # See http://www.elasticsearch.org/guide/reference/api/bulk.html
222
+
223
+ should "serialize Hashes" do
224
+ Configuration.client.expects(:post).with do |url, json|
225
+ url == "#{Configuration.url}/_bulk"
226
+ json =~ /"_index":"dummy"/
227
+ json =~ /"_type":"document"/
228
+ json =~ /"_id":"1"/
229
+ json =~ /"_id":"2"/
230
+ json =~ /"id":"1"/
231
+ json =~ /"id":"2"/
232
+ json =~ /"title":"One"/
233
+ json =~ /"title":"Two"/
234
+ end.returns('{}')
235
+
236
+ @index.bulk_store [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
237
+
238
+ end
239
+
240
+ should "serialize ActiveModel instances" do
241
+ Configuration.client.expects(:post).with do |url, json|
242
+ url == "#{Configuration.url}/_bulk"
243
+ json =~ /"_index":"active_model_articles"/
244
+ json =~ /"_type":"article"/
245
+ json =~ /"_id":"1"/
246
+ json =~ /"_id":"2"/
247
+ json =~ /"id":"1"/
248
+ json =~ /"id":"2"/
249
+ json =~ /"title":"One"/
250
+ json =~ /"title":"Two"/
251
+ end.returns('{}')
252
+
253
+ one = ActiveModelArticle.new 'title' => 'One'; one.id = '1'
254
+ two = ActiveModelArticle.new 'title' => 'Two'; two.id = '2'
255
+
256
+ ActiveModelArticle.index.bulk_store [ one, two ]
257
+
258
+ end
259
+
260
+ should "try again when an exception occurs" do
261
+ Configuration.client.expects(:post).raises(RestClient::RequestFailed).at_least(2)
262
+
263
+ assert_raise(RestClient::RequestFailed) do
264
+ @index.bulk_store [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
265
+ end
266
+ end
267
+
268
+ should_eventually "raise exception when collection item does not have ID" do
269
+ # TODO: raise exception when collection item does not have ID
270
+ end
271
+
272
+ end
273
+
274
+ context "when importing" do
275
+ setup do
276
+ @index = Slingshot::Index.new 'import'
277
+ end
278
+
279
+ class ::ImportData
280
+ DATA = (1..4).to_a
281
+
282
+ def self.paginate(options={})
283
+ options = {:page => 1, :per_page => 1000}.update options
284
+ DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] )
285
+ end
286
+
287
+ def self.each(&block); DATA.each &block; end
288
+ def self.map(&block); DATA.map &block; end
289
+ def self.count; DATA.size; end
290
+ end
291
+
292
+ should "be initialized with a collection" do
293
+ @index.expects(:bulk_store).returns(:true)
294
+
295
+ assert_nothing_raised { @index.import [{ :id => 1, :title => 'Article' }] }
296
+ end
297
+
298
+ should "be initialized with a class and params" do
299
+ @index.expects(:bulk_store).returns(:true)
300
+
301
+ assert_nothing_raised { @index.import ImportData }
302
+ end
303
+
304
+ context "plain collection" do
305
+
306
+ should "just store it in bulk" do
307
+ collection = [{ :id => 1, :title => 'Article' }]
308
+ @index.expects(:bulk_store).with( collection ).returns(true)
309
+
310
+ @index.import collection
311
+ end
312
+
313
+ end
314
+
315
+ context "class" do
316
+
317
+ should "call the passed method and bulk store the results" do
318
+ @index.expects(:bulk_store).with([1, 2, 3, 4]).returns(true)
319
+
320
+ @index.import ImportData, :paginate
321
+ end
322
+
323
+ should "pass the params to the passed method and bulk store the results" do
324
+ @index.expects(:bulk_store).with([1, 2]).returns(true)
325
+ @index.expects(:bulk_store).with([3, 4]).returns(true)
326
+
327
+ @index.import ImportData, :paginate, :page => 1, :per_page => 2
328
+ end
329
+
330
+ should "pass the class when method not passed" do
331
+ @index.expects(:bulk_store).with(ImportData).returns(true)
332
+
333
+ @index.import ImportData
334
+ end
335
+
336
+ end
337
+
338
+ context "with passed block" do
339
+
340
+ context "and plain collection" do
341
+
342
+ should "allow to manipulate the collection in the block" do
343
+ Slingshot::Index.any_instance.expects(:bulk_store).with([{ :id => 1, :title => 'ARTICLE' }])
344
+
345
+
346
+ @index.import [{ :id => 1, :title => 'Article' }] do |articles|
347
+ articles.map { |article| article.update :title => article[:title].upcase }
348
+ end
349
+ end
350
+
351
+ end
352
+
353
+ context "and object" do
354
+
355
+ should "call the passed block on every batch" do
356
+ Slingshot::Index.any_instance.expects(:bulk_store).with([1, 2])
357
+ Slingshot::Index.any_instance.expects(:bulk_store).with([3, 4])
358
+
359
+ runs = 0
360
+ @index.import ImportData, :paginate, :per_page => 2 do |documents|
361
+ runs += 1
362
+ # Don't forget to return the documents at the end of the block
363
+ documents
364
+ end
365
+
366
+ assert_equal 2, runs
367
+ end
368
+
369
+ should "allow to manipulate the documents in passed block" do
370
+ Slingshot::Index.any_instance.expects(:bulk_store).with([2, 3])
371
+ Slingshot::Index.any_instance.expects(:bulk_store).with([4, 5])
372
+
373
+
374
+ @index.import ImportData, :paginate, :per_page => 2 do |documents|
375
+ # Add 1 to every "document" and return them
376
+ documents.map { |d| d + 1 }
377
+ end
378
+ end
379
+
380
+ end
381
+
382
+ end
383
+
384
+ end
385
+
163
386
  end
164
387
 
165
388
  end