tire 0.1.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.
Files changed (83) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +435 -0
  5. data/Rakefile +75 -0
  6. data/examples/dsl.rb +73 -0
  7. data/examples/rails-application-template.rb +144 -0
  8. data/examples/tire-dsl.rb +617 -0
  9. data/lib/tire.rb +35 -0
  10. data/lib/tire/client.rb +40 -0
  11. data/lib/tire/configuration.rb +29 -0
  12. data/lib/tire/dsl.rb +33 -0
  13. data/lib/tire/index.rb +209 -0
  14. data/lib/tire/logger.rb +60 -0
  15. data/lib/tire/model/callbacks.rb +23 -0
  16. data/lib/tire/model/import.rb +18 -0
  17. data/lib/tire/model/indexing.rb +50 -0
  18. data/lib/tire/model/naming.rb +30 -0
  19. data/lib/tire/model/persistence.rb +34 -0
  20. data/lib/tire/model/persistence/attributes.rb +60 -0
  21. data/lib/tire/model/persistence/finders.rb +61 -0
  22. data/lib/tire/model/persistence/storage.rb +75 -0
  23. data/lib/tire/model/search.rb +97 -0
  24. data/lib/tire/results/collection.rb +56 -0
  25. data/lib/tire/results/item.rb +39 -0
  26. data/lib/tire/results/pagination.rb +30 -0
  27. data/lib/tire/rubyext/hash.rb +3 -0
  28. data/lib/tire/rubyext/symbol.rb +11 -0
  29. data/lib/tire/search.rb +117 -0
  30. data/lib/tire/search/facet.rb +41 -0
  31. data/lib/tire/search/filter.rb +28 -0
  32. data/lib/tire/search/highlight.rb +37 -0
  33. data/lib/tire/search/query.rb +42 -0
  34. data/lib/tire/search/sort.rb +29 -0
  35. data/lib/tire/tasks.rb +88 -0
  36. data/lib/tire/version.rb +3 -0
  37. data/test/fixtures/articles/1.json +1 -0
  38. data/test/fixtures/articles/2.json +1 -0
  39. data/test/fixtures/articles/3.json +1 -0
  40. data/test/fixtures/articles/4.json +1 -0
  41. data/test/fixtures/articles/5.json +1 -0
  42. data/test/integration/active_model_searchable_test.rb +80 -0
  43. data/test/integration/active_record_searchable_test.rb +193 -0
  44. data/test/integration/facets_test.rb +65 -0
  45. data/test/integration/filters_test.rb +46 -0
  46. data/test/integration/highlight_test.rb +52 -0
  47. data/test/integration/index_mapping_test.rb +44 -0
  48. data/test/integration/index_store_test.rb +68 -0
  49. data/test/integration/persistent_model_test.rb +35 -0
  50. data/test/integration/query_string_test.rb +43 -0
  51. data/test/integration/results_test.rb +28 -0
  52. data/test/integration/sort_test.rb +36 -0
  53. data/test/models/active_model_article.rb +31 -0
  54. data/test/models/active_model_article_with_callbacks.rb +49 -0
  55. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  56. data/test/models/active_record_article.rb +12 -0
  57. data/test/models/article.rb +15 -0
  58. data/test/models/persistent_article.rb +11 -0
  59. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  60. data/test/models/supermodel_article.rb +22 -0
  61. data/test/models/validated_model.rb +11 -0
  62. data/test/test_helper.rb +52 -0
  63. data/test/unit/active_model_lint_test.rb +17 -0
  64. data/test/unit/client_test.rb +43 -0
  65. data/test/unit/configuration_test.rb +71 -0
  66. data/test/unit/index_test.rb +390 -0
  67. data/test/unit/logger_test.rb +114 -0
  68. data/test/unit/model_callbacks_test.rb +90 -0
  69. data/test/unit/model_import_test.rb +71 -0
  70. data/test/unit/model_persistence_test.rb +400 -0
  71. data/test/unit/model_search_test.rb +289 -0
  72. data/test/unit/results_collection_test.rb +131 -0
  73. data/test/unit/results_item_test.rb +59 -0
  74. data/test/unit/rubyext_hash_test.rb +19 -0
  75. data/test/unit/search_facet_test.rb +69 -0
  76. data/test/unit/search_filter_test.rb +36 -0
  77. data/test/unit/search_highlight_test.rb +46 -0
  78. data/test/unit/search_query_test.rb +55 -0
  79. data/test/unit/search_sort_test.rb +50 -0
  80. data/test/unit/search_test.rb +204 -0
  81. data/test/unit/tire_test.rb +55 -0
  82. data/tire.gemspec +54 -0
  83. metadata +372 -0
