tire 0.4.0.pre → 0.4.0.rc

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 (46) hide show
  1. data/README.markdown +6 -10
  2. data/examples/rails-application-template.rb +6 -6
  3. data/examples/tire-dsl.rb +10 -9
  4. data/lib/tire.rb +8 -0
  5. data/lib/tire/dsl.rb +7 -6
  6. data/lib/tire/index.rb +22 -12
  7. data/lib/tire/model/import.rb +3 -2
  8. data/lib/tire/model/indexing.rb +21 -13
  9. data/lib/tire/model/naming.rb +2 -1
  10. data/lib/tire/model/persistence.rb +1 -1
  11. data/lib/tire/results/collection.rb +20 -17
  12. data/lib/tire/results/item.rb +1 -1
  13. data/lib/tire/rubyext/ruby_1_8.rb +7 -0
  14. data/lib/tire/search.rb +33 -19
  15. data/lib/tire/search/facet.rb +5 -0
  16. data/lib/tire/search/query.rb +8 -3
  17. data/lib/tire/tasks.rb +47 -14
  18. data/lib/tire/utils.rb +17 -0
  19. data/lib/tire/version.rb +13 -2
  20. data/test/integration/active_model_indexing_test.rb +1 -0
  21. data/test/integration/active_model_searchable_test.rb +7 -5
  22. data/test/integration/active_record_searchable_test.rb +159 -72
  23. data/test/integration/count_test.rb +34 -0
  24. data/test/integration/dsl_search_test.rb +22 -0
  25. data/test/integration/explanation_test.rb +44 -0
  26. data/test/integration/facets_test.rb +15 -0
  27. data/test/integration/fuzzy_queries_test.rb +20 -0
  28. data/test/integration/mongoid_searchable_test.rb +1 -0
  29. data/test/integration/persistent_model_test.rb +22 -1
  30. data/test/integration/text_query_test.rb +17 -3
  31. data/test/models/active_record_models.rb +43 -1
  32. data/test/models/mongoid_models.rb +0 -1
  33. data/test/models/persistent_article_in_namespace.rb +12 -0
  34. data/test/models/supermodel_article.rb +5 -10
  35. data/test/test_helper.rb +16 -2
  36. data/test/unit/index_test.rb +90 -16
  37. data/test/unit/model_import_test.rb +4 -4
  38. data/test/unit/model_search_test.rb +13 -10
  39. data/test/unit/results_collection_test.rb +6 -0
  40. data/test/unit/results_item_test.rb +8 -0
  41. data/test/unit/search_facet_test.rb +9 -0
  42. data/test/unit/search_query_test.rb +36 -7
  43. data/test/unit/search_test.rb +70 -1
  44. data/test/unit/tire_test.rb +23 -12
  45. data/tire.gemspec +11 -8
  46. metadata +90 -48
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class CountIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Count" do
9
+
10
+ should "return total number of hits for the query, but no hits" do
11
+ s = Tire.search 'articles-test', :search_type => 'count' do
12
+ query { term :tags, 'ruby' }
13
+ end
14
+
15
+ assert_equal 2, s.results.total
16
+ assert_equal 0, s.results.count
17
+ assert s.results.empty?
18
+ end
19
+
20
+ should "return facets in results" do
21
+ s = Tire.search 'articles-test', :search_type => 'count' do
22
+ query { term :tags, 'ruby' }
23
+ facet('tags') { terms :tags }
24
+ end
25
+
26
+ assert ! s.results.facets['tags'].empty?
27
+ assert_equal 2, s.results.facets['tags']['terms'].select { |t| t['term'] == 'ruby' }. first['count']
28
+ assert_equal 1, s.results.facets['tags']['terms'].select { |t| t['term'] == 'python' }.first['count']
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class DSLSearchIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "DSL" do
9
+
10
+ should "allow passing search payload as a Hash" do
11
+ s = Tire.search 'articles-test', :query => { :query_string => { :query => 'ruby' } },
12
+ :facets => { 'tags' => { :filter => { :term => {:tags => 'ruby' } } } }
13
+ # p s.results
14
+ assert_equal 2, s.results.count
15
+ assert_equal 2, s.results.facets['tags']['count']
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ExplanationIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Explanation" do
9
+ teardown { Tire.index('explanation-test').delete }
10
+
11
+ setup do
12
+ content = "A Fox one day fell into a deep well and could find no means of escape."
13
+
14
+ Tire.index 'explanation-test' do
15
+ delete
16
+ create
17
+ store :id => 1, :content => content
18
+ refresh
19
+ end
20
+ end
21
+
22
+ should "add '_explanation' field to the result item" do
23
+ # Tire::Configuration.logger STDERR, :level => 'debug'
24
+ s = Tire.search 'explanation-test', :explain => true do
25
+ query do
26
+ boolean do
27
+ should { string 'content:Fox' }
28
+ end
29
+ end
30
+ end
31
+
32
+ doc = s.results.first
33
+
34
+ explanation = doc._explanation
35
+
36
+ assert explanation.description.include?("product of:")
37
+ assert explanation.value < 0.6
38
+ assert_not_nil explanation.details
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -210,6 +210,21 @@ module Tire
210
210
 
211
211
  end
212
212
 
213
+ context "filter" do
214
+ should "return a filtered facet" do
215
+ s = Tire.search('articles-test') do
216
+ query { all }
217
+ facet 'filtered' do
218
+ filter :tags, 'ruby'
219
+ end
220
+ end
221
+
222
+ assert_equal 5, s.results.size, s.results.inspect
223
+ facets = s.results.facets["filtered"]
224
+ assert_equal 2, facets["count"], facets.inspect
225
+ end
226
+ end
227
+
213
228
  end
214
229
 
215
230
  end
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class FuzzyQueryIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Fuzzy query" do
9
+ should "fuzzily find article by tag" do
10
+ results = Tire.search('articles-test') { query { fuzzy :tags, 'irlang' } }.results
11
+
12
+ assert_equal 1, results.count
13
+ assert_equal ["erlang"], results.first[:tags]
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -1,4 +1,5 @@
1
1
  require 'test_helper'
2
+ require File.expand_path('../../models/mongoid_models', __FILE__)
2
3
 
3
4
  begin
4
5
  require "mongo"
@@ -17,7 +17,6 @@ module Tire
17
17
  end
18
18
 
19
19
  context "PersistentModel" do
20
-
21
20
  should "search with simple query" do
22
21
  PersistentArticle.create :id => 1, :title => 'One'
23
22
  PersistentArticle.index.refresh
@@ -88,6 +87,28 @@ module Tire
88
87
  assert_equal 2, results.num_pages
89
88
  assert_equal 0, results.offset_value
90
89
  end
90
+
91
+ end
92
+
93
+ context "with namespaced models" do
94
+ setup do
95
+ MyNamespace::PersistentArticleInNamespace.create :title => 'Test'
96
+ MyNamespace::PersistentArticleInNamespace.index.refresh
97
+ end
98
+
99
+ teardown do
100
+ MyNamespace::PersistentArticleInNamespace.index.delete
101
+ end
102
+
103
+ should "find the document in the index" do
104
+ results = MyNamespace::PersistentArticleInNamespace.search 'test'
105
+
106
+ assert results.any?, "No results returned: #{results.inspect}"
107
+ assert_equal 1, results.count
108
+
109
+ assert_instance_of MyNamespace::PersistentArticleInNamespace, results.first
110
+ end
111
+
91
112
  end
92
113
 
93
114
  end
@@ -7,17 +7,31 @@ module Tire
7
7
 
8
8
  context "Text query" do
9
9
  setup do
10
- ::RestClient.put "#{URL}/articles-test/article/plus-one", {title: "+1 !!!"}.to_json
11
- ::RestClient.post "#{URL}/articles-test/_refresh", ''
10
+ Tire.index('articles-test') do
11
+ store :type => 'article', :title => '+1 !!!'
12
+ store :type => 'article', :title => 'Furry Kitten'
13
+ refresh
14
+ end
12
15
  end
13
16
 
14
17
  should "find article by title" do
15
- results = Tire.search('articles-test') { query { text :title, '+1' } }.results
18
+ results = Tire.search('articles-test') do
19
+ query { text :title, '+1' }
20
+ end.results
16
21
 
17
22
  assert_equal 1, results.count
18
23
  assert_equal "+1 !!!", results.first[:title]
19
24
  end
20
25
 
26
+ should "allow to pass options (fuzziness)" do
27
+ results = Tire.search('articles-test') do
28
+ query { text :title, 'fuzzy mitten', :fuzziness => 0.5, :operator => 'and' }
29
+ end.results
30
+
31
+ assert_equal 1, results.count
32
+ assert_equal "Furry Kitten", results.first[:title]
33
+ end
34
+
21
35
  end
22
36
 
23
37
  end
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'active_record'
3
2
 
4
3
  class ActiveRecordArticle < ActiveRecord::Base
@@ -78,3 +77,46 @@ class ActiveRecordClassWithDynamicIndexName < ActiveRecord::Base
78
77
  "dynamic" + '_' + "index"
79
78
  end
80
79
  end
80
+
81
+ # Used in test for multiple class instances in one index,
82
+ # and single table inheritance (STI) support.
83
+
84
+ class ActiveRecordModelOne < ActiveRecord::Base
85
+ include Tire::Model::Search
86
+ include Tire::Model::Callbacks
87
+ self.table_name = 'active_record_model_one'
88
+ index_name 'active_record_model_one'
89
+ end
90
+
91
+ class ActiveRecordModelTwo < ActiveRecord::Base
92
+ include Tire::Model::Search
93
+ include Tire::Model::Callbacks
94
+ self.table_name = 'active_record_model_two'
95
+ index_name 'active_record_model_two'
96
+ end
97
+
98
+ class ActiveRecordAsset < ActiveRecord::Base
99
+ include Tire::Model::Search
100
+ include Tire::Model::Callbacks
101
+ end
102
+
103
+ class ActiveRecordVideo < ActiveRecordAsset
104
+ index_name 'active_record_assets'
105
+ end
106
+
107
+ class ActiveRecordPhoto < ActiveRecordAsset
108
+ index_name 'active_record_assets'
109
+ end
110
+
111
+ # Namespaced ActiveRecord models
112
+
113
+ module ActiveRecordNamespace
114
+ def self.table_name_prefix
115
+ 'active_record_namespace_'
116
+ end
117
+ end
118
+
119
+ class ActiveRecordNamespace::MyModel < ActiveRecord::Base
120
+ include Tire::Model::Search
121
+ include Tire::Model::Callbacks
122
+ end
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'mongoid'
3
2
 
4
3
  class MongoidArticle
@@ -0,0 +1,12 @@
1
+ # Example namespaced class with ElasticSearch persistence
2
+ #
3
+ # The `document_type` is `my_namespace/persistent_article_in_namespace`
4
+ #
5
+
6
+ module MyNamespace
7
+ class PersistentArticleInNamespace
8
+ include Tire::Model::Persistence
9
+
10
+ property :title
11
+ end
12
+ end
@@ -1,22 +1,17 @@
1
1
  # Example ActiveModel class for testing :searchable mode
2
2
 
3
3
  require 'rubygems'
4
- require 'supermodel'
4
+ require 'redis/persistence'
5
5
 
6
- class SupermodelArticle < SuperModel::Base
7
- include SuperModel::RandomID
6
+ class SupermodelArticle
7
+ include Redis::Persistence
8
8
 
9
9
  include Tire::Model::Search
10
10
  include Tire::Model::Callbacks
11
11
 
12
+ property :title
13
+
12
14
  mapping do
13
15
  indexes :title, :type => 'string', :boost => 15, :analyzer => 'czech'
14
16
  end
15
-
16
- alias :persisted? :exists?
17
-
18
- def destroyed?
19
- !self.class.find(self.id) rescue true
20
- end
21
-
22
17
  end
data/test/test_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rubygems'
2
+ require 'bundler/setup'
2
3
 
3
4
  require 'pathname'
4
5
  require 'test/unit'
@@ -7,14 +8,27 @@ require 'yajl/json_gem'
7
8
  require 'sqlite3'
8
9
 
9
10
  require 'shoulda'
10
- require 'turn/autorun' unless ENV["TM_FILEPATH"] || ENV["CI"]
11
+ require 'turn/autorun' unless ENV["TM_FILEPATH"] || ENV["CI"] || defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
11
12
  require 'mocha'
12
13
 
13
14
  require 'active_support/core_ext/hash/indifferent_access'
14
15
 
15
16
  require 'tire'
16
17
 
17
- Dir[File.dirname(__FILE__) + '/models/**/*.rb'].each { |m| require m }
18
+ # Require basic model files
19
+ #
20
+ require File.dirname(__FILE__) + '/models/active_model_article'
21
+ require File.dirname(__FILE__) + '/models/active_model_article_with_callbacks'
22
+ require File.dirname(__FILE__) + '/models/active_model_article_with_custom_document_type'
23
+ require File.dirname(__FILE__) + '/models/active_model_article_with_custom_index_name'
24
+ require File.dirname(__FILE__) + '/models/active_record_models'
25
+ require File.dirname(__FILE__) + '/models/article'
26
+ require File.dirname(__FILE__) + '/models/persistent_article'
27
+ require File.dirname(__FILE__) + '/models/persistent_article_in_namespace'
28
+ require File.dirname(__FILE__) + '/models/persistent_article_with_casting'
29
+ require File.dirname(__FILE__) + '/models/persistent_article_with_defaults'
30
+ require File.dirname(__FILE__) + '/models/persistent_articles_with_custom_index_name'
31
+ require File.dirname(__FILE__) + '/models/validated_model'
18
32
 
19
33
  class Test::Unit::TestCase
20
34
 
@@ -14,6 +14,14 @@ module Tire
14
14
  assert_equal 'dummy', @index.name
15
15
  end
16
16
 
17
+ should "return HTTP response" do
18
+ assert_respond_to @index, :response
19
+
20
+ Configuration.client.expects(:head).returns(mock_response('OK'))
21
+ @index.exists?
22
+ assert_equal 'OK', @index.response.body
23
+ end
24
+
17
25
  should "return true when exists" do
18
26
  Configuration.client.expects(:head).returns(mock_response(''))
19
27
  assert @index.exists?
@@ -173,6 +181,21 @@ module Tire
173
181
  @index.store article
174
182
  end
175
183
 
184
+ should "properly encode namespaced document types" do
185
+ Configuration.client.expects(:post).with do |url,document|
186
+ url == "#{Configuration.url}/dummy/my_namespace%2Fmy_model/"
187
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
188
+
189
+ module MyNamespace
190
+ class MyModel
191
+ def document_type; "my_namespace/my_model"; end
192
+ def to_indexed_json; "{}"; end
193
+ end
194
+ end
195
+
196
+ @index.store MyNamespace::MyModel.new
197
+ end
198
+
176
199
  should "set default type" do
177
200
  Configuration.client.expects(:post).with("#{Configuration.url}/dummy/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
178
201
  @index.store :title => 'Test'
@@ -190,6 +213,11 @@ module Tire
190
213
  assert_raise(ArgumentError) { @index.store document }
191
214
  end
192
215
 
216
+ should "raise deprecation warning when trying to store a JSON string" do
217
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
218
+ @index.store '{"foo" : "bar"}'
219
+ end
220
+
193
221
  context "document with ID" do
194
222
 
195
223
  should "store Hash it under its ID property" do
@@ -264,6 +292,12 @@ module Tire
264
292
  end
265
293
  end
266
294
 
295
+ should "properly encode document type" do
296
+ Configuration.client.expects(:get).with("#{Configuration.url}/dummy/my_namespace%2Fmy_model/id-1").
297
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
298
+ article = @index.retrieve 'my_namespace/my_model', 'id-1'
299
+ end
300
+
267
301
  end
268
302
 
269
303
  context "when removing" do
@@ -275,6 +309,13 @@ module Tire
275
309
  @index.remove :id => 1, :type => 'article', :title => 'Test'
276
310
  end
277
311
 
312
+ should "get namespaced type from document" do
313
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/articles%2Farticle/1").
314
+ returns(mock_response('{"ok":true,"_id":"1"}')).twice
315
+ @index.remove :id => 1, :type => 'articles/article', :title => 'Test'
316
+ @index.remove :id => 1, :type => 'articles/article', :title => 'Test'
317
+ end
318
+
278
319
  should "set default type" do
279
320
  Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/document/1").
280
321
  returns(mock_response('{"ok":true,"_id":"1"}'))
@@ -306,6 +347,12 @@ module Tire
306
347
  end
307
348
  end
308
349
 
350
+ should "properly encode document type" do
351
+ Configuration.client.expects(:delete).with("#{Configuration.url}/dummy/my_namespace%2Fmy_model/id-1").
352
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
353
+ article = @index.remove 'my_namespace/my_model', 'id-1'
354
+ end
355
+
309
356
  end
310
357
 
311
358
  context "when storing in bulk" do
@@ -353,12 +400,40 @@ module Tire
353
400
 
354
401
  end
355
402
 
356
- should "try again when an error response is received" do
403
+ context "namespaced models" do
404
+ should "not URL-escape the document_type" do
405
+ Configuration.client.expects(:post).with do |url, json|
406
+ puts url, json
407
+ url == "#{Configuration.url}/_bulk" &&
408
+ json =~ %r|"_index":"my_namespace_my_models"| &&
409
+ json =~ %r|"_type":"my_namespace/my_model"|
410
+ end.returns(mock_response('{}', 200))
411
+
412
+ module MyNamespace
413
+ class MyModel
414
+ def document_type; "my_namespace/my_model"; end
415
+ def to_indexed_json; "{}"; end
416
+ end
417
+ end
418
+
419
+ Tire.index('my_namespace_my_models').bulk_store [ MyNamespace::MyModel.new ]
420
+ end
421
+ end
422
+
423
+ should "try again when an exception occurs" do
357
424
  Configuration.client.expects(:post).returns(mock_response('Server error', 503)).at_least(2)
358
425
 
359
426
  assert !@index.bulk_store([ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ])
360
427
  end
361
428
 
429
+ should "try again and the raise when an exception occurs" do
430
+ Configuration.client.expects(:post).returns(mock_response('Server error', 503)).at_least(2)
431
+
432
+ assert_raise(RuntimeError) do
433
+ @index.bulk_store([ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ], {:raise => true})
434
+ end
435
+ end
436
+
362
437
  should "try again when a connection error occurs" do
363
438
  Configuration.client.expects(:post).raises(Errno::ECONNREFUSED, "Connection refused - connect(2)").at_least(2)
364
439
 
@@ -417,7 +492,7 @@ module Tire
417
492
 
418
493
  should "just store it in bulk" do
419
494
  collection = [{ :id => 1, :title => 'Article' }]
420
- @index.expects(:bulk_store).with( collection ).returns(true)
495
+ @index.expects(:bulk_store).with(collection, {} ).returns(true)
421
496
 
422
497
  @index.import collection
423
498
  end
@@ -427,20 +502,20 @@ module Tire
427
502
  context "class" do
428
503
 
429
504
  should "call the passed method and bulk store the results" do
430
- @index.expects(:bulk_store).with([1, 2, 3, 4]).returns(true)
505
+ @index.expects(:bulk_store).with { |c, o| c == [1, 2, 3, 4] }.returns(true)
431
506
 
432
- @index.import ImportData, :paginate
507
+ @index.import ImportData, :method => 'paginate'
433
508
  end
434
509
 
435
510
  should "pass the params to the passed method and bulk store the results" do
436
- @index.expects(:bulk_store).with([1, 2]).returns(true)
437
- @index.expects(:bulk_store).with([3, 4]).returns(true)
511
+ @index.expects(:bulk_store).with { |c,o| c == [1, 2] }.returns(true)
512
+ @index.expects(:bulk_store).with { |c,o| c == [3, 4] }.returns(true)
438
513
 
439
- @index.import ImportData, :paginate, :page => 1, :per_page => 2
514
+ @index.import ImportData, :method => 'paginate', :page => 1, :per_page => 2
440
515
  end
441
516
 
442
517
  should "pass the class when method not passed" do
443
- @index.expects(:bulk_store).with(ImportData).returns(true)
518
+ @index.expects(:bulk_store).with { |c,o| c == ImportData }.returns(true)
444
519
 
445
520
  @index.import ImportData
446
521
  end
@@ -452,7 +527,7 @@ module Tire
452
527
  context "and plain collection" do
453
528
 
454
529
  should "allow to manipulate the collection in the block" do
455
- Tire::Index.any_instance.expects(:bulk_store).with([{ :id => 1, :title => 'ARTICLE' }])
530
+ Tire::Index.any_instance.expects(:bulk_store).with([{ :id => 1, :title => 'ARTICLE' }], {})
456
531
 
457
532
 
458
533
  @index.import [{ :id => 1, :title => 'Article' }] do |articles|
@@ -465,11 +540,11 @@ module Tire
465
540
  context "and object" do
466
541
 
467
542
  should "call the passed block on every batch" do
468
- Tire::Index.any_instance.expects(:bulk_store).with([1, 2])
469
- Tire::Index.any_instance.expects(:bulk_store).with([3, 4])
543
+ Tire::Index.any_instance.expects(:bulk_store).with { |collection, options| collection == [1, 2] }
544
+ Tire::Index.any_instance.expects(:bulk_store).with { |collection, options| collection == [3, 4] }
470
545
 
471
546
  runs = 0
472
- @index.import ImportData, :paginate, :per_page => 2 do |documents|
547
+ @index.import ImportData, :method => 'paginate', :per_page => 2 do |documents|
473
548
  runs += 1
474
549
  # Don't forget to return the documents at the end of the block
475
550
  documents
@@ -479,11 +554,10 @@ module Tire
479
554
  end
480
555
 
481
556
  should "allow to manipulate the documents in passed block" do
482
- Tire::Index.any_instance.expects(:bulk_store).with([2, 3])
483
- Tire::Index.any_instance.expects(:bulk_store).with([4, 5])
484
-
557
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] }
558
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] }
485
559
 
486
- @index.import ImportData, :paginate, :per_page => 2 do |documents|
560
+ @index.import ImportData, :method => :paginate, :per_page => 2 do |documents|
487
561
  # Add 1 to every "document" and return them
488
562
  documents.map { |d| d + 1 }
489
563
  end