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,3 @@
1
+ module Tire
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1 @@
1
+ {"title" : "One", "tags" : ["ruby"], "published_on" : "2011-01-01"}
@@ -0,0 +1 @@
1
+ {"title" : "Two", "tags" : ["ruby", "python"], "published_on" : "2011-01-02"}
@@ -0,0 +1 @@
1
+ {"title" : "Three", "tags" : ["java"], "published_on" : "2011-01-02"}
@@ -0,0 +1 @@
1
+ {"title" : "Four", "tags" : ["erlang"], "published_on" : "2011-01-03"}
@@ -0,0 +1 @@
1
+ {"title" : "Five", "tags" : ["javascript", "java"], "published_on" : "2011-01-04"}
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ActiveModelSearchableIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ def setup
9
+ super
10
+ SupermodelArticle.delete_all
11
+ @model = SupermodelArticle.new :title => 'Test'
12
+ end
13
+
14
+ def teardown
15
+ super
16
+ SupermodelArticle.delete_all
17
+ end
18
+
19
+ context "ActiveModel" do
20
+
21
+ setup do
22
+ Tire.index('supermodel_articles').delete
23
+ load File.expand_path('../../models/supermodel_article.rb', __FILE__)
24
+ end
25
+ teardown { Tire.index('supermodel_articles').delete }
26
+
27
+ should "configure mapping" do
28
+ assert_equal 'czech', SupermodelArticle.mapping[:title][:analyzer]
29
+ assert_equal 15, SupermodelArticle.mapping[:title][:boost]
30
+
31
+ assert_equal 'czech', SupermodelArticle.index.mapping['supermodel_article']['properties']['title']['analyzer']
32
+ end
33
+
34
+ should "save document into index on save and find it with score" do
35
+ a = SupermodelArticle.new :title => 'Test'
36
+ a.save
37
+ id = a.id
38
+
39
+ a.index.refresh
40
+ sleep(1.5)
41
+
42
+ results = SupermodelArticle.search 'test'
43
+
44
+ assert_equal 1, results.count
45
+ assert_instance_of SupermodelArticle, results.first
46
+ assert_equal 'Test', results.first.title
47
+ assert_not_nil results.first.score
48
+ assert_equal id, results.first.id
49
+ end
50
+
51
+ should "remove document from index on destroy" do
52
+ a = SupermodelArticle.new :title => 'Test'
53
+ a.save
54
+ a.destroy
55
+
56
+ a.index.refresh
57
+ sleep(1.25)
58
+
59
+ results = SupermodelArticle.search 'test'
60
+
61
+ assert_equal 0, results.count
62
+ end
63
+
64
+ should "retrieve sorted documents by IDs returned from search" do
65
+ SupermodelArticle.create! :title => 'foo'
66
+ SupermodelArticle.create! :title => 'bar'
67
+
68
+ SupermodelArticle.index.refresh
69
+ results = SupermodelArticle.search 'foo OR bar^100'
70
+
71
+ assert_equal 2, results.count
72
+
73
+ assert_equal 'bar', results.first.title
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,193 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ActiveRecordSearchableIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ def setup
9
+ super
10
+ File.delete fixtures_path.join('articles.db') rescue nil
11
+
12
+ ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => fixtures_path.join('articles.db') )
13
+
14
+ ActiveRecord::Migration.verbose = false
15
+ ActiveRecord::Schema.define(:version => 1) do
16
+ create_table :active_record_articles do |t|
17
+ t.string :title
18
+ t.datetime :created_at, :default => 'NOW()'
19
+ end
20
+ end
21
+ end
22
+
23
+ def teardown
24
+ super
25
+ File.delete fixtures_path.join('articles.db') rescue nil
26
+ end
27
+
28
+ context "ActiveRecord integration" do
29
+
30
+ setup do
31
+ Tire.index('active_record_articles').delete
32
+ load File.expand_path('../../models/active_record_article.rb', __FILE__)
33
+ end
34
+ teardown { Tire.index('active_record_articles').delete }
35
+
36
+ should "configure mapping" do
37
+ assert_equal 'snowball', ActiveRecordArticle.mapping[:title][:analyzer]
38
+ assert_equal 10, ActiveRecordArticle.mapping[:title][:boost]
39
+
40
+ assert_equal 'snowball', ActiveRecordArticle.index.mapping['active_record_article']['properties']['title']['analyzer']
41
+ end
42
+
43
+ should "save document into index on save and find it" do
44
+ a = ActiveRecordArticle.new :title => 'Test'
45
+ a.save!
46
+ id = a.id
47
+
48
+ a.index.refresh
49
+ sleep(1.5) # Leave ES some breathing room here...
50
+
51
+ results = ActiveRecordArticle.search 'test'
52
+
53
+ assert_equal 1, results.count
54
+
55
+ assert_instance_of ActiveRecordArticle, results.first
56
+ assert_not_nil results.first.id
57
+ assert_equal id, results.first.id
58
+ assert results.first.persisted?, "Record should be persisted"
59
+ assert_not_nil results.first._score
60
+ assert_equal 'Test', results.first.title
61
+ end
62
+
63
+ should "remove document from index on destroy" do
64
+ a = ActiveRecordArticle.new :title => 'Test'
65
+ a.save!
66
+ a.destroy
67
+
68
+ a.index.refresh
69
+ results = ActiveRecordArticle.search 'test'
70
+
71
+ assert_equal 0, results.count
72
+ end
73
+
74
+ should "return documents with scores" do
75
+ ActiveRecordArticle.create! :title => 'foo'
76
+ ActiveRecordArticle.create! :title => 'bar'
77
+
78
+ ActiveRecordArticle.index.refresh
79
+ results = ActiveRecordArticle.search 'foo OR bar^100'
80
+ assert_equal 2, results.count
81
+
82
+ assert_equal 'bar', results.first.title
83
+ end
84
+
85
+ context "with pagination" do
86
+ setup do
87
+ 1.upto(9) { |number| ActiveRecordArticle.create :title => "Test#{number}" }
88
+ ActiveRecordArticle.index.refresh
89
+ end
90
+
91
+ context "and parameter searches" do
92
+
93
+ should "find first page with five results" do
94
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 1
95
+ assert_equal 5, results.size
96
+
97
+ assert_equal 2, results.total_pages
98
+ assert_equal 1, results.current_page
99
+ assert_equal 0, results.previous_page
100
+ assert_equal 2, results.next_page
101
+
102
+ assert_equal 'Test1', results.first.title
103
+ end
104
+
105
+ should "find next page with five results" do
106
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 2
107
+ assert_equal 4, results.size
108
+
109
+ assert_equal 2, results.total_pages
110
+ assert_equal 2, results.current_page
111
+ assert_equal 1, results.previous_page
112
+ assert_equal 3, results.next_page
113
+
114
+ assert_equal 'Test6', results.first.title
115
+ end
116
+
117
+ should "find not find missing page" do
118
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 3
119
+ assert_equal 0, results.size
120
+
121
+ assert_equal 2, results.total_pages
122
+ assert_equal 3, results.current_page
123
+ assert_equal 2, results.previous_page
124
+ assert_equal 4, results.next_page
125
+
126
+ assert_nil results.first
127
+ end
128
+
129
+ end
130
+
131
+ context "and block searches" do
132
+ setup { @q = 'test*' }
133
+
134
+ should "find first page with five results" do
135
+ results = ActiveRecordArticle.search nil, :per_page => 5, :page => 1 do |search|
136
+ search.query { |query| query.string @q }
137
+ search.sort { title }
138
+ search.from 0
139
+ search.size 5
140
+ end
141
+ assert_equal 5, results.size
142
+
143
+ assert_equal 2, results.total_pages
144
+ assert_equal 1, results.current_page
145
+ assert_equal 0, results.previous_page
146
+ assert_equal 2, results.next_page
147
+
148
+ assert_equal 'Test1', results.first.title
149
+ end
150
+
151
+ should "find next page with five results" do
152
+ results = ActiveRecordArticle.search nil, :per_page => 5, :page => 2 do |search|
153
+ search.query { |query| query.string @q }
154
+ search.sort { title }
155
+ search.from 5
156
+ search.size 5
157
+ end
158
+ assert_equal 4, results.size
159
+
160
+ assert_equal 2, results.total_pages
161
+ assert_equal 2, results.current_page
162
+ assert_equal 1, results.previous_page
163
+ assert_equal 3, results.next_page
164
+
165
+ assert_equal 'Test6', results.first.title
166
+ end
167
+
168
+ should "find not find missing page" do
169
+ results = ActiveRecordArticle.search nil, :per_page => 5, :page => 3 do |search|
170
+ search.query { |query| query.string @q }
171
+ search.sort { title }
172
+ search.from 10
173
+ search.size 5
174
+ end
175
+ assert_equal 0, results.size
176
+
177
+ assert_equal 2, results.total_pages
178
+ assert_equal 3, results.current_page
179
+ assert_equal 2, results.previous_page
180
+ assert_equal 4, results.next_page
181
+
182
+ assert_nil results.first
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+
189
+ end
190
+
191
+ end
192
+
193
+ end
@@ -0,0 +1,65 @@
1
+ require 'test_helper'
2
+ require 'date'
3
+
4
+ module Tire
5
+
6
+ class FacetsIntegrationTest < Test::Unit::TestCase
7
+ include Test::Integration
8
+
9
+ context "Facets" do
10
+
11
+ should "return results scoped to current query" do
12
+ q = 'tags:ruby'
13
+ s = Tire.search('articles-test') do
14
+ query { string q }
15
+ facet 'tags' do
16
+ terms :tags
17
+ end
18
+ end
19
+ facets = s.results.facets['tags']['terms']
20
+ assert_equal 2, facets.count
21
+ assert_equal 'ruby', facets.first['term']
22
+ assert_equal 2, facets.first['count']
23
+ end
24
+
25
+ should "allow to specify global facets and query-scoped facets" do
26
+ q = 'tags:ruby'
27
+ s = Tire.search('articles-test') do
28
+ query { string q }
29
+ facet 'scoped-tags' do
30
+ terms :tags
31
+ end
32
+ facet 'global-tags', :global => true do
33
+ terms :tags
34
+ end
35
+ end
36
+
37
+ scoped_facets = s.results.facets['scoped-tags']['terms']
38
+ global_facets = s.results.facets['global-tags']['terms']
39
+
40
+ assert_equal 2, scoped_facets.count
41
+ assert_equal 5, global_facets.count
42
+ end
43
+
44
+ context "date histogram" do
45
+
46
+ should "return aggregated values for all results" do
47
+ s = Tire.search('articles-test') do
48
+ query { all }
49
+ facet 'published_on' do
50
+ date :published_on
51
+ end
52
+ end
53
+
54
+ facets = s.results.facets['published_on']['entries']
55
+ assert_equal 4, facets.size, facets.inspect
56
+ assert_equal 2, facets.entries[1]["count"], facets.inspect
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,46 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class FiltersIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Filters" do
9
+
10
+ should "filter the results" do
11
+ s = Tire.search('articles-test') do
12
+ query { string 'title:T*' }
13
+ filter :terms, :tags => ['ruby']
14
+ end
15
+
16
+ assert_equal 1, s.results.count
17
+ assert_equal 'Two', s.results.first.title
18
+ end
19
+
20
+ should "filter the results with multiple filters" do
21
+ s = Tire.search('articles-test') do
22
+ query { string 'title:F*' }
23
+ filter :or, {:terms => {:tags => ['ruby']}},
24
+ {:terms => {:tags => ['erlang']}}
25
+ end
26
+
27
+ assert_equal 1, s.results.count
28
+ assert_equal 'Four', s.results.first.title
29
+ end
30
+
31
+ should "not influence facets" do
32
+ s = Tire.search('articles-test') do
33
+ query { string 'title:T*' }
34
+ filter :terms, :tags => ['ruby']
35
+ facet('tags') { terms :tags }
36
+ end
37
+
38
+ assert_equal 1, s.results.count
39
+ assert_equal 3, s.results.facets['tags'].size
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class HighlightIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Highlight" do
9
+ teardown { Tire.index('highlight-test').delete }
10
+
11
+ should "add 'highlight' field to the result item" do
12
+ # Tire::Configuration.logger STDERR, :level => 'debug'
13
+ s = Tire.search('articles-test') do
14
+ query { string 'Two' }
15
+ highlight :title
16
+ end
17
+
18
+ doc = s.results.first
19
+
20
+ assert_equal 1, doc.highlight.title.size
21
+ assert doc.highlight.title.to_s.include?('<em>'), "Highlight does not include default highlight tag"
22
+ end
23
+
24
+ should "return entire content with highlighted fragments" do
25
+ # Tire::Configuration.logger STDERR, :level => 'debug'
26
+
27
+ content = "A Fox one day fell into a deep well and could find no means of escape. A Goat, overcome with thirst, came to the same well, and seeing the Fox, inquired if the water was good. Concealing his sad plight under a merry guise, the Fox indulged in a lavish praise of the water, saying it was excellent beyond measure, and encouraging him to descend. The Goat, mindful only of his thirst, thoughtlessly jumped down, but just as he drank, the Fox informed him of the difficulty they were both in and suggested a scheme for their common escape. \"If,\" said he, \"you will place your forefeet upon the wall and bend your head, I will run up your back and escape, and will help you out afterwards.\" The Goat readily assented and the Fox leaped upon his back. Steadying himself with the Goat horns, he safely reached the mouth of the well and made off as fast as he could. When the Goat upbraided him for breaking his promise, he turned around and cried out, \"You foolish old fellow! If you had as many brains in your head as you have hairs in your beard, you would never have gone down before you had inspected the way up, nor have exposed yourself to dangers from which you had no means of escape.\" Look before you leap."
28
+
29
+ Tire.index 'highlight-test' do
30
+ delete
31
+ create
32
+ store :id => 1, :content => content
33
+ refresh
34
+ end
35
+ sleep(0.5)
36
+
37
+ s = Tire.search('highlight-test') do
38
+ query { string 'fox' }
39
+ highlight :content => { :number_of_fragments => 0 }
40
+ end
41
+
42
+ doc = s.results.first
43
+ assert_not_nil doc.highlight.content
44
+
45
+ highlight = doc.highlight.content
46
+ assert highlight.to_s.include?('<em>'), "Highlight does not include default highlight tag"
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end