tire 0.5.4 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.travis.yml +3 -0
  2. data/README.markdown +49 -29
  3. data/examples/rails-application-template.rb +15 -15
  4. data/examples/tire-dsl.rb +24 -24
  5. data/lib/tire.rb +2 -1
  6. data/lib/tire/alias.rb +3 -3
  7. data/lib/tire/dsl.rb +17 -19
  8. data/lib/tire/index.rb +67 -10
  9. data/lib/tire/model/callbacks.rb +1 -1
  10. data/lib/tire/model/indexing.rb +2 -2
  11. data/lib/tire/model/naming.rb +1 -1
  12. data/lib/tire/model/percolate.rb +3 -3
  13. data/lib/tire/model/persistence.rb +1 -1
  14. data/lib/tire/model/persistence/attributes.rb +2 -2
  15. data/lib/tire/model/persistence/storage.rb +2 -2
  16. data/lib/tire/model/search.rb +8 -8
  17. data/lib/tire/multi_search.rb +4 -4
  18. data/lib/tire/rubyext/ruby_1_8.rb +1 -1
  19. data/lib/tire/search.rb +1 -1
  20. data/lib/tire/search/scan.rb +1 -1
  21. data/lib/tire/tasks.rb +1 -1
  22. data/lib/tire/version.rb +7 -11
  23. data/test/integration/active_record_searchable_test.rb +42 -0
  24. data/test/integration/dis_max_queries_test.rb +1 -1
  25. data/test/integration/dsl_search_test.rb +9 -0
  26. data/test/integration/index_mapping_test.rb +82 -7
  27. data/test/integration/persistent_model_test.rb +17 -0
  28. data/test/integration/sort_test.rb +16 -0
  29. data/test/models/active_record_models.rb +9 -0
  30. data/test/models/persistent_article.rb +1 -1
  31. data/test/models/persistent_article_in_index.rb +1 -1
  32. data/test/models/persistent_article_in_namespace.rb +1 -1
  33. data/test/models/persistent_article_with_percolation.rb +5 -0
  34. data/test/models/persistent_articles_with_custom_index_name.rb +1 -1
  35. data/test/test_helper.rb +1 -1
  36. data/test/unit/index_alias_test.rb +1 -1
  37. data/test/unit/index_test.rb +79 -1
  38. data/test/unit/model_persistence_test.rb +11 -1
  39. data/test/unit/model_search_test.rb +2 -2
  40. data/test/unit/tire_test.rb +11 -7
  41. data/tire.gemspec +4 -3
  42. metadata +61 -51
@@ -32,6 +32,10 @@ module Tire
32
32
  create_table :active_record_class_with_dynamic_index_names do |t|
33
33
  t.string :title
34
34
  end
35
+ create_table :active_record_model_with_percolations do |t|
36
+ t.string :title
37
+ t.datetime :created_at, :default => 'NOW()'
38
+ end
35
39
  end
36
40
  end
37
41
 
@@ -158,6 +162,16 @@ module Tire
158
162
  assert hit['_score'] > 0
159
163
  end
160
164
  end
165
+
166
+ should "provide access to highlighted fields in hit" do
167
+ results = ActiveRecordArticle.search :load => true do
168
+ query { string '"Test 1" OR "Test 2"' }
169
+ highlight :title
170
+ end
171
+ results.each_with_hit do |result, hit|
172
+ assert_equal 1, hit['highlight']['title'].size
173
+ end
174
+ end
161
175
  end
162
176
 
163
177
  context "with pagination" do
@@ -295,6 +309,16 @@ module Tire
295
309
  assert_nil results.first
296
310
  end
297
311
 
312
+ should "find second page with four loaded models" do
313
+ results = ActiveRecordArticle.search :load => true, :page => 2, :per_page => 5 do |search|
314
+ search.query { |query| query.string @q }
315
+ search.sort { by :title }
316
+ end
317
+ assert_equal 4, results.size
318
+ assert results.all? { |r| assert_instance_of ActiveRecordArticle, r }
319
+ assert_equal 'Test6', results.first.title
320
+ end
321
+
298
322
  end
