tire 0.4.0.pre → 0.4.0.rc

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