tire 0.1.0

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