299
323
 
300
324
  context "with from/size" do
@@ -571,6 +595,24 @@ module Tire
571
595
  end
572
596
 
573
597
  end
598
+
599
+ context "percolated search" do
600
+ setup do
601
+ ActiveRecordModelWithPercolation.index.register_percolator_query('alert') { string 'warning' }
602
+ Tire.index('_percolator').refresh
603
+ end
604
+
605
+ should "return matching queries when percolating" do
606
+ a = ActiveRecordModelWithPercolation.new :title => 'Warning!'
607
+ assert_contains a.percolate, 'alert'
608
+ end
609
+
610
+ should "return matching queries when saving" do
611
+ a = ActiveRecordModelWithPercolation.create! :title => 'Warning!'
612
+ assert_contains a.matches, 'alert'
613
+ end
614
+ end
615
+
574
616
  end
575
617
 
576
618
  end
@@ -22,7 +22,7 @@ module Tire
22
22
  Tire.index('dis_max_test').delete
23
23
  end
24
24
 
25
- should "boost matches in both fields" do
25
+ should_eventually "boost matches in both fields" do
26
26
  dis_max = Tire.search 'dis_max_test' do
27
27
  query do
28
28
  dis_max do
@@ -13,6 +13,15 @@ module Tire
13
13
 
14
14
  assert_equal 2, s.results.count
15
15
  assert_equal 2, s.results.facets['tags']['count']