@@ -0,0 +1,17 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
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
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ClientTest < Test::Unit::TestCase
6
+
7
+ context "Base" do
8
+ setup { @http ||= Client::Base.new }
9
+
10
+ should "have abstract methods" do
11
+ assert_raise(ArgumentError) { @http.get }
12
+ assert_raise(NoMethodError) { @http.get 'URL' }
13
+
14
+ assert_raise(ArgumentError) { @http.post }
15
+ assert_raise(ArgumentError) { @http.post 'URL' }
16
+ assert_raise(NoMethodError) { @http.post 'URL', 'DATA' }
17
+
18
+ assert_raise(ArgumentError) { @http.put }
19
+ assert_raise(ArgumentError) { @http.put 'URL' }
20
+
21
+ assert_raise(ArgumentError) { @http.delete }
22
+ assert_raise(NoMethodError) { @http.delete 'URL' }
23
+ end
24
+ end
25
+
26
+ context "RestClient" do
27
+
28
+ should "be default" do
29
+ assert_equal Client::RestClient, Configuration.client
30
+ end
31
+
32
+ should "respond to HTTP methods" do
33
+ assert_respond_to Client::RestClient, :get
34
+ assert_respond_to Client::RestClient, :post
35
+ assert_respond_to Client::RestClient, :put
36
+ assert_respond_to Client::RestClient, :delete
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ConfigurationTest < Test::Unit::TestCase
6
+
7
+ def teardown
8
+ Tire::Configuration.reset
9
+ end
10
+
11
+ context "Configuration" do
12
+ setup do
13
+ Configuration.instance_variable_set(:@url, nil)
14
+ Configuration.instance_variable_set(:@client, nil)
15
+ end
16
+
17
+ teardown do
18
+ Configuration.reset
19
+ end
20
+
21
+ should "return default URL" do
22
+ assert_equal 'http://localhost:9200', Configuration.url
23
+ end
24
+
25
+ should "allow setting and retrieving the URL" do
26
+ assert_nothing_raised { Configuration.url 'http://example.com' }
27
+ assert_equal 'http://example.com', Configuration.url
28
+ end
29
+
30
+ should "return default client" do
31
+ assert_equal Client::RestClient, Configuration.client
32
+ end
33
+
34
+ should "allow setting and retrieving the client" do
35
+ assert_nothing_raised { Configuration.client Client::Base }
36
+ assert_equal Client::Base, Configuration.client
37
+ end
38
+
39
+ should "return nil as logger by default" do
40
+ assert_nil Configuration.logger
41
+ end
42
+
43
+ should "return set and return logger" do
44
+ Configuration.logger STDERR
45
+ assert_not_nil Configuration.logger
46
+ assert_instance_of Tire::Logger, Configuration.logger
47
+ end
48
+
49
+ should "allow to reset the configuration for specific property" do
50
+ Configuration.url 'http://example.com'
51
+ Configuration.client Client::Base
52
+ assert_equal 'http://example.com', Configuration.url
53
+ Configuration.reset :url
54
+ assert_equal 'http://localhost:9200', Configuration.url
55
+ assert_equal Client::Base, Configuration.client
56
+ end
57
+
58
+ should "allow to reset the configuration for all properties" do
59
+ Configuration.url 'http://example.com'
60
+ Configuration.client Client::Base
61
+ assert_equal 'http://example.com', Configuration.url
62
+ assert_equal Client::Base, Configuration.client
63
+ Configuration.reset
64
+ assert_equal 'http://localhost:9200', Configuration.url
65
+ assert_equal Client::RestClient, Configuration.client
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,390 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class IndexTest < Test::Unit::TestCase
6
+
7
+ context "Index" do
8
+
9
+ setup do
10
+ @index = Tire::Index.new 'dummy'
11
+ end
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
+
27
+ should "create new index" do
28
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
29
+ assert @index.create
30
+ end
31
+
32
+ should "not raise exception and just return false when trying to create existing index" do
33
+ Configuration.client.expects(:post).raises(RestClient::BadRequest)
34
+ assert_nothing_raised { assert ! @index.create }
35
+ end
36
+
37
+ should "delete index" do
38
+ Configuration.client.expects(:delete).returns(mock_response('{"ok":true,"acknowledged":true}'))
39
+ assert @index.delete
40
+ end
41
+
42
+ should "not raise exception and just return false when deleting non-existing index" do
43
+ Configuration.client.expects(:delete).returns(mock_response('{"error":"[articles] missing"}'))
44
+ assert_nothing_raised { assert ! @index.delete }
45
+ Configuration.client.expects(:delete).raises(RestClient::BadRequest)
46
+ assert_nothing_raised { assert ! @index.delete }
47
+ end
48
+
49
+ should "refresh the index" do
50
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_shards":{}}'))
51
+ assert_nothing_raised { assert @index.refresh }
52
+ end
53
+
54
+ context "mapping" do
55
+
56
+ should "create index with mapping" do
57
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
58
+
59
+ assert @index.create :settings => { :number_of_shards => 1 },
60
+ :mappings => { :article => {
61
+ :properties => {
62
+ :title => { :boost => 2.0,
63
+ :type => 'string',
64
+ :store => 'yes',
65
+ :analyzer => 'snowball' }
66
+ }
67
+ }
68
+ }
69
+ end
70
+
71
+ should "return the mapping" do
72
+ json =<<-JSON
73
+ {
74
+ "dummy" : {
75
+ "article" : {
76
+ "properties" : {
77
+ "title" : { "type" : "string", "boost" : 2.0 },
78
+ "category" : { "type" : "string", "analyzed" : "no" }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ JSON
84
+ Configuration.client.stubs(:get).returns(mock_response(json))
85
+
86
+ assert_equal 'string', @index.mapping['article']['properties']['title']['type']
87
+ assert_equal 2.0, @index.mapping['article']['properties']['title']['boost']
88
+ end
89
+
90
+ end
91
+
92
+ context "when storing" do
93
+
94
+ should "properly set type from args" do
95
+ Configuration.client.expects(:post).with("#{Configuration.url}/dummy/article/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}')).twice
96
+ @index.store 'article', :title => 'Test'
97
+ @index.store :article, :title => 'Test'
98
+ end
99
+
100
+ should "set default type" do
101
+ Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
102
+ @index.store :title => 'Test'
103
+ end
104
+
105
+ should "call #to_indexed_json on non-String documents" do
106
+ document = { :title => 'Test' }
107
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
108
+ document.expects(:to_indexed_json)
109
+ @index.store document
110
+ end
111
+
112
+ should "raise error when storing neither String nor object with #to_indexed_json method" do
113
+ class MyDocument;end; document = MyDocument.new
114
+ assert_raise(ArgumentError) { @index.store document }
115
+ end
116
+
117
+ context "document with ID" do
118
+
119
+ should "store Hash it under its ID property" do
120
+ Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/123",
121
+ Yajl::Encoder.encode({:id => 123, :title => 'Test'})).
122
+ returns(mock_response('{"ok":true,"_id":"123"}'))
123
+ @index.store :id => 123, :title => 'Test'
124
+ end
125
+
126
+ should "store a custom class under its ID property" do
127
+ Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/123",
128
+ {:id => 123, :title => 'Test', :body => 'Lorem'}.to_json).
129
+ returns(mock_response('{"ok":true,"_id":"123"}'))
130
+ @index.store Article.new(:id => 123, :title => 'Test', :body => 'Lorem')
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+
137
+ context "when retrieving" do
138
+
139
+ setup do
140
+ Configuration.reset :wrapper
141
+
142
+ Configuration.client.stubs(:post).with("#{Configuration.url}/dummy/article/", '{"title":"Test"}').
143
+ returns(mock_response('{"ok":true,"_id":"id-1"}'))
144
+ @index.store :article, :title => 'Test'
145
+ end
146
+
147
+ should "return document in default wrapper" do
148
+ Configuration.client.expects(:get).with("#{Configuration.url}/dummy/article/id-1").
149
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
150
+ article = @index.retrieve :article, 'id-1'
151
+ assert_instance_of Results::Item, article
152
+ assert_equal 'Test', article.title
153
+ assert_equal 'Test', article[:title]
154
+ end
155
+
156
+ should "return document as a hash" do
157
+ Configuration.wrapper Hash
158
+
159
+ Configuration.client.expects(:get).with("#{Configuration.url}/dummy/article/id-1").
160
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
161
+ article = @index.retrieve :article, 'id-1'
162
+ assert_instance_of Hash, article
163
+ end
164
+
165
+ should "return document in custom wrapper" do
166
+ Configuration.wrapper Article
167
+
168
+ Configuration.client.expects(:get).with("#{Configuration.url}/dummy/article/id-1").
169
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
170
+ article = @index.retrieve :article, 'id-1'
171
+ assert_instance_of Article, article
172
+ assert_equal 'Test', article.title
173
+ end
174
+
175
+ end
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 = Tire::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
+ Tire::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
+ Tire::Index.any_instance.expects(:bulk_store).with([1, 2])
357
+ Tire::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
+ Tire::Index.any_instance.expects(:bulk_store).with([2, 3])
371
+ Tire::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
+
386
+ end
387
+
388
+ end
389
+
390
+ end