load_balanced_tire 0.1
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.
- data/.gitignore +14 -0
- data/.travis.yml +29 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +760 -0
- data/Rakefile +78 -0
- data/examples/rails-application-template.rb +249 -0
- data/examples/tire-dsl.rb +876 -0
- data/lib/tire.rb +55 -0
- data/lib/tire/alias.rb +296 -0
- data/lib/tire/configuration.rb +30 -0
- data/lib/tire/dsl.rb +43 -0
- data/lib/tire/http/client.rb +62 -0
- data/lib/tire/http/clients/curb.rb +61 -0
- data/lib/tire/http/clients/faraday.rb +71 -0
- data/lib/tire/http/response.rb +27 -0
- data/lib/tire/index.rb +361 -0
- data/lib/tire/logger.rb +60 -0
- data/lib/tire/model/callbacks.rb +40 -0
- data/lib/tire/model/import.rb +26 -0
- data/lib/tire/model/indexing.rb +128 -0
- data/lib/tire/model/naming.rb +100 -0
- data/lib/tire/model/percolate.rb +99 -0
- data/lib/tire/model/persistence.rb +71 -0
- data/lib/tire/model/persistence/attributes.rb +143 -0
- data/lib/tire/model/persistence/finders.rb +66 -0
- data/lib/tire/model/persistence/storage.rb +69 -0
- data/lib/tire/model/search.rb +307 -0
- data/lib/tire/results/collection.rb +114 -0
- data/lib/tire/results/item.rb +86 -0
- data/lib/tire/results/pagination.rb +54 -0
- data/lib/tire/rubyext/hash.rb +8 -0
- data/lib/tire/rubyext/ruby_1_8.rb +7 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +188 -0
- data/lib/tire/search/facet.rb +74 -0
- data/lib/tire/search/filter.rb +28 -0
- data/lib/tire/search/highlight.rb +37 -0
- data/lib/tire/search/query.rb +186 -0
- data/lib/tire/search/scan.rb +114 -0
- data/lib/tire/search/script_field.rb +23 -0
- data/lib/tire/search/sort.rb +25 -0
- data/lib/tire/tasks.rb +135 -0
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +22 -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_indexing_test.rb +51 -0
- data/test/integration/active_model_searchable_test.rb +114 -0
- data/test/integration/active_record_searchable_test.rb +446 -0
- data/test/integration/boolean_queries_test.rb +43 -0
- data/test/integration/count_test.rb +34 -0
- data/test/integration/custom_score_queries_test.rb +88 -0
- data/test/integration/dis_max_queries_test.rb +68 -0
- data/test/integration/dsl_search_test.rb +22 -0
- data/test/integration/explanation_test.rb +44 -0
- data/test/integration/facets_test.rb +259 -0
- data/test/integration/filtered_queries_test.rb +66 -0
- data/test/integration/filters_test.rb +63 -0
- data/test/integration/fuzzy_queries_test.rb +20 -0
- data/test/integration/highlight_test.rb +64 -0
- data/test/integration/index_aliases_test.rb +122 -0
- data/test/integration/index_mapping_test.rb +43 -0
- data/test/integration/index_store_test.rb +96 -0
- data/test/integration/index_update_document_test.rb +111 -0
- data/test/integration/mongoid_searchable_test.rb +309 -0
- data/test/integration/percolator_test.rb +111 -0
- data/test/integration/persistent_model_test.rb +130 -0
- data/test/integration/prefix_query_test.rb +43 -0
- data/test/integration/query_return_version_test.rb +70 -0
- data/test/integration/query_string_test.rb +52 -0
- data/test/integration/range_queries_test.rb +36 -0
- data/test/integration/reindex_test.rb +46 -0
- data/test/integration/results_test.rb +39 -0
- data/test/integration/scan_test.rb +56 -0
- data/test/integration/script_fields_test.rb +38 -0
- data/test/integration/sort_test.rb +36 -0
- data/test/integration/text_query_test.rb +39 -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_document_type.rb +7 -0
- data/test/models/active_model_article_with_custom_index_name.rb +7 -0
- data/test/models/active_record_models.rb +122 -0
- data/test/models/article.rb +15 -0
- data/test/models/mongoid_models.rb +97 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +17 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +93 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/configuration_test.rb +74 -0
- data/test/unit/http_client_test.rb +76 -0
- data/test/unit/http_response_test.rb +49 -0
- data/test/unit/index_alias_test.rb +275 -0
- data/test/unit/index_test.rb +894 -0
- data/test/unit/logger_test.rb +125 -0
- data/test/unit/model_callbacks_test.rb +116 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +528 -0
- data/test/unit/model_search_test.rb +913 -0
- data/test/unit/results_collection_test.rb +281 -0
- data/test/unit/results_item_test.rb +162 -0
- data/test/unit/rubyext_test.rb +66 -0
- data/test/unit/search_facet_test.rb +153 -0
- data/test/unit/search_filter_test.rb +42 -0
- data/test/unit/search_highlight_test.rb +46 -0
- data/test/unit/search_query_test.rb +301 -0
- data/test/unit/search_scan_test.rb +113 -0
- data/test/unit/search_script_field_test.rb +26 -0
- data/test/unit/search_sort_test.rb +50 -0
- data/test/unit/search_test.rb +499 -0
- data/test/unit/tire_test.rb +126 -0
- data/tire.gemspec +90 -0
- metadata +549 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Tire
|
2
|
+
module Search
|
3
|
+
|
4
|
+
class Sort
|
5
|
+
def initialize(&block)
|
6
|
+
@value = []
|
7
|
+
block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
def by(name, direction=nil)
|
11
|
+
@value << ( direction ? { name => direction } : name )
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_ary
|
16
|
+
@value
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_json
|
20
|
+
@value.to_json
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/tire/tasks.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
namespace :tire do
|
5
|
+
|
6
|
+
full_comment = <<-DESC.gsub(/ /, '')
|
7
|
+
Import data from your model using paginate: rake environment tire:import CLASS='MyModel'.
|
8
|
+
|
9
|
+
Pass params for the `paginate` method:
|
10
|
+
$ rake environment tire:import CLASS='Article' PARAMS='{:page => 1}'
|
11
|
+
|
12
|
+
Force rebuilding the index (delete and create):
|
13
|
+
$ rake environment tire:import CLASS='Article' PARAMS='{:page => 1}' FORCE=1
|
14
|
+
|
15
|
+
Set target index name:
|
16
|
+
$ rake environment tire:import CLASS='Article' INDEX='articles-new'
|
17
|
+
DESC
|
18
|
+
desc full_comment
|
19
|
+
task :import do |t|
|
20
|
+
|
21
|
+
def elapsed_to_human(elapsed)
|
22
|
+
hour = 60*60
|
23
|
+
day = hour*24
|
24
|
+
|
25
|
+
case elapsed
|
26
|
+
when 0..59
|
27
|
+
"#{sprintf("%1.5f", elapsed)} seconds"
|
28
|
+
when 60..hour-1
|
29
|
+
"#{elapsed/60} minutes and #{elapsed % 60} seconds"
|
30
|
+
when hour..day
|
31
|
+
"#{elapsed/hour} hours and #{elapsed % hour} minutes"
|
32
|
+
else
|
33
|
+
"#{elapsed/hour} hours"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if ENV['CLASS'].to_s == ''
|
38
|
+
puts '='*90, 'USAGE', '='*90, full_comment, ""
|
39
|
+
exit(1)
|
40
|
+
end
|
41
|
+
|
42
|
+
klass = eval(ENV['CLASS'].to_s)
|
43
|
+
params = eval(ENV['PARAMS'].to_s) || {}
|
44
|
+
|
45
|
+
params.update :method => 'paginate'
|
46
|
+
|
47
|
+
index = Tire::Index.new( ENV['INDEX'] || klass.tire.index.name )
|
48
|
+
|
49
|
+
if ENV['FORCE']
|
50
|
+
puts "[IMPORT] Deleting index '#{index.name}'"
|
51
|
+
index.delete
|
52
|
+
end
|
53
|
+
|
54
|
+
unless index.exists?
|
55
|
+
mapping = defined?(Yajl) ? Yajl::Encoder.encode(klass.tire.mapping_to_hash, :pretty => true) :
|
56
|
+
MultiJson.encode(klass.tire.mapping_to_hash)
|
57
|
+
puts "[IMPORT] Creating index '#{index.name}' with mapping:", mapping
|
58
|
+
index.create :mappings => klass.tire.mapping_to_hash, :settings => klass.tire.settings
|
59
|
+
end
|
60
|
+
|
61
|
+
STDOUT.sync = true
|
62
|
+
puts "[IMPORT] Starting import for the '#{ENV['CLASS']}' class"
|
63
|
+
tty_cols = 80
|
64
|
+
total = klass.all.count rescue nil
|
65
|
+
offset = (total.to_s.size*2)+8
|
66
|
+
done = 0
|
67
|
+
|
68
|
+
STDOUT.puts '-'*tty_cols
|
69
|
+
elapsed = Benchmark.realtime do
|
70
|
+
|
71
|
+
# Add Kaminari-powered "paginate" method
|
72
|
+
#
|
73
|
+
if defined?(Kaminari) && klass.respond_to?(:page)
|
74
|
+
klass.instance_eval do
|
75
|
+
def paginate(options = {})
|
76
|
+
page(options[:page]).per(options[:per_page]).to_a
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end unless klass.respond_to?(:paginate)
|
80
|
+
|
81
|
+
# Import the documents
|
82
|
+
#
|
83
|
+
index.import(klass, params) do |documents|
|
84
|
+
|
85
|
+
if total
|
86
|
+
done += documents.to_a.size
|
87
|
+
# I CAN HAZ PROGREZ BAR LIEK HOMEBRU!
|
88
|
+
percent = ( (done.to_f / total) * 100 ).to_i
|
89
|
+
glyphs = ( percent * ( (tty_cols-offset).to_f/100 ) ).to_i
|
90
|
+
STDOUT.print( "#" * glyphs )
|
91
|
+
STDOUT.print( "\r"*tty_cols+"#{done}/#{total} | \e[1m#{percent}%\e[0m " )
|
92
|
+
end
|
93
|
+
|
94
|
+
# Don't forget to return the documents collection back!
|
95
|
+
documents
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
puts "", '='*80, "Import finished in #{elapsed_to_human(elapsed)}"
|
100
|
+
end
|
101
|
+
|
102
|
+
namespace :index do
|
103
|
+
|
104
|
+
full_comment = <<-DESC.gsub(/ /, '')
|
105
|
+
Delete indices passed in the INDEX environment variable; separate multiple indices by comma.
|
106
|
+
|
107
|
+
Pass name of a single index to drop in the INDEX environmnet variable:
|
108
|
+
$ rake environment tire:index:drop INDEX=articles
|
109
|
+
|
110
|
+
Pass names of multiple indices to drop in the INDEX or INDICES environmnet variable:
|
111
|
+
$ rake environment tire:index:drop INDICES=articles-2011-01,articles-2011-02
|
112
|
+
|
113
|
+
DESC
|
114
|
+
desc full_comment
|
115
|
+
task :drop do
|
116
|
+
index_names = (ENV['INDEX'] || ENV['INDICES']).to_s.split(/,\s*/)
|
117
|
+
|
118
|
+
if index_names.empty?
|
119
|
+
puts '='*90, 'USAGE', '='*90, full_comment, ""
|
120
|
+
exit(1)
|
121
|
+
end
|
122
|
+
|
123
|
+
index_names.each do |name|
|
124
|
+
index = Tire::Index.new(name)
|
125
|
+
print "* Deleting index \e[1m#{index.name}\e[0m... "
|
126
|
+
puts index.delete ? "\e[32mOK\e[0m" : "\e[31mFAILED\e[0m | #{index.response.body}"
|
127
|
+
end
|
128
|
+
|
129
|
+
puts ""
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
data/lib/tire/utils.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Tire
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
def escape(s)
|
7
|
+
URI.encode_www_form_component(s.to_s)
|
8
|
+
end
|
9
|
+
|
10
|
+
def unescape(s)
|
11
|
+
s = s.to_s.respond_to?(:force_encoding) ? s.to_s.force_encoding(Encoding::UTF_8) : s.to_s
|
12
|
+
URI.decode_www_form_component(s)
|
13
|
+
end
|
14
|
+
|
15
|
+
extend self
|
16
|
+
end
|
17
|
+
end
|
data/lib/tire/version.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Tire
|
2
|
+
VERSION = "0.4.2"
|
3
|
+
|
4
|
+
CHANGELOG =<<-END
|
5
|
+
IMPORTANT CHANGES LATELY:
|
6
|
+
|
7
|
+
Version 0.4.1
|
8
|
+
-------------
|
9
|
+
* Added a Index#settings method to retrieve index settings as a Hash
|
10
|
+
* Added support for the "scan" search in the Ruby API
|
11
|
+
* Added support for reindexing the index documents into new index
|
12
|
+
* Added basic support for index aliases
|
13
|
+
* Changed, that Index#bulk_store runs against an index endpoint, not against `/_bulk`
|
14
|
+
* Refactorings, fixes, Ruby 1.8 compatibility
|
15
|
+
|
16
|
+
Version 0.4.2
|
17
|
+
-------------
|
18
|
+
* Fixed incorrect handling of PUT requests in the Curb client
|
19
|
+
* Fixed, that blocks passed to `Tire::Index.new` or `Tire.index` losed the scope
|
20
|
+
* Added `Tire::Alias`, interface and DSL to manage aliases as resources
|
21
|
+
END
|
22
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
{"title" : "One", "tags" : ["ruby"], "published_on" : "2011-01-01", "words" : 125, "draft" : true}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"title" : "Two", "tags" : ["ruby", "python"], "published_on" : "2011-01-02", "words" : 250}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"title" : "Three", "tags" : ["java"], "published_on" : "2011-01-02", "words" : 375}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"title" : "Four", "tags" : ["erlang"], "published_on" : "2011-01-03", "words" : 250}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"title" : "Five", "tags" : ["javascript", "java"], "published_on" : "2011-01-04", "words" : 125}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require File.expand_path('../../models/supermodel_article', __FILE__)
|
3
|
+
|
4
|
+
module Tire
|
5
|
+
|
6
|
+
class ActiveModelSearchableIntegrationTest < Test::Unit::TestCase
|
7
|
+
include Test::Integration
|
8
|
+
|
9
|
+
class ::ActiveModelArticleWithCustomAsSerialization < ActiveModelArticleWithCallbacks
|
10
|
+
mapping do
|
11
|
+
indexes :title
|
12
|
+
indexes :content
|
13
|
+
indexes :characters, :as => 'content.length'
|
14
|
+
indexes :readability, :as => proc {
|
15
|
+
content.split(/\W/).reject { |t| t.blank? }.size /
|
16
|
+
content.split(/\./).size
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
super
|
23
|
+
ActiveModelArticleWithCustomAsSerialization.index.delete
|
24
|
+
end
|
25
|
+
|
26
|
+
def teardown
|
27
|
+
super
|
28
|
+
ActiveModelArticleWithCustomAsSerialization.index.delete
|
29
|
+
end
|
30
|
+
|
31
|
+
context "ActiveModel serialization" do
|
32
|
+
|
33
|
+
setup do
|
34
|
+
@model = ActiveModelArticleWithCustomAsSerialization.new \
|
35
|
+
:id => 1,
|
36
|
+
:title => 'Test article',
|
37
|
+
:content => 'Lorem Ipsum. Dolor Sit Amet.'
|
38
|
+
@model.update_index
|
39
|
+
@model.index.refresh
|
40
|
+
end
|
41
|
+
|
42
|
+
should "serialize the content length" do
|
43
|
+
m = ActiveModelArticleWithCustomAsSerialization.search('*').first
|
44
|
+
assert_equal 28, m.characters
|
45
|
+
assert_equal 2, m.readability
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require File.expand_path('../../models/supermodel_article', __FILE__)
|
3
|
+
|
4
|
+
module Tire
|
5
|
+
|
6
|
+
class ActiveModelSearchableIntegrationTest < Test::Unit::TestCase
|
7
|
+
include Test::Integration
|
8
|
+
|
9
|
+
def setup
|
10
|
+
super
|
11
|
+
Redis::Persistence.config.redis = Redis.new db: ENV['REDIS_PERSISTENCE_TEST_DATABASE'] || 14
|
12
|
+
Redis::Persistence.config.redis.flushdb
|
13
|
+
@model = SupermodelArticle.new :title => 'Test'
|
14
|
+
end
|
15
|
+
|
16
|
+
def teardown
|
17
|
+
super
|
18
|
+
SupermodelArticle.all.each { |a| a.destroy }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "ActiveModel integration" do
|
22
|
+
|
23
|
+
setup do
|
24
|
+
Tire.index('supermodel_articles').delete
|
25
|
+
load File.expand_path('../../models/supermodel_article.rb', __FILE__)
|
26
|
+
end
|
27
|
+
teardown { Tire.index('supermodel_articles').delete }
|
28
|
+
|
29
|
+
should "configure mapping" do
|
30
|
+
assert_equal 'czech', SupermodelArticle.mapping[:title][:analyzer]
|
31
|
+
assert_equal 15, SupermodelArticle.mapping[:title][:boost]
|
32
|
+
|
33
|
+
assert_equal 'czech', SupermodelArticle.index.mapping['supermodel_article']['properties']['title']['analyzer']
|
34
|
+
end
|
35
|
+
|
36
|
+
should "save document into index on save and find it with score" do
|
37
|
+
a = SupermodelArticle.new :title => 'Test'
|
38
|
+
a.save
|
39
|
+
id = a.id
|
40
|
+
|
41
|
+
# Store document of another type in the index
|
42
|
+
Index.new 'supermodel_articles' do
|
43
|
+
store :type => 'other-thing', :title => 'Title for other thing'
|
44
|
+
end
|
45
|
+
|
46
|
+
a.index.refresh
|
47
|
+
|
48
|
+
# The index should contain 2 documents
|
49
|
+
assert_equal 2, Tire.search('supermodel_articles') { query { all } }.results.size
|
50
|
+
|
51
|
+
results = SupermodelArticle.search 'test'
|
52
|
+
|
53
|
+
# The model should find only 1 document
|
54
|
+
assert_equal 1, results.count
|
55
|
+
|
56
|
+
assert_instance_of Results::Item, results.first
|
57
|
+
assert_equal 'Test', results.first.title
|
58
|
+
assert_not_nil results.first._score
|
59
|
+
assert_equal id.to_s, results.first.id.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
should "remove document from index on destroy" do
|
63
|
+
a = SupermodelArticle.new :title => 'Test'
|
64
|
+
a.save
|
65
|
+
assert_equal 1, SupermodelArticle.all.size
|
66
|
+
|
67
|
+
a.destroy
|
68
|
+
assert_equal 0, SupermodelArticle.all.size
|
69
|
+
|
70
|
+
a.index.refresh
|
71
|
+
results = SupermodelArticle.search 'test'
|
72
|
+
|
73
|
+
assert_equal 0, results.count
|
74
|
+
end
|
75
|
+
|
76
|
+
should "retrieve sorted documents by IDs returned from search" do
|
77
|
+
SupermodelArticle.create :title => 'foo'
|
78
|
+
SupermodelArticle.create :id => 'abc123', :title => 'bar'
|
79
|
+
|
80
|
+
SupermodelArticle.index.refresh
|
81
|
+
results = SupermodelArticle.search 'foo OR bar^100'
|
82
|
+
|
83
|
+
assert_equal 2, results.count
|
84
|
+
|
85
|
+
assert_equal 'bar', results.first.title
|
86
|
+
assert_equal 'abc123', results.first.id
|
87
|
+
end
|
88
|
+
|
89
|
+
context "within Rails" do
|
90
|
+
|
91
|
+
setup do
|
92
|
+
module ::Rails; end
|
93
|
+
end
|
94
|
+
|
95
|
+
should "load the underlying model" do
|
96
|
+
a = SupermodelArticle.new :title => 'Test'
|
97
|
+
a.save
|
98
|
+
a.index.refresh
|
99
|
+
|
100
|
+
results = SupermodelArticle.search 'test'
|
101
|
+
|
102
|
+
assert_instance_of Results::Item, results.first
|
103
|
+
assert_instance_of SupermodelArticle, results.first.load
|
104
|
+
|
105
|
+
assert_equal 'Test', results.first.load.title
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,446 @@
|
|
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
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" )
|
11
|
+
|
12
|
+
ActiveRecord::Migration.verbose = false
|
13
|
+
ActiveRecord::Schema.define(:version => 1) do
|
14
|
+
create_table :active_record_articles do |t|
|
15
|
+
t.string :title
|
16
|
+
t.datetime :created_at, :default => 'NOW()'
|
17
|
+
end
|
18
|
+
create_table :active_record_comments do |t|
|
19
|
+
t.string :author
|
20
|
+
t.text :body
|
21
|
+
t.references :article
|
22
|
+
t.timestamps
|
23
|
+
end
|
24
|
+
create_table :active_record_stats do |t|
|
25
|
+
t.integer :pageviews
|
26
|
+
t.string :period
|
27
|
+
t.references :article
|
28
|
+
end
|
29
|
+
create_table :active_record_class_with_tire_methods do |t|
|
30
|
+
t.string :title
|
31
|
+
end
|
32
|
+
create_table :active_record_class_with_dynamic_index_names do |t|
|
33
|
+
t.string :title
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "ActiveRecord integration" do
|
39
|
+
|
40
|
+
setup do
|
41
|
+
ActiveRecordArticle.destroy_all
|
42
|
+
Tire.index('active_record_articles').delete
|
43
|
+
|
44
|
+
load File.expand_path('../../models/active_record_models.rb', __FILE__)
|
45
|
+
end
|
46
|
+
|
47
|
+
teardown do
|
48
|
+
ActiveRecordArticle.destroy_all
|
49
|
+
Tire.index('active_record_articles').delete
|
50
|
+
end
|
51
|
+
|
52
|
+
should "configure mapping" do
|
53
|
+
assert_equal 'snowball', ActiveRecordArticle.mapping[:title][:analyzer]
|
54
|
+
assert_equal 10, ActiveRecordArticle.mapping[:title][:boost]
|
55
|
+
|
56
|
+
assert_equal 'snowball', ActiveRecordArticle.index.mapping['active_record_article']['properties']['title']['analyzer']
|
57
|
+
end
|
58
|
+
|
59
|
+
should "save document into index on save and find it" do
|
60
|
+
a = ActiveRecordArticle.new :title => 'Test'
|
61
|
+
a.save!
|
62
|
+
id = a.id
|
63
|
+
|
64
|
+
a.index.refresh
|
65
|
+
|
66
|
+
results = ActiveRecordArticle.search 'test'
|
67
|
+
|
68
|
+
assert results.any?
|
69
|
+
assert_equal 1, results.count
|
70
|
+
|
71
|
+
assert_instance_of Results::Item, results.first
|
72
|
+
assert_not_nil results.first.id
|
73
|
+
assert_equal id.to_s, results.first.id.to_s
|
74
|
+
assert results.first.persisted?, "Record should be persisted"
|
75
|
+
assert_not_nil results.first._score
|
76
|
+
assert_equal 'Test', results.first.title
|
77
|
+
end
|
78
|
+
|
79
|
+
should "raise exception on invalid query" do
|
80
|
+
ActiveRecordArticle.create! :title => 'Test'
|
81
|
+
|
82
|
+
assert_raise Search::SearchRequestFailed do
|
83
|
+
ActiveRecordArticle.search '[x'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with eager loading" do
|
88
|
+
setup do
|
89
|
+
ActiveRecordArticle.destroy_all
|
90
|
+
5.times { |n| ActiveRecordArticle.create! :title => "Test #{n+1}" }
|
91
|
+
ActiveRecordArticle.index.refresh
|
92
|
+
end
|
93
|
+
|
94
|
+
should "load records on query search" do
|
95
|
+
results = ActiveRecordArticle.search '"Test 1"', :load => true
|
96
|
+
|
97
|
+
assert results.any?
|
98
|
+
assert_equal ActiveRecordArticle.find(1), results.first
|
99
|
+
end
|
100
|
+
|
101
|
+
should "load records on block search" do
|
102
|
+
results = ActiveRecordArticle.search :load => true do
|
103
|
+
query { string '"Test 1"' }
|
104
|
+
end
|
105
|
+
|
106
|
+
assert_equal ActiveRecordArticle.find(1), results.first
|
107
|
+
end
|
108
|
+
|
109
|
+
should "load records with options on query search" do
|
110
|
+
assert_equal ActiveRecordArticle.find(['1'], :include => 'comments').first,
|
111
|
+
ActiveRecordArticle.search('"Test 1"',
|
112
|
+
:load => { :include => 'comments' }).results.first
|
113
|
+
end
|
114
|
+
|
115
|
+
should "return empty collection for nonmatching query" do
|
116
|
+
assert_nothing_raised do
|
117
|
+
results = ActiveRecordArticle.search :load => true do
|
118
|
+
query { string '"Hic Sunt Leones"' }
|
119
|
+
end
|
120
|
+
assert_equal 0, results.size
|
121
|
+
assert ! results.any?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
should "remove document from index on destroy" do
|
127
|
+
a = ActiveRecordArticle.new :title => 'Test remove...'
|
128
|
+
a.save!
|
129
|
+
assert_equal 1, ActiveRecordArticle.count
|
130
|
+
|
131
|
+
a.destroy
|
132
|
+
assert_equal 0, ActiveRecordArticle.all.size
|
133
|
+
|
134
|
+
a.index.refresh
|
135
|
+
results = ActiveRecordArticle.search 'test'
|
136
|
+
assert_equal 0, results.count
|
137
|
+
end
|
138
|
+
|
139
|
+
should "return documents with scores" do
|
140
|
+
ActiveRecordArticle.create! :title => 'foo'
|
141
|
+
ActiveRecordArticle.create! :title => 'bar'
|
142
|
+
|
143
|
+
ActiveRecordArticle.index.refresh
|
144
|
+
results = ActiveRecordArticle.search 'foo OR bar^100'
|
145
|
+
assert_equal 2, results.count
|
146
|
+
|
147
|
+
assert_equal 'bar', results.first.title
|
148
|
+
end
|
149
|
+
|
150
|
+
context "with pagination" do
|
151
|
+
setup do
|
152
|
+
1.upto(9) { |number| ActiveRecordArticle.create :title => "Test#{number}" }
|
153
|
+
ActiveRecordArticle.index.refresh
|
154
|
+
end
|
155
|
+
|
156
|
+
context "and parameter searches" do
|
157
|
+
|
158
|
+
should "find first page with five results" do
|
159
|
+
results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 1
|
160
|
+
assert_equal 5, results.size
|
161
|
+
|
162
|
+
# WillPaginate
|
163
|
+
#
|
164
|
+
assert_equal 2, results.total_pages
|
165
|
+
assert_equal 1, results.current_page
|
166
|
+
assert_equal nil, results.previous_page
|
167
|
+
assert_equal 2, results.next_page
|
168
|
+
|
169
|
+
# Kaminari
|
170
|
+
#
|
171
|
+
assert_equal 5, results.limit_value
|
172
|
+
assert_equal 9, results.total_count
|
173
|
+
assert_equal 2, results.num_pages
|
174
|
+
assert_equal 0, results.offset_value
|
175
|
+
|
176
|
+
assert_equal 'Test1', results.first.title
|
177
|
+
end
|
178
|
+
|
179
|
+
should "find next page with five results" do
|
180
|
+
results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 2
|
181
|
+
assert_equal 4, results.size
|
182
|
+
|
183
|
+
assert_equal 2, results.total_pages
|
184
|
+
assert_equal 2, results.current_page
|
185
|
+
assert_equal 1, results.previous_page
|
186
|
+
assert_equal nil, results.next_page
|
187
|
+
|
188
|
+
#kaminari
|
189
|
+
assert_equal 5, results.limit_value
|
190
|
+
assert_equal 9, results.total_count
|
191
|
+
assert_equal 2, results.num_pages
|
192
|
+
assert_equal 5, results.offset_value
|
193
|
+
|
194
|
+
assert_equal 'Test6', results.first.title
|
195
|
+
end
|
196
|
+
|
197
|
+
should "find not find missing page" do
|
198
|
+
results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 3
|
199
|
+
assert_equal 0, results.size
|
200
|
+
|
201
|
+
assert_equal 2, results.total_pages
|
202
|
+
assert_equal 3, results.current_page
|
203
|
+
assert_equal 2, results.previous_page
|
204
|
+
assert_equal nil, results.next_page
|
205
|
+
|
206
|
+
#kaminari
|
207
|
+
assert_equal 5, results.limit_value
|
208
|
+
assert_equal 9, results.total_count
|
209
|
+
assert_equal 2, results.num_pages
|
210
|
+
assert_equal 10, results.offset_value
|
211
|
+
|
212
|
+
assert_nil results.first
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
context "and block searches" do
|
218
|
+
setup { @q = 'test*' }
|
219
|
+
|
220
|
+
should "find first page with five results" do
|
221
|
+
results = ActiveRecordArticle.search do |search|
|
222
|
+
search.query { |query| query.string @q }
|
223
|
+
search.sort { by :title }
|
224
|
+
search.from 0
|
225
|
+
search.size 5
|
226
|
+
end
|
227
|
+
assert_equal 5, results.size
|
228
|
+
|
229
|
+
assert_equal 2, results.total_pages
|
230
|
+
assert_equal 1, results.current_page
|
231
|
+
assert_equal nil, results.previous_page
|
232
|
+
assert_equal 2, results.next_page
|
233
|
+
|
234
|
+
assert_equal 'Test1', results.first.title
|
235
|
+
end
|
236
|
+
|
237
|
+
should "find next page with five results" do
|
238
|
+
results = ActiveRecordArticle.search do |search|
|
239
|
+
search.query { |query| query.string @q }
|
240
|
+
search.sort { by :title }
|
241
|
+
search.from 5
|
242
|
+
search.size 5
|
243
|
+
end
|
244
|
+
assert_equal 4, results.size
|
245
|
+
|
246
|
+
assert_equal 2, results.total_pages
|
247
|
+
assert_equal 2, results.current_page
|
248
|
+
assert_equal 1, results.previous_page
|
249
|
+
assert_equal nil, results.next_page
|
250
|
+
|
251
|
+
assert_equal 'Test6', results.first.title
|
252
|
+
end
|
253
|
+
|
254
|
+
should "not find a missing page" do
|
255
|
+
results = ActiveRecordArticle.search do |search|
|
256
|
+
search.query { |query| query.string @q }
|
257
|
+
search.sort { by :title }
|
258
|
+
search.from 10
|
259
|
+
search.size 5
|
260
|
+
end
|
261
|
+
assert_equal 0, results.size
|
262
|
+
|
263
|
+
assert_equal 2, results.total_pages
|
264
|
+
assert_equal 3, results.current_page
|
265
|
+
assert_equal 2, results.previous_page
|
266
|
+
assert_equal nil, results.next_page
|
267
|
+
|
268
|
+
assert_nil results.first
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
context "with proxy" do
|
276
|
+
|
277
|
+
should "allow access to Tire instance methods" do
|
278
|
+
a = ActiveRecordClassWithTireMethods.create :title => 'One'
|
279
|
+
assert_equal "THIS IS MY INDEX!", a.index
|
280
|
+
assert_instance_of Tire::Index, a.tire.index
|
281
|
+
assert a.tire.index.exists?, "Index should exist"
|
282
|
+
end
|
283
|
+
|
284
|
+
should "allow access to Tire class methods" do
|
285
|
+
class ::ActiveRecordClassWithTireMethods < ActiveRecord::Base
|
286
|
+
def self.search(*)
|
287
|
+
"THIS IS MY SEARCH!"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
ActiveRecordClassWithTireMethods.create :title => 'One'
|
292
|
+
ActiveRecordClassWithTireMethods.tire.index.refresh
|
293
|
+
|
294
|
+
assert_equal "THIS IS MY SEARCH!", ActiveRecordClassWithTireMethods.search
|
295
|
+
|
296
|
+
results = ActiveRecordClassWithTireMethods.tire.search 'one'
|
297
|
+
|
298
|
+
assert_equal 'One', results.first.title
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
context "with dynamic index name" do
|
304
|
+
setup do
|
305
|
+
@a = ActiveRecordClassWithDynamicIndexName.create! :title => 'Test'
|
306
|
+
@a.index.refresh
|
307
|
+
end
|
308
|
+
|
309
|
+
should "search in proper index" do
|
310
|
+
assert_equal 'dynamic_index', ActiveRecordClassWithDynamicIndexName.index.name
|
311
|
+
assert_equal 'dynamic_index', @a.index.name
|
312
|
+
|
313
|
+
results = ActiveRecordClassWithDynamicIndexName.search 'test'
|
314
|
+
assert_equal 'dynamic_index', results.first._index
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
context "within Rails" do
|
319
|
+
|
320
|
+
setup do
|
321
|
+
module ::Rails; end
|
322
|
+
|
323
|
+
a = ActiveRecordArticle.new :title => 'Test'
|
324
|
+
a.comments.build :author => 'fool', :body => 'Works!'
|
325
|
+
a.stats.build :pageviews => 12, :period => '2011-08'
|
326
|
+
a.save!
|
327
|
+
@id = a.id.to_s
|
328
|
+
|
329
|
+
a.index.refresh
|
330
|
+
@item = ActiveRecordArticle.search('test').first
|
331
|
+
end
|
332
|
+
|
333
|
+
should "have access to indexed properties" do
|
334
|
+
assert_equal 'Test', @item.title
|
335
|
+
assert_equal 'fool', @item.comments.first.author
|
336
|
+
assert_equal 12, @item.stats.first.pageviews
|
337
|
+
end
|
338
|
+
|
339
|
+
should "load the underlying models" do
|
340
|
+
assert_instance_of Results::Item, @item
|
341
|
+
assert_instance_of ActiveRecordArticle, @item.load
|
342
|
+
assert_equal 'Test', @item.load.title
|
343
|
+
|
344
|
+
assert_instance_of Results::Item, @item.comments.first
|
345
|
+
assert_instance_of ActiveRecordComment, @item.comments.first.load
|
346
|
+
assert_equal 'fool', @item.comments.first.load.author
|
347
|
+
end
|
348
|
+
|
349
|
+
should "load the underlying model with options" do
|
350
|
+
ActiveRecordArticle.expects(:find).with(@id, :include => 'comments')
|
351
|
+
@item.load(:include => 'comments')
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
context "with multiple class instances in one index" do
|
357
|
+
setup do
|
358
|
+
ActiveRecord::Schema.define do
|
359
|
+
create_table(:active_record_assets) { |t| t.string :title, :timestamp }
|
360
|
+
create_table(:active_record_model_one) { |t| t.string :title, :timestamp }
|
361
|
+
create_table(:active_record_model_two) { |t| t.string :title, :timestamp }
|
362
|
+
end
|
363
|
+
|
364
|
+
ActiveRecordModelOne.create :title => 'Title One', timestamp: Time.now.to_i
|
365
|
+
ActiveRecordModelTwo.create :title => 'Title Two', timestamp: Time.now.to_i
|
366
|
+
ActiveRecordModelOne.tire.index.refresh
|
367
|
+
ActiveRecordModelTwo.tire.index.refresh
|
368
|
+
|
369
|
+
|
370
|
+
ActiveRecordVideo.create! :title => 'Title One', timestamp: Time.now.to_i
|
371
|
+
ActiveRecordPhoto.create! :title => 'Title Two', timestamp: Time.now.to_i
|
372
|
+
ActiveRecordAsset.tire.index.refresh
|
373
|
+
end
|
374
|
+
|
375
|
+
teardown do
|
376
|
+
ActiveRecordModelOne.destroy_all
|
377
|
+
ActiveRecordModelTwo.destroy_all
|
378
|
+
ActiveRecordModelOne.tire.index.delete
|
379
|
+
ActiveRecordModelTwo.tire.index.delete
|
380
|
+
|
381
|
+
ActiveRecordAsset.destroy_all
|
382
|
+
ActiveRecordAsset.tire.index.delete
|
383
|
+
ActiveRecordModelOne.destroy_all
|
384
|
+
end
|
385
|
+
|
386
|
+
should "eagerly load instances of multiple classes, from multiple indices" do
|
387
|
+
s = Tire.search ['active_record_model_one', 'active_record_model_two'], :load => true do
|
388
|
+
query { string 'title' }
|
389
|
+
sort { by :timestamp }
|
390
|
+
end
|
391
|
+
|
392
|
+
# puts s.results[0].inspect
|
393
|
+
|
394
|
+
assert_equal 2, s.results.length
|
395
|
+
assert_instance_of ActiveRecordModelOne, s.results[0]
|
396
|
+
assert_instance_of ActiveRecordModelTwo, s.results[1]
|
397
|
+
end
|
398
|
+
|
399
|
+
should "eagerly load all STI descendant records" do
|
400
|
+
s = Tire.search('active_record_assets', :load => true) do
|
401
|
+
query { string 'title' }
|
402
|
+
sort { by :timestamp }
|
403
|
+
end
|
404
|
+
|
405
|
+
assert_equal 2, s.results.length
|
406
|
+
assert_instance_of ActiveRecordVideo, s.results[0]
|
407
|
+
assert_instance_of ActiveRecordPhoto, s.results[1]
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
context "with namespaced models" do
|
412
|
+
setup do
|
413
|
+
ActiveRecord::Schema.define { create_table(:active_record_namespace_my_models) { |t| t.string :title, :timestamp } }
|
414
|
+
|
415
|
+
ActiveRecordNamespace::MyModel.create :title => 'Test'
|
416
|
+
ActiveRecordNamespace::MyModel.tire.index.refresh
|
417
|
+
end
|
418
|
+
|
419
|
+
teardown do
|
420
|
+
ActiveRecordNamespace::MyModel.destroy_all
|
421
|
+
ActiveRecordNamespace::MyModel.tire.index.delete
|
422
|
+
end
|
423
|
+
|
424
|
+
should "save document into index on save and find it" do
|
425
|
+
results = ActiveRecordNamespace::MyModel.search 'test'
|
426
|
+
|
427
|
+
assert results.any?, "No results returned: #{results.inspect}"
|
428
|
+
assert_equal 1, results.count
|
429
|
+
|
430
|
+
assert_instance_of Results::Item, results.first
|
431
|
+
end
|
432
|
+
|
433
|
+
should "eagerly load the records from returned hits" do
|
434
|
+
results = ActiveRecordNamespace::MyModel.search 'test', :load => true
|
435
|
+
|
436
|
+
assert results.any?, "No results returned: #{results.inspect}"
|
437
|
+
assert_instance_of ActiveRecordNamespace::MyModel, results.first
|
438
|
+
assert_equal ActiveRecordNamespace::MyModel.find(1), results.first
|
439
|
+
end
|
440
|
+
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
end
|
445
|
+
|
446
|
+
end
|