tire 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +435 -0
- data/Rakefile +75 -0
- data/examples/dsl.rb +73 -0
- data/examples/rails-application-template.rb +144 -0
- data/examples/tire-dsl.rb +617 -0
- data/lib/tire.rb +35 -0
- data/lib/tire/client.rb +40 -0
- data/lib/tire/configuration.rb +29 -0
- data/lib/tire/dsl.rb +33 -0
- data/lib/tire/index.rb +209 -0
- data/lib/tire/logger.rb +60 -0
- data/lib/tire/model/callbacks.rb +23 -0
- data/lib/tire/model/import.rb +18 -0
- data/lib/tire/model/indexing.rb +50 -0
- data/lib/tire/model/naming.rb +30 -0
- data/lib/tire/model/persistence.rb +34 -0
- data/lib/tire/model/persistence/attributes.rb +60 -0
- data/lib/tire/model/persistence/finders.rb +61 -0
- data/lib/tire/model/persistence/storage.rb +75 -0
- data/lib/tire/model/search.rb +97 -0
- data/lib/tire/results/collection.rb +56 -0
- data/lib/tire/results/item.rb +39 -0
- data/lib/tire/results/pagination.rb +30 -0
- data/lib/tire/rubyext/hash.rb +3 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +117 -0
- data/lib/tire/search/facet.rb +41 -0
- data/lib/tire/search/filter.rb +28 -0
- data/lib/tire/search/highlight.rb +37 -0
- data/lib/tire/search/query.rb +42 -0
- data/lib/tire/search/sort.rb +29 -0
- data/lib/tire/tasks.rb +88 -0
- data/lib/tire/version.rb +3 -0
- data/test/fixtures/articles/1.json +1 -0
- data/test/fixtures/articles/2.json +1 -0
- data/test/fixtures/articles/3.json +1 -0
- data/test/fixtures/articles/4.json +1 -0
- data/test/fixtures/articles/5.json +1 -0
- data/test/integration/active_model_searchable_test.rb +80 -0
- data/test/integration/active_record_searchable_test.rb +193 -0
- data/test/integration/facets_test.rb +65 -0
- data/test/integration/filters_test.rb +46 -0
- data/test/integration/highlight_test.rb +52 -0
- data/test/integration/index_mapping_test.rb +44 -0
- data/test/integration/index_store_test.rb +68 -0
- data/test/integration/persistent_model_test.rb +35 -0
- data/test/integration/query_string_test.rb +43 -0
- data/test/integration/results_test.rb +28 -0
- data/test/integration/sort_test.rb +36 -0
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_index_name.rb +5 -0
- data/test/models/active_record_article.rb +12 -0
- data/test/models/article.rb +15 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +22 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +52 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/client_test.rb +43 -0
- data/test/unit/configuration_test.rb +71 -0
- data/test/unit/index_test.rb +390 -0
- data/test/unit/logger_test.rb +114 -0
- data/test/unit/model_callbacks_test.rb +90 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +400 -0
- data/test/unit/model_search_test.rb +289 -0
- data/test/unit/results_collection_test.rb +131 -0
- data/test/unit/results_item_test.rb +59 -0
- data/test/unit/rubyext_hash_test.rb +19 -0
- data/test/unit/search_facet_test.rb +69 -0
- data/test/unit/search_filter_test.rb +36 -0
- data/test/unit/search_highlight_test.rb +46 -0
- data/test/unit/search_query_test.rb +55 -0
- data/test/unit/search_sort_test.rb +50 -0
- data/test/unit/search_test.rb +204 -0
- data/test/unit/tire_test.rb +55 -0
- data/tire.gemspec +54 -0
- metadata +372 -0
data/lib/tire/version.rb
ADDED
@@ -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
|