16
+ assert_match /_search\?pretty' -d '{/, s.to_curl, 'Make sure to ignore payload in URL params'
17
+ end
18
+
19
+ should "allow passing URL parameters" do
20
+ s = Tire.search 'articles-test', search_type: 'count', query: { match: { tags: 'ruby' } }
21
+
22
+ assert_equal 0, s.results.count
23
+ assert_equal 2, s.results.total
24
+ assert_match /_search.*search_type=count.*' -d '{/, s.to_curl
16
25
  end
17
26
 
18
27
  should "allow building search query iteratively" do
@@ -8,7 +8,7 @@ module Tire
8
8
  context "Default mapping" do
9
9
  teardown { Tire.index('mapped-index').delete; sleep 0.1 }
10
10
 
11
- should "create and return the default mapping" do
11
+ should "create and return the default mapping as a Hash" do
12
12
 
13
13
  index = Tire.index 'mapped-index' do
14
14
  create
@@ -24,17 +24,92 @@ module Tire
24
24
 
25
25
  context "Creating index with mapping" do
26
26
  teardown { Tire.index('mapped-index').delete; sleep 0.1 }
27
-
27
+
28
28
  should "create the specified mapping" do
29
-
30
29
  index = Tire.index 'mapped-index' do
31
- create :mappings => { :article => { :properties => { :title => { :type => 'string', :boost => 2.0, :store => 'yes' } } } }
30
+ create mappings: {
31
+ article: {
32
+ _all: { enabled: false },
33
+ properties: {
34
+ title: { type: 'string', boost: 2.0, store: 'yes' }
35
+ }
36
+ }
37
+ }
32
38
  end
33
39
 
34
- # p index.mapping
40
+ # p index.mapping
41
+ assert_equal false, index.mapping['article']['_all']['enabled'], index.mapping.inspect
35
42
  assert_equal 2.0, index.mapping['article']['properties']['title']['boost'], index.mapping.inspect
36
- assert_equal 'yes', index.mapping['article']['properties']['title']['store'], index.mapping.inspect
37
-
43
+ end
44
+ end
45
+
46
+ context "Update mapping" do
47
+ setup { Tire.index("mapped-index").create; sleep 1 }
48
+ teardown { Tire.index("mapped-index").delete; sleep 0.1 }
49
+
50
+ should "update the mapping for type" do
51
+ index = Tire.index("mapped-index")
52
+
53
+ index.mapping "article", :properties => { :body => { :type => "string" } }
54
+ assert_equal({ "type" => "string" }, index.mapping["article"]["properties"]["body"])
55
+
56
+ assert index.mapping("article", :properties => { :title => { :type => "string" } })
57
+
58
+ mapping = index.mapping
59
+
60
+ # Verify return value
61
+ assert mapping, index.response.inspect
62
+
63
+ # Verify response
64
+ assert_equal( { "type" => "string" }, mapping["article"]["properties"]["body"] )
65
+ assert_equal( { "type" => "string" }, mapping["article"]["properties"]["title"] )
66
+ end
67
+
68
+ should "fail to update the mapping in an incompatible way" do
69
+ index = Tire.index("mapped-index")
70
+
71
+ # 1. Update initial index mapping
72
+ assert index.mapping "article", properties: { body: { type: "string" } }
73
+ assert_equal( { "type" => "string" }, index.mapping["article"]["properties"]["body"] )
74
+
75
+ # 2. Attempt to update the mapping in incompatible way (change property type)
76
+ mapping = index.mapping "article", :properties => { :body => { :type => "integer" } }
77
+
78
+ # Verify return value
79
+ assert !mapping, index.response.inspect
80
+ #
81
+ # Verify response
82
+ assert_match /MergeMappingException/, index.response.body
83
+ end
84
+
85
+ should "honor the `ignore_conflicts` option" do
86
+ index = Tire.index("mapped-index")
87
+
88
+ # 1. Update initial index mapping
89
+ assert index.mapping "article", properties: { body: { type: "string" } }
90
+ assert_equal( { "type" => "string" }, index.mapping["article"]["properties"]["body"] )
91
+
92
+ # 2. Attempt to update the mapping in incompatible way and ignore conflicts
93
+ mapping = index.mapping "article", ignore_conflicts: true, properties: { body: { type: "integer" } }
94
+
95
+ # Verify return value (true since we ignore conflicts)
96
+ assert mapping, index.response.inspect
97
+ end
98
+
99
+ end
100
+
101
+ context "Delete mapping" do
102
+ setup { Tire.index("mapped-index").create; sleep 1 }
103
+ teardown { Tire.index("mapped-index").delete; sleep 0.1 }
104
+
105
+ should "delete the mapping for type" do
106
+ index = Tire.index("mapped-index")
107
+
108
+ # 1. Update initial index mapping
109
+ assert index.mapping 'article', properties: { body: { type: "string" } }
110
+
111
+ assert index.delete_mapping 'article'
112
+ assert index.mapping.empty?, index.response.inspect
38
113
  end
39
114
  end
40
115
 
@@ -182,6 +182,23 @@ module Tire
182
182
  end
183
183
  end
184
184
 
185
+ context "percolated search" do
186
+ setup do
187
+ PersistentArticleWithPercolation.index.register_percolator_query('alert') { string 'warning' }
188
+ Tire.index('_percolator').refresh
189
+ end
190
+
191
+ should "return matching queries when percolating" do
192
+ a = PersistentArticleWithPercolation.new :title => 'Warning!'
193
+ assert_contains a.percolate, 'alert'
194
+ end
195
+
196
+ should "return matching queries when saving" do
197
+ a = PersistentArticleWithPercolation.create :title => 'Warning!'
198
+ assert_contains a.matches, 'alert'
199
+ end
200
+ end
201
+
185
202
  end
186
203
 
187
204
  end
@@ -29,6 +29,22 @@ module Tire
29
29
  assert_equal 'Two', s.results.first[:title]
30
30
  end
31
31
 
32
+ should "sort by multiple fields" do
33
+ q = '*'
34
+ s = Tire.search('articles-test') do
35
+ query { string q }
36
+ sort do
37
+ by :words, :desc
38
+ by :title, :asc
39
+ end
40
+ end
41
+
42
+ # p s.results.to_a.map { |i| [i.title, i.sort] }
43
+ assert_equal 'Three', s.results[0][:title]
44
+ assert_equal 'Four', s.results[1][:title]
45
+ assert_equal 'Two', s.results[2][:title]
46
+ end
47
+
32
48
  end
33
49
 
34
50
  end
@@ -120,3 +120,12 @@ class ActiveRecordNamespace::MyModel < ActiveRecord::Base
120
120
  include Tire::Model::Search
121
121
  include Tire::Model::Callbacks
122
122
  end
123
+
124
+ # Model with percolation
125
+
126
+ class ActiveRecordModelWithPercolation < ActiveRecord::Base
127
+ include Tire::Model::Search
128
+ include Tire::Model::Callbacks
129
+
130
+ percolate!
131
+ end
@@ -1,4 +1,4 @@
1
- # Example class with ElasticSearch persistence
1
+ # Example class with Elasticsearch persistence
2
2
 
3
3
  class PersistentArticle
4
4
 
@@ -1,4 +1,4 @@
1
- # Example class with ElasticSearch persistence in index `persistent_articles`
1
+ # Example class with Elasticsearch persistence in index `persistent_articles`
2
2
  #
3
3
  # The `index` is `persistent_articles`
4
4
  #
@@ -1,4 +1,4 @@
1
- # Example namespaced class with ElasticSearch persistence
1
+ # Example namespaced class with Elasticsearch persistence
2
2
  #
3
3
  # The `document_type` is `my_namespace/persistent_article_in_namespace`
4
4
  #
@@ -0,0 +1,5 @@
1
+ class PersistentArticleWithPercolation
2
+ include Tire::Model::Persistence
3
+ property :title
4
+ percolate!
5
+ end
@@ -1,4 +1,4 @@
1
- # Example class with ElasticSearch persistence and custom index name
1
+ # Example class with Elasticsearch persistence and custom index name
2
2
 
3
3
  class PersistentArticleWithCustomIndexName
4
4
 
@@ -84,7 +84,7 @@ module Test::Integration
84
84
  begin
85
85
  ::RestClient.get URL
86
86
  rescue Errno::ECONNREFUSED
87
- abort "\n\n#{'-'*87}\n[ABORTED] You have to run ElasticSearch on #{URL} for integration tests\n#{'-'*87}\n\n"
87
+ abort "\n\n#{'-'*87}\n[ABORTED] You have to run Elasticsearch on #{URL} for integration tests\n#{'-'*87}\n\n"
88
88
  end
89
89
 
90
90
  ::RestClient.delete "#{URL}/articles-test" rescue nil
@@ -130,7 +130,7 @@ module Tire
130
130
 
131
131
  context "saving" do
132
132
 
133
- should "send data to ElasticSearch" do
133
+ should "send data to Elasticsearch" do
134
134
  Configuration.client.expects(:post).with do |url, json|
135
135
  url == "#{Configuration.url}/_aliases" &&
136
136
  json =~ /"index":"index_2012_05"/ &&
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  module Tire
@@ -170,7 +172,7 @@ module Tire
170
172
  }
171
173
  end
172
174
 
173
- should "return the mapping" do
175
+ should "return the mapping as a Hash" do
174
176
  json =<<-JSON
175
177
  {
176
178
  "dummy" : {
@@ -189,6 +191,33 @@ module Tire
189
191
  assert_equal 2.0, @index.mapping['article']['properties']['title']['boost']
190
192
  end
191
193
 
194
+ should "update the mapping" do
195
+ Configuration.client.expects(:put).returns(mock_response('{"ok":true,"acknowledged":true}', 200))
196
+ assert @index.mapping( 'document', { properties: { body: {type: 'string', analyzer: 'english'} } } )
197
+ end
198
+
199
+ should "fail to update the mapping when conflicts occur" do
200
+ Configuration.client.expects(:put).returns(mock_response('{"error":"MergeMappingException","status":400}', 400))
201
+ assert ! @index.mapping( 'document', { properties: { body: {type: 'string', analyzer: 'english'} } } )
202
+ end
203
+
204
+ should "raise an exception for the bang method" do
205
+ Configuration.client.expects(:put).returns(mock_response('{"error":"MergeMappingException","status":400}', 400))
206
+ assert_raise(RuntimeError) do
207
+ @index.mapping!("blah", {})
208
+ end
209
+ end
210
+
211
+ should "delete the mapping" do
212
+ Configuration.client.expects(:delete).returns(mock_response('{"ok":true}', 200))
213
+ assert @index.delete_mapping('document')
214
+ end
215
+
216
+ should "fail when deleting the mapping for non-existing type" do
217
+ Configuration.client.expects(:delete).returns(mock_response('{"error":"TypeMissingException[[dummy] type[document] missing]","status":404}', 400))
218
+ assert ! @index.delete_mapping('document')
219
+ end
220
+
192
221
  end
193
222
 
194
223
  context "settings" do
@@ -296,6 +325,16 @@ module Tire
296
325
  @index.store( {:id => 123, :title => 'Test'}, {:routing => 'abc'} )
297
326
  end
298
327
 
328
+ should "extract the replication type from options" do
329
+ Configuration.client.expects(:post).
330
+ with do |url, payload|
331
+ assert_equal "#{Configuration.url}/dummy/document/?replication=async", url
332
+ end.
333
+ returns(mock_response('{"ok":true,"_id":"test"}'))
334
+
335
+ @index.store({:title => 'Test'}, {:replication => 'async'})
336
+ end
337
+
299
338
  context "document with ID" do
300
339
 
301
340
  should "store Hash it under its ID property" do
@@ -331,6 +370,16 @@ module Tire
331
370
  assert_equal 'c', @index.get_id_from_document(document_3)
332
371
  end
333
372
 
373
+ should "escape the ID in URL" do
374
+ Configuration.client.expects(:post).with do |url, payload|
375
+ assert_equal "#{@index.url}/document/%C3%A4ccent", url
376
+ assert_equal 'äccent', MultiJson.load(payload)['id']
377
+ end.
378
+ returns(mock_response('{"ok":true,"_id":"äccent"}'))
379
+
380
+ @index.store :id => 'äccent'
381
+ end
382
+
334
383
  end
335
384
 
336
385
  end
@@ -389,6 +438,13 @@ module Tire
389
438
  end
390
439
  end
391
440
 
441
+ should "escape the ID in URL" do
442
+ Configuration.client.expects(:get).with("#{@index.url}/document/%C3%A4ccent").
443
+ returns(mock_response('{"_id":"äccent"}'))
444
+
445
+ @index.retrieve 'document', 'äccent'
446
+ end
447
+
392
448
  should "properly encode document type" do
393
449
  Configuration.client.expects(:get).with("#{@index.url}/my_namespace%2Fmy_model/id-1").
394
450
  returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
@@ -467,6 +523,13 @@ module Tire
467
523
  end
468
524
  end
469
525
 
526
+ should "escape the ID in URL" do
527
+ Configuration.client.expects(:delete).with("#{@index.url}/document/%C3%A4ccent").
528
+ returns(mock_response('{"ok":true,"_id":"äccent"}'))
529
+
530
+ @index.remove 'document', 'äccent'
531
+ end
532
+
470
533
  should "properly encode document type" do
471
534
  Configuration.client.expects(:delete).with("#{@index.url}/my_namespace%2Fmy_model/id-1").
472
535
  returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
@@ -523,6 +586,15 @@ module Tire
523
586
  assert_raise(ArgumentError) { @index.update(nil, '123', :script => 'foobar') }
524
587
  end
525
588
 
589
+ should "escape the ID in URL" do
590
+ Configuration.client.expects(:post).with do |url,payload|
591
+ assert_equal( "#{@index.url}/document/%C3%A4ccent/_update", url )
592
+ end.
593
+ returns(mock_response('{"ok":"true","_id":"äccent","_version":"2"}'))
594
+
595
+ assert @index.update('document', 'äccent', {:doc => {:foo => 'bar'}})
596
+ end
597
+
526
598
  should "raise an error when no script or partial document is passed" do
527
599
  assert_raise ArgumentError do
528
600
  @index.update('article', "42", {:foo => 'bar'})
@@ -749,6 +821,12 @@ module Tire
749
821
  @index.bulk :index, [ {:id => '1', :title => 'One'} ]
750
822
  end
751
823
 
824
+
825
+ should "return immediately with empty collection" do
826
+ Configuration.client.expects(:post).never
827
+ @index.bulk_store []
828
+ end
829
+
752
830
  end
753
831
 
754
832
  context "when importing" do