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,289 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+ module Model
5
+
6
+ class SearchTest < Test::Unit::TestCase
7
+
8
+ context "Model::Search" do
9
+
10
+ setup do
11
+ @stub = stub('search') { stubs(:query).returns(self); stubs(:perform).returns(self); stubs(:results).returns([]) }
12
+ end
13
+
14
+ teardown do
15
+ ActiveModelArticleWithCustomIndexName.index_name 'custom-index-name'
16
+ end
17
+
18
+ should "have the search method" do
19
+ assert_respond_to Model::Search, :search
20
+ assert_respond_to ActiveModelArticle, :search
21
+ end
22
+
23
+ should "search in index named after class name by default" do
24
+ i = 'active_model_articles'
25
+ Tire::Search::Search.expects(:new).with(i, {}).returns(@stub)
26
+
27
+ ActiveModelArticle.search 'foo'
28
+ end
29
+
30
+ should "search in custom name" do
31
+ first = 'custom-index-name'
32
+ second = 'another-custom-index-name'
33
+
34
+ Tire::Search::Search.expects(:new).with(first, {}).returns(@stub)
35
+ ActiveModelArticleWithCustomIndexName.index_name 'custom-index-name'
36
+ ActiveModelArticleWithCustomIndexName.search 'foo'
37
+
38
+ Tire::Search::Search.expects(:new).with(second, {}).returns(@stub)
39
+ ActiveModelArticleWithCustomIndexName.index_name 'another-custom-index-name'
40
+ ActiveModelArticleWithCustomIndexName.search 'foo'
41
+
42
+ Tire::Search::Search.expects(:new).with(first, {}).returns(@stub)
43
+ ActiveModelArticleWithCustomIndexName.index_name 'custom-index-name'
44
+ ActiveModelArticleWithCustomIndexName.search 'foo'
45
+ end
46
+
47
+ should "allow to refresh index" do
48
+ Index.any_instance.expects(:refresh)
49
+
50
+ ActiveModelArticle.index.refresh
51
+ end
52
+
53
+ should "wrap results in proper class with ID and score and not change the original wrapper" do
54
+ response = { 'hits' => { 'hits' => [{'_id' => 1, '_score' => 0.8, '_source' => { 'title' => 'Article' }}] } }
55
+ Configuration.client.expects(:post).returns(mock_response(response.to_json))
56
+
57
+ collection = ActiveModelArticle.search 'foo'
58
+ assert_instance_of Results::Collection, collection
59
+
60
+ assert_equal Results::Item, Tire::Configuration.wrapper
61
+
62
+ document = collection.first
63
+
64
+ assert_instance_of ActiveModelArticle, document
65
+ assert_not_nil document.score
66
+ assert_equal 1, document.id
67
+ assert_equal 'Article', document.title
68
+ end
69
+
70
+ context "searching with a block" do
71
+
72
+ should "pass on whatever block it received" do
73
+ Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
74
+ Tire::Search::Query.any_instance.expects(:string).with('foo').returns(@stub)
75
+
76
+ ActiveModelArticle.search { query { string 'foo' } }
77
+ end
78
+
79
+ should "allow to pass block with argument to query, allowing to use local variables from outer scope" do
80
+ Tire::Search::Query.any_instance.expects(:instance_eval).never
81
+ Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
82
+ Tire::Search::Query.any_instance.expects(:string).with('foo').returns(@stub)
83
+
84
+ my_query = 'foo'
85
+ ActiveModelArticle.search do
86
+ query do |query|
87
+ query.string(my_query)
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ context "searching with query string" do
95
+
96
+ setup do
97
+ @q = 'foo AND bar'
98
+
99
+ Tire::Search::Query.any_instance.expects(:string).with( @q ).returns(@stub)
100
+ Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
101
+ end
102
+
103
+ should "search for query string" do
104
+ ActiveModelArticle.search @q
105
+ end
106
+
107
+ should "allow to pass :order option" do
108
+ Tire::Search::Sort.any_instance.expects(:title)
109
+
110
+ ActiveModelArticle.search @q, :order => 'title'
111
+ end
112
+
113
+ should "allow to pass :sort option as :order option" do
114
+ Tire::Search::Sort.any_instance.expects(:title)
115
+
116
+ ActiveModelArticle.search @q, :sort => 'title'
117
+ end
118
+
119
+ should "allow to specify sort direction" do
120
+ Tire::Search::Sort.any_instance.expects(:title).with('DESC')
121
+
122
+ ActiveModelArticle.search @q, :order => 'title DESC'
123
+ end
124
+
125
+ should "allow to specify more fields to sort on" do
126
+ Tire::Search::Sort.any_instance.expects(:title).with('DESC')
127
+ Tire::Search::Sort.any_instance.expects(:field).with('author.name', nil)
128
+
129
+ ActiveModelArticle.search @q, :order => ['title DESC', 'author.name']
130
+ end
131
+
132
+ should "allow to specify number of results per page" do
133
+ Tire::Search::Search.any_instance.expects(:size).with(20)
134
+
135
+ ActiveModelArticle.search @q, :per_page => 20
136
+ end
137
+
138
+ should "allow to specify first page in paginated results" do
139
+ Tire::Search::Search.any_instance.expects(:size).with(10)
140
+ Tire::Search::Search.any_instance.expects(:from).with(0)
141
+
142
+ ActiveModelArticle.search @q, :per_page => 10, :page => 1
143
+ end
144
+
145
+ should "allow to specify page further in paginated results" do
146
+ Tire::Search::Search.any_instance.expects(:size).with(10)
147
+ Tire::Search::Search.any_instance.expects(:from).with(20)
148
+
149
+ ActiveModelArticle.search @q, :per_page => 10, :page => 3
150
+ end
151
+
152
+ end
153
+
154
+ should "not set callback when hooks are missing" do
155
+ @model = ActiveModelArticle.new
156
+ @model.expects(:update_elastic_search_index).never
157
+
158
+ @model.save
159
+ end
160
+
161
+ should "fire :after_save callbacks" do
162
+ @model = ActiveModelArticleWithCallbacks.new
163
+ @model.expects(:update_elastic_search_index)
164
+
165
+ @model.save
166
+ end
167
+
168
+ should "fire :after_destroy callbacks" do
169
+ @model = ActiveModelArticleWithCallbacks.new
170
+ @model.expects(:update_elastic_search_index)
171
+
172
+ @model.destroy
173
+ end
174
+
175
+ should "store the record in index on :update_elastic_search_index when saved" do
176
+ @model = ActiveModelArticleWithCallbacks.new
177
+ Tire::Index.any_instance.expects(:store)
178
+
179
+ @model.save
180
+ end
181
+
182
+ should "remove the record from index on :update_elastic_search_index when destroyed" do
183
+ @model = ActiveModelArticleWithCallbacks.new
184
+ i = mock('index') { expects(:remove) }
185
+ Tire::Index.expects(:new).with('active_model_article_with_callbacks').returns(i)
186
+
187
+ @model.destroy
188
+ end
189
+
190
+ context "with custom mapping" do
191
+
192
+ should "create the index with mapping" do
193
+ expected_mapping = {
194
+ :mappings => { :model_with_custom_mapping => {
195
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
196
+ }}
197
+ }
198
+
199
+ Tire::Index.any_instance.expects(:create).with(expected_mapping)
200
+
201
+ class ::ModelWithCustomMapping
202
+ extend ActiveModel::Naming
203
+
204
+ include Tire::Model::Search
205
+ include Tire::Model::Callbacks
206
+
207
+ mapping do
208
+ indexes :title, :type => 'string', :analyzer => 'snowball', :boost => 10
209
+ end
210
+
211
+ end
212
+
213
+ assert_equal 'snowball', ModelWithCustomMapping.mapping[:title][:analyzer]
214
+ end
215
+
216
+ end
217
+
218
+ context "serialization" do
219
+ setup { Tire::Index.any_instance.stubs(:create).returns(true) }
220
+
221
+ should "serialize itself into JSON without 'root'" do
222
+ @model = ActiveModelArticle.new 'title' => 'Test'
223
+ assert_equal({'title' => 'Test'}.to_json, @model.to_indexed_json)
224
+ end
225
+
226
+ should "serialize itself with serializable_hash when no mapping is set" do
227
+
228
+ class ::ModelWithoutMapping
229
+ extend ActiveModel::Naming
230
+ include ActiveModel::Serialization
231
+ include Tire::Model::Search
232
+ include Tire::Model::Callbacks
233
+
234
+ # Do NOT configure any mapping
235
+
236
+ attr_reader :attributes
237
+
238
+ def initialize(attributes = {}); @attributes = attributes; end
239
+
240
+ def method_missing(name, *args, &block)
241
+ attributes[name.to_sym] || attributes[name.to_s] || super
242
+ end
243
+ end
244
+
245
+ model = ::ModelWithoutMapping.new :one => 1, :two => 2
246
+ assert_equal( {:one => 1, :two => 2}, model.serializable_hash )
247
+
248
+ # Bot properties are returned
249
+ assert_equal( {:one => 1, :two => 2}.to_json, model.to_indexed_json )
250
+ end
251
+
252
+ should "serialize only mapped properties when mapping is set" do
253
+
254
+ class ::ModelWithMapping
255
+ extend ActiveModel::Naming
256
+ include ActiveModel::Serialization
257
+ include Tire::Model::Search
258
+ include Tire::Model::Callbacks
259
+
260
+ mapping do
261
+ # ONLY index the 'one' attribute
262
+ indexes :one, :type => 'string', :analyzer => 'keyword'
263
+ end
264
+
265
+ attr_reader :attributes
266
+
267
+ def initialize(attributes = {}); @attributes = attributes; end
268
+
269
+ def method_missing(name, *args, &block)
270
+ attributes[name.to_sym] || attributes[name.to_s] || super
271
+ end
272
+ end
273
+
274
+ model = ::ModelWithMapping.new :one => 1, :two => 2
275
+ assert_equal( {:one => 1, :two => 2}, model.serializable_hash )
276
+
277
+ # Only the mapped property is returned
278
+ assert_equal( {:one => 1}.to_json, model.to_indexed_json )
279
+
280
+ end
281
+
282
+ end
283
+
284
+ end
285
+
286
+ end
287
+
288
+ end
289
+ end
@@ -0,0 +1,131 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ResultsCollectionTest < Test::Unit::TestCase
6
+
7
+ context "Collection" do
8
+ setup do
9
+ Configuration.reset :wrapper
10
+ @default_response = { 'hits' => { 'hits' => [{'_id' => 1, '_score' => 1, '_source' => {:title => 'Test'}},
11
+ {'_id' => 2},
12
+ {'_id' => 3}] } }
13
+ end
14
+
15
+ should "be iterable" do
16
+ assert_respond_to Results::Collection.new(@default_response), :each
17
+ assert_respond_to Results::Collection.new(@default_response), :size
18
+ assert_nothing_raised do
19
+ Results::Collection.new(@default_response).each { |item| item.id + 1 }
20
+ Results::Collection.new(@default_response).map { |item| item.id + 1 }
21
+ end
22
+ end
23
+
24
+ should "have size" do
25
+ assert_equal 3, Results::Collection.new(@default_response).size
26
+ end
27
+
28
+ should "be initialized with parsed json" do
29
+ assert_nothing_raised do
30
+ collection = Results::Collection.new( @default_response )
31
+ assert_equal 3, collection.results.count
32
+ end
33
+ end
34
+
35
+ should "store passed options" do
36
+ collection = Results::Collection.new( @default_response, :per_page => 20, :page => 2 )
37
+ assert_equal 20, collection.options[:per_page]
38
+ assert_equal 2, collection.options[:page]
39
+ end
40
+
41
+ context "wrapping results" do
42
+
43
+ setup do
44
+ @response = { 'hits' => { 'hits' => [ { '_id' => 1, '_score' => 0.5, '_source' => { :title => 'Test', :body => 'Lorem' } } ] } }
45
+ end
46
+
47
+ should "wrap hits in Item by default" do
48
+ document = Results::Collection.new(@response).first
49
+ assert_kind_of Results::Item, document
50
+ assert_equal 'Test', document.title
51
+ end
52
+
53
+ should "NOT allow access to raw underlying Hash in Item" do
54
+ document = Results::Collection.new(@response).first
55
+ assert_nil document[:_source]
56
+ assert_nil document['_source']
57
+ end
58
+
59
+ should "allow wrapping hits in a Hash" do
60
+ Configuration.wrapper(Hash)
61
+
62
+ document = Results::Collection.new(@response).first
63
+ assert_kind_of Hash, document
64
+ assert_raise(NoMethodError) { document.title }
65
+ assert_equal 'Test', document['_source'][:title]
66
+ end
67
+
68
+ should "allow wrapping hits in custom class" do
69
+ Configuration.wrapper(Article)
70
+
71
+ article = Results::Collection.new(@response).first
72
+ assert_kind_of Article, article
73
+ assert_equal 'Test', article.title
74
+ end
75
+
76
+ should "return score" do
77
+ document = Results::Collection.new(@response).first
78
+ assert_equal 0.5, document._score
79
+ end
80
+
81
+ should "return id" do
82
+ document = Results::Collection.new(@response).first
83
+ assert_equal 1, document.id
84
+ end
85
+
86
+ end
87
+
88
+ context "while paginating results" do
89
+
90
+ setup do
91
+ @default_response = { 'hits' => { 'hits' => [{'_id' => 1, '_score' => 1, '_source' => {:title => 'Test'}},
92
+ {'_id' => 2},
93
+ {'_id' => 3}],
94
+ 'total' => 3,
95
+ 'took' => 1 } }
96
+ @collection = Results::Collection.new( @default_response, :per_page => 1, :page => 2 )
97
+ end
98
+
99
+ should "return total entries" do
100
+ assert_equal 3, @collection.total
101
+ assert_equal 3, @collection.total_entries
102
+ end
103
+
104
+ should "return total pages" do
105
+ assert_equal 3, @collection.total_pages
106
+ end
107
+
108
+ should "return total pages when per_page option not set" do
109
+ collection = Results::Collection.new( @default_response, :page => 1 )
110
+ assert_equal 1, collection.total_pages
111
+ end
112
+
113
+ should "return current page" do
114
+ assert_equal 2, @collection.current_page
115
+ end
116
+
117
+ should "return previous page" do
118
+ assert_equal 1, @collection.previous_page
119
+ end
120
+
121
+ should "return next page" do
122
+ assert_equal 3, @collection.next_page
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+
131
+ end
@@ -0,0 +1,59 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ResultsItemTest < Test::Unit::TestCase
6
+
7
+ context "Item" do
8
+
9
+ setup do
10
+ @document = Results::Item.new :title => 'Test', :author => { :name => 'Kafka' }
11
+ end
12
+
13
+ should "be initialized with a Hash" do
14
+ assert_raise(ArgumentError) { Results::Item.new('FUUUUUUU') }
15
+
16
+ assert_nothing_raised do
17
+ d = Results::Item.new(:id => 1)
18
+ assert_instance_of Results::Item, d
19
+ end
20
+ end
21
+
22
+ should "respond to :to_indexed_json" do
23
+ assert_respond_to Results::Item.new, :to_indexed_json
24
+ end
25
+
26
+ should "retrieve simple values from underlying hash" do
27
+ assert_equal 'Test', @document[:title]
28
+ end
29
+
30
+ should "retrieve hash values from underlying hash" do
31
+ assert_equal 'Kafka', @document[:author][:name]
32
+ end
33
+
34
+ should "allow to retrieve value by methods" do
35
+ assert_not_nil @document.title
36
+ assert_equal 'Test', @document.title
37
+ end
38
+
39
+ should "return nil for non-existing keys/methods" do
40
+ assert_nothing_raised { @document.whatever }
41
+ assert_nil @document.whatever
42
+ end
43
+
44
+ should "not care about symbols or strings in keys" do
45
+ @document = Results::Item.new 'title' => 'Test'
46
+ assert_not_nil @document.title
47
+ assert_equal 'Test', @document.title
48
+ end
49
+
50
+ should "allow to retrieve values from nested hashes" do
51
+ assert_not_nil @document.author.name
52
+ assert_equal 'Kafka', @document.author.name
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ end