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
@@ -39,8 +39,8 @@ module Tire
39
39
  end
40
40
 
41
41
  should "call the passed block on every batch, and NOT manipulate the documents array" do
42
- Tire::Index.any_instance.expects(:bulk_store).with([1, 2])
43
- Tire::Index.any_instance.expects(:bulk_store).with([3, 4])
42
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [1, 2] }
43
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [3, 4] }
44
44
 
45
45
  runs = 0
46
46
  ImportModel.import :per_page => 2 do |documents|
@@ -53,8 +53,8 @@ module Tire
53
53
  end
54
54
 
55
55
  should "manipulate the documents in passed block" do
56
- Tire::Index.any_instance.expects(:bulk_store).with([2, 3])
57
- Tire::Index.any_instance.expects(:bulk_store).with([4, 5])
56
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] }
57
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] }
58
58
 
59
59
  ImportModel.import :per_page => 2 do |documents|
60
60
  # Add 1 to every "document" and return them
@@ -357,7 +357,13 @@ module Tire
357
357
  :type => 'object',
358
358
  :properties => {
359
359
  :first_name => { :type => 'string' },
360
- :last_name => { :type => 'string', :boost => 100 }
360
+ :last_name => { :type => 'string', :boost => 100 },
361
+ :posts => {
362
+ :type => 'object',
363
+ :properties => {
364
+ :title => { :type => 'string', :boost => 10 }
365
+ }
366
+ }
361
367
  }
362
368
  }
363
369
  }
@@ -378,6 +384,10 @@ module Tire
378
384
  indexes :author do
379
385
  indexes :first_name, :type => 'string'
380
386
  indexes :last_name, :type => 'string', :boost => 100
387
+
388
+ indexes :posts do
389
+ indexes :title, :type => 'string', :boost => 10
390
+ end
381
391
  end
382
392
  end
383
393
 
@@ -385,6 +395,7 @@ module Tire
385
395
 
386
396
  assert_not_nil ModelWithNestedMapping.mapping[:author][:properties][:last_name]
387
397
  assert_equal 100, ModelWithNestedMapping.mapping[:author][:properties][:last_name][:boost]
398
+ assert_equal 10, ModelWithNestedMapping.mapping[:author][:properties][:posts][:properties][:title][:boost]
388
399
  end
389
400
 
390
401
  should "define mapping for nested documents" do
@@ -519,20 +530,12 @@ module Tire
519
530
  @model = ActiveModelArticle.new 'id' => 1, 'title' => 'Test'
520
531
  assert_nil MultiJson.decode(@model.to_indexed_json)[:id]
521
532
  assert_nil MultiJson.decode(@model.to_indexed_json)['id']
522
-
523
- @model = SupermodelArticle.new 'id' => 1, 'title' => 'Test'
524
- assert_nil MultiJson.decode(@model.to_indexed_json)[:id]
525
- assert_nil MultiJson.decode(@model.to_indexed_json)['id']
526
533
  end
527
534
 
528
535
  should "not include the type property in serialized document (_source)" do
529
536
  @model = ActiveModelArticle.new 'type' => 'foo', 'title' => 'Test'
530
537
  assert_nil MultiJson.decode(@model.to_indexed_json)[:type]
531
538
  assert_nil MultiJson.decode(@model.to_indexed_json)['type']
532
-
533
- @model = SupermodelArticle.new 'type' => 'foo', 'title' => 'Test'
534
- assert_nil MultiJson.decode(@model.to_indexed_json)[:type]
535
- assert_nil MultiJson.decode(@model.to_indexed_json)['type']
536
539
  end
537
540
 
538
541
  should "serialize itself with serializable_hash when no mapping is set" do
@@ -592,7 +595,7 @@ module Tire
592
595
  assert_equal( {:one => 1}.to_json, model.to_indexed_json )
593
596
 
594
597
  end
595
-
598
+
596
599
  should "serialize mapped properties when mapping procs are set" do
597
600
  class ::ModelWithMappingProcs
598
601
  extend ActiveModel::Naming
@@ -119,6 +119,12 @@ module Tire
119
119
  assert_equal "article", document._type
120
120
  end
121
121
 
122
+ should "properly decode type" do
123
+ @response = { 'hits' => { 'hits' => [ { '_id' => 1, '_type' => 'foo%2Fbar' } ] } }
124
+ document = Results::Collection.new(@response).first
125
+ assert_equal "foo/bar", document._type
126
+ end
127
+
122
128
  end
123
129
 
124
130
  context "wrapping results with selected fields" do
@@ -60,6 +60,14 @@ module Tire
60
60
  assert_equal 'Test', @document.title
61
61
  end
62
62
 
63
+ should "not care about symbols or strings in composite keys" do
64
+ @document = Results::Item.new :highlight => { 'name.ngrams' => 'abc' }
65
+
66
+ assert_not_nil @document.highlight['name.ngrams']
67
+ assert_equal 'abc', @document.highlight['name.ngrams']
68
+ assert_equal @document.highlight['name.ngrams'], @document.highlight['name.ngrams'.to_sym]
69
+ end
70
+
63
71
  should "allow to retrieve values from nested hashes" do
64
72
  assert_not_nil @document.author.name
65
73
  assert_equal 'Kafka', @document.author.name
@@ -137,6 +137,15 @@ module Tire::Search
137
137
  end
138
138
  end
139
139
 
140
+ context "filter facet" do
141
+ should "encode facet options" do
142
+ f = Facet.new('filter_facet') do
143
+ filter :tags, 'ruby'
144
+ end
145
+ assert_equal({ :filter_facet => { :filter => { :term => { :tags => 'ruby' } } } }.to_json, f.to_json)
146
+ end
147
+ end
148
+
140
149
  end
141
150
 
142
151
  end
@@ -5,26 +5,33 @@ module Tire::Search
5
5
  class QueryTest < Test::Unit::TestCase
6
6
 
7
7
  context "Query" do
8
-
9
8
  should "be serialized to JSON" do
10
9
  assert_respond_to Query.new, :to_json
11
10
  end
12
11
 
13
12
  should "return itself as a Hash" do
14
13
  assert_respond_to Query.new, :to_hash
15
- assert_equal( { :term => { :foo => 'bar' } }, Query.new.term(:foo, 'bar').to_hash )
14
+ assert_equal( { :term => { :foo => { :term => 'bar' } } }, Query.new.term(:foo, 'bar').to_hash )
16
15
  end
17
16
 
18
17
  should "allow a block to be given" do
19
- assert_equal( { :term => { :foo => 'bar' } }.to_json, Query.new do
18
+ assert_equal( { :term => { :foo => { :term => 'bar' } } }.to_json, Query.new do
20
19
  term(:foo, 'bar')
21
20
  end.to_json)
22
21
  end
22
+ end
23
23
 
24
+ context "Term query" do
24
25
  should "allow search for single term" do
25
- assert_equal( { :term => { :foo => 'bar' } }, Query.new.term(:foo, 'bar') )
26
+ assert_equal( { :term => { :foo => { :term => 'bar' } } }, Query.new.term(:foo, 'bar') )
26
27
  end
27
28
 
29
+ should "allow search for single term passing an options hash" do
30
+ assert_equal( { :term => { :foo => { :term => 'bar', :boost => 2.0 } } }, Query.new.term(:foo, 'bar', :boost => 2.0) )
31
+ end
32
+ end
33
+
34
+ context "Terms query" do
28
35
  should "allow search for multiple terms" do
29
36
  assert_equal( { :terms => { :foo => ['bar', 'baz'] } }, Query.new.terms(:foo, ['bar', 'baz']) )
30
37
  end
@@ -33,11 +40,15 @@ module Tire::Search
33
40
  assert_equal( { :terms => { :foo => ['bar', 'baz'], :minimum_match => 2 } },
34
41
  Query.new.terms(:foo, ['bar', 'baz'], :minimum_match => 2) )
35
42
  end
43
+ end
36
44
 
45
+ context "Range query" do
37
46
  should "allow search for a range" do
38
47
  assert_equal( { :range => { :age => { :gte => 21 } } }, Query.new.range(:age, { :gte => 21 }) )
39
48
  end
49
+ end
40
50
 
51
+ context "Text query" do
41
52
  should "allow search with a text search" do
42
53
  assert_equal( { :text => {'field' => {:query => 'foo'}}}, Query.new.text('field', 'foo'))
43
54
  end
@@ -46,7 +57,9 @@ module Tire::Search
46
57
  assert_equal( { :text => {'field' => {:query => 'foo', :operator => 'and'}}},
47
58
  Query.new.text('field', 'foo', :operator => 'and'))
48
59
  end
60
+ end
49
61
 
62
+ context "Query String query" do
50
63
  should "allow search with a query string" do
51
64
  assert_equal( { :query_string => { :query => 'title:foo' } },
52
65
  Query.new.string('title:foo') )
@@ -66,7 +79,9 @@ module Tire::Search
66
79
  assert_equal( { :query_string => { :query => 'foo', :fields => ['title.*'], :use_dis_max => true } },
67
80
  Query.new.string('foo', :fields => ['title.*'], :use_dis_max => true) )
68
81
  end
82
+ end
69
83
 
84
+ context "Custom Score query" do
70
85
  should "allow to set script for custom score queries" do
71
86
  query = Query.new.custom_score(:script => "_score * doc['price'].value") do
72
87
  string 'foo'
@@ -84,15 +99,30 @@ module Tire::Search
84
99
  assert_equal 1, query[:custom_score][:params][:a]
85
100
  assert_equal 2, query[:custom_score][:params][:b]
86
101
  end
102
+ end
87
103
 
104
+ context "All query" do
88
105
  should "search for all documents" do
89
106
  assert_equal( { :match_all => { } }, Query.new.all )
90
107
  end
108
+ end
91
109
 
110
+ context "IDs query" do
92
111
  should "search for documents by IDs" do
93
112
  assert_equal( { :ids => { :values => [1, 2], :type => 'foo' } },
94
113
  Query.new.ids([1, 2], 'foo') )
95
114
  end
115
+ end
116
+
117
+ context "FuzzyQuery" do
118
+
119
+ should "allow a fuzzy search" do
120
+ assert_equal( { :fuzzy => { :foo => { :term => 'bar' } } }, Query.new.fuzzy(:foo, 'bar') )
121
+ end
122
+
123
+ should "allow a fuzzy search with an options hash" do
124
+ assert_equal( { :term => { :foo => { :term => 'bar', :boost => 1.0, :min_similarity => 0.5 } } }, Query.new.term(:foo, 'bar', :boost => 1.0, :min_similarity => 0.5 ) )
125
+ end
96
126
 
97
127
  end
98
128
 
@@ -159,7 +189,6 @@ module Tire::Search
159
189
 
160
190
  end
161
191
 
162
-
163
192
  context "FilteredQuery" do
164
193
 
165
194
  should "not raise an error when no block is given" do
@@ -173,7 +202,7 @@ module Tire::Search
173
202
  end
174
203
 
175
204
  query[:filtered].tap do |f|
176
- assert_equal( { :term => { :foo => 'bar' } }, f[:query].to_hash )
205
+ assert_equal( { :term => { :foo => { :term => 'bar' } } }, f[:query].to_hash )
177
206
  assert_equal( { :tags => ['ruby'] }, f[:filter][:and].first[:terms] )
178
207
  end
179
208
  end
@@ -202,7 +231,7 @@ module Tire::Search
202
231
  end
203
232
 
204
233
  query[:filtered].tap do |f|
205
- assert_equal( { :term => { :foo => 'bar' } }, f[:query].to_hash )
234
+ assert_equal( { :term => { :foo => { :term => 'bar' } } }, f[:query].to_hash )
206
235
  assert_equal( { :tags => ['ruby'] }, f[:filter][:and].first[:terms] )
207
236
  end
208
237
  end
@@ -35,6 +35,48 @@ module Tire
35
35
  assert_match %r|index/bar/_search|, s.url
36
36
  end
37
37
 
38
+ should "allow to pass search parameters" do
39
+ s = Search::Search.new('index', :routing => 123, :timeout => 1) { query { string 'foo' } }
40
+
41
+ assert ! s.params.empty?
42
+
43
+ assert_match %r|routing=123|, s.params
44
+ assert_match %r|timeout=1|, s.params
45
+ end
46
+
47
+ should "encode search parameters in the request" do
48
+ Configuration.client.expects(:get).with do |url, payload|
49
+ url.include? 'routing=123&timeout=1'
50
+ end.returns mock_response( { 'hits' => { 'hits' => [ {:_id => 1} ] } }.to_json )
51
+
52
+ Search::Search.new('index', :routing => 123, :timeout => 1) { query { string 'foo' } }.perform
53
+ end
54
+
55
+ should "encode missing params as an empty string" do
56
+ Configuration.client.expects(:get).with do |url, payload|
57
+ (! url.include? '?') && (! url.include? '&')
58
+ end.returns mock_response( { 'hits' => { 'hits' => [ {:_id => 1} ] } }.to_json )
59
+
60
+ s = Search::Search.new('index') { query { string 'foo' } }
61
+ s.perform
62
+
63
+ assert_equal '', s.params
64
+ end
65
+
66
+ should "properly encode namespaced document type" do
67
+ Configuration.client.expects(:get).with do |url, payload|
68
+ url.match %r|index/my_application%2Farticle/_search|
69
+ end.returns mock_response( { 'hits' => { 'hits' => [ {:_id => 1} ] } }.to_json )
70
+
71
+ s = Search::Search.new('index', :type => 'my_application/article') do
72
+ query { string 'foo' }
73
+ end
74
+ s.perform
75
+
76
+ assert_match %r|index/my_application%2Farticle/_search|, s.url
77
+ assert_match %r|index/my_application%2Farticle/_search|, s.to_curl
78
+ end
79
+
38
80
  should "allow to pass block to query" do
39
81
  Search::Query.any_instance.expects(:instance_eval)
40
82
 
@@ -180,7 +222,7 @@ module Tire
180
222
  hash = MultiJson.decode( s.to_json )
181
223
  assert_equal [{'title' => 'desc'}, '_score'], hash['sort']
182
224
  end
183
-
225
+
184
226
  end
185
227
 
186
228
  context "facets" do
@@ -343,6 +385,33 @@ module Tire
343
385
 
344
386
  end
345
387
 
388
+ context "explain" do
389
+
390
+ should "default to false" do
391
+ s = Search::Search.new('index') do
392
+ end
393
+ hash = MultiJson.decode( s.to_json )
394
+ assert_nil hash['explain']
395
+ end
396
+
397
+ should "set the explain field in the request when true" do
398
+ s = Search::Search.new('index') do
399
+ explain true
400
+ end
401
+ hash = MultiJson.decode( s.to_json )
402
+ assert_equal true, hash['explain']
403
+ end
404
+
405
+ should "not set the explain field when false" do
406
+ s = Search::Search.new('index') do
407
+ explain false
408
+ end
409
+ hash = MultiJson.decode( s.to_json )
410
+ assert_nil hash['explain']
411
+ end
412
+
413
+ end
414
+
346
415
  context "boolean queries" do
347
416
 
348
417
  should "wrap other queries" do
@@ -23,21 +23,17 @@ module Tire
23
23
  end
24
24
 
25
25
  should "allow searching with a Ruby Hash" do
26
- Tire::Configuration.client.expects(:post).
27
- with('http://localhost:9200/dummy/_search','{"query":{"query_string":{"query":"foo"}}}').
28
- returns( mock_response('{}') )
29
- Tire::Results::Collection.expects(:new)
26
+ payload = { :query => { :query_string => { :query => 'foo' } } }
27
+ Search::Search.expects(:new).with('dummy', :payload => payload).returns( stub(:perform => true) )
30
28
 
31
- Tire.search 'dummy', :query => { :query_string => { :query => 'foo' }}
29
+ Tire.search 'dummy', payload
32
30
  end
33
31
 
34
32
  should "allow searching with a JSON string" do
35
- Tire::Configuration.client.expects(:post).
36
- with('http://localhost:9200/dummy/_search','{"query":{"query_string":{"query":"foo"}}}').
37
- returns( mock_response('{}') )
38
- Tire::Results::Collection.expects(:new)
33
+ payload = '{"query":{"query_string":{"query":"foo"}}}'
34
+ Search::Search.expects(:new).with('dummy', :payload => payload).returns( stub(:perform => true) )
39
35
 
40
- Tire.search 'dummy', '{"query":{"query_string":{"query":"foo"}}}'
36
+ Tire.search 'dummy', payload
41
37
  end
42
38
 
43
39
  should "raise an error when passed incorrect payload" do
@@ -48,8 +44,8 @@ module Tire
48
44
 
49
45
  should "raise SearchRequestFailed when receiving bad response from backend" do
50
46
  assert_raise(Search::SearchRequestFailed) do
51
- Tire::Configuration.client.expects(:post).returns( mock_response('INDEX DOES NOT EXIST', 404) )
52
- Tire.search 'not-existing', :query => { :query_string => { :query => 'foo' }}
47
+ Tire::Configuration.client.expects(:get).returns( mock_response('INDEX DOES NOT EXIST', 404) )
48
+ Tire.search('not-existing', :query => { :query_string => { :query => 'foo' }}).results
53
49
  end
54
50
  end
55
51
 
@@ -70,6 +66,21 @@ module Tire
70
66
 
71
67
  end
72
68
 
69
+ context "utils" do
70
+
71
+ should "encode a string for URL" do
72
+ assert_equal 'foo+bar', Utils.escape('foo bar')
73
+ assert_equal 'foo%2Fbar', Utils.escape('foo/bar')
74
+ assert_equal 'foo%21', Utils.escape('foo!')
75
+ end
76
+
77
+ should "encode a string from URL" do
78
+ assert_equal 'foo bar', Utils.unescape('foo+bar')
79
+ assert_equal 'foo/bar', Utils.unescape('foo%2Fbar')
80
+ assert_equal 'foo!', Utils.unescape('foo%21')
81
+ end
82
+
83
+ end
73
84
  end
74
85
 
75
86
  end
data/tire.gemspec CHANGED
@@ -28,27 +28,30 @@ Gem::Specification.new do |s|
28
28
  #
29
29
  s.add_dependency "rake"
30
30
  s.add_dependency "rest-client", "~> 1.6.0"
31
- s.add_dependency "multi_json", "~> 1.0"
32
- s.add_dependency "activemodel", "~> 3.0"
33
- s.add_dependency "hashr", "~> 0.0.16"
31
+ s.add_dependency "multi_json", "~> 1.1"
32
+ s.add_dependency "activemodel", ">= 3.0"
33
+ s.add_dependency "hashr", "~> 0.0.19"
34
+ s.add_dependency "rack", ">= 1.4" if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
34
35
 
35
36
  # = Development dependencies
36
37
  #
37
- s.add_development_dependency "bundler", "~> 1.0"
38
+ s.add_development_dependency "bundler", ">= 1.1"
38
39
  s.add_development_dependency "yajl-ruby", "~> 0.8.0"
39
40
  s.add_development_dependency "shoulda"
40
41
  s.add_development_dependency "mocha"
41
- s.add_development_dependency "activerecord", "~> 3"
42
- s.add_development_dependency "mongoid", "~> 2.2.1"
42
+ s.add_development_dependency "activerecord", ">= 3.0"
43
43
  s.add_development_dependency "sqlite3"
44
- s.add_development_dependency "supermodel", "~> 0.1.6"
44
+ s.add_development_dependency "mongoid", "~> 2.2.1"
45
+ s.add_development_dependency "bson_ext"
46
+ s.add_development_dependency "redis-persistence"
45
47
  s.add_development_dependency "curb"
48
+ s.add_development_dependency "minitest"
46
49
 
47
50
  # These gems are not needed for CI at <http://travis-ci.org/#!/karmi/tire>
48
51
  #
49
52
  unless ENV["CI"]
50
53
  s.add_development_dependency "rdoc"
51
- s.add_development_dependency "turn", "~> 0.9"
54
+ s.add_development_dependency "turn", "~> 0.9" if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
52
55
  end
53
56
 
54
57
  s.description = <<-DESC