tire-erez 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +32 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +10 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.markdown +775 -0
  7. data/Rakefile +51 -0
  8. data/examples/rails-application-template.rb +263 -0
  9. data/examples/tire-dsl.rb +932 -0
  10. data/lib/tire.rb +59 -0
  11. data/lib/tire/alias.rb +296 -0
  12. data/lib/tire/configuration.rb +38 -0
  13. data/lib/tire/count.rb +85 -0
  14. data/lib/tire/dsl.rb +114 -0
  15. data/lib/tire/http/client.rb +62 -0
  16. data/lib/tire/http/clients/curb.rb +61 -0
  17. data/lib/tire/http/clients/faraday.rb +71 -0
  18. data/lib/tire/http/response.rb +27 -0
  19. data/lib/tire/index.rb +443 -0
  20. data/lib/tire/logger.rb +60 -0
  21. data/lib/tire/model/callbacks.rb +40 -0
  22. data/lib/tire/model/import.rb +27 -0
  23. data/lib/tire/model/indexing.rb +134 -0
  24. data/lib/tire/model/naming.rb +100 -0
  25. data/lib/tire/model/percolate.rb +99 -0
  26. data/lib/tire/model/persistence.rb +72 -0
  27. data/lib/tire/model/persistence/attributes.rb +148 -0
  28. data/lib/tire/model/persistence/finders.rb +54 -0
  29. data/lib/tire/model/persistence/storage.rb +77 -0
  30. data/lib/tire/model/search.rb +322 -0
  31. data/lib/tire/multi_search.rb +263 -0
  32. data/lib/tire/results/collection.rb +156 -0
  33. data/lib/tire/results/item.rb +94 -0
  34. data/lib/tire/results/pagination.rb +68 -0
  35. data/lib/tire/rubyext/hash.rb +8 -0
  36. data/lib/tire/rubyext/ruby_1_8.rb +1 -0
  37. data/lib/tire/rubyext/symbol.rb +11 -0
  38. data/lib/tire/rubyext/uri_escape.rb +74 -0
  39. data/lib/tire/search.rb +211 -0
  40. data/lib/tire/search/facet.rb +81 -0
  41. data/lib/tire/search/filter.rb +28 -0
  42. data/lib/tire/search/highlight.rb +37 -0
  43. data/lib/tire/search/queries/match.rb +40 -0
  44. data/lib/tire/search/query.rb +250 -0
  45. data/lib/tire/search/scan.rb +114 -0
  46. data/lib/tire/search/script_field.rb +23 -0
  47. data/lib/tire/search/sort.rb +25 -0
  48. data/lib/tire/tasks.rb +138 -0
  49. data/lib/tire/utils.rb +17 -0
  50. data/lib/tire/version.rb +18 -0
  51. data/test/fixtures/articles/1.json +1 -0
  52. data/test/fixtures/articles/2.json +1 -0
  53. data/test/fixtures/articles/3.json +1 -0
  54. data/test/fixtures/articles/4.json +1 -0
  55. data/test/fixtures/articles/5.json +1 -0
  56. data/test/integration/active_model_indexing_test.rb +51 -0
  57. data/test/integration/active_model_searchable_test.rb +114 -0
  58. data/test/integration/active_record_searchable_test.rb +620 -0
  59. data/test/integration/boolean_queries_test.rb +43 -0
  60. data/test/integration/boosting_queries_test.rb +32 -0
  61. data/test/integration/bulk_test.rb +86 -0
  62. data/test/integration/count_test.rb +64 -0
  63. data/test/integration/custom_score_queries_test.rb +89 -0
  64. data/test/integration/dis_max_queries_test.rb +68 -0
  65. data/test/integration/dsl_search_test.rb +30 -0
  66. data/test/integration/explanation_test.rb +44 -0
  67. data/test/integration/facets_test.rb +311 -0
  68. data/test/integration/filtered_queries_test.rb +66 -0
  69. data/test/integration/filters_test.rb +75 -0
  70. data/test/integration/fuzzy_queries_test.rb +20 -0
  71. data/test/integration/highlight_test.rb +64 -0
  72. data/test/integration/index_aliases_test.rb +122 -0
  73. data/test/integration/index_mapping_test.rb +43 -0
  74. data/test/integration/index_store_test.rb +112 -0
  75. data/test/integration/index_update_document_test.rb +121 -0
  76. data/test/integration/match_query_test.rb +79 -0
  77. data/test/integration/mongoid_searchable_test.rb +309 -0
  78. data/test/integration/multi_search_test.rb +114 -0
  79. data/test/integration/nested_query_test.rb +135 -0
  80. data/test/integration/percolator_test.rb +111 -0
  81. data/test/integration/persistent_model_test.rb +205 -0
  82. data/test/integration/prefix_query_test.rb +43 -0
  83. data/test/integration/query_return_version_test.rb +70 -0
  84. data/test/integration/query_string_test.rb +52 -0
  85. data/test/integration/range_queries_test.rb +36 -0
  86. data/test/integration/reindex_test.rb +56 -0
  87. data/test/integration/results_test.rb +58 -0
  88. data/test/integration/scan_test.rb +56 -0
  89. data/test/integration/script_fields_test.rb +38 -0
  90. data/test/integration/sort_test.rb +52 -0
  91. data/test/integration/text_query_test.rb +39 -0
  92. data/test/models/active_model_article.rb +31 -0
  93. data/test/models/active_model_article_with_callbacks.rb +49 -0
  94. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  95. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  96. data/test/models/active_record_models.rb +131 -0
  97. data/test/models/article.rb +15 -0
  98. data/test/models/mongoid_models.rb +85 -0
  99. data/test/models/persistent_article.rb +11 -0
  100. data/test/models/persistent_article_in_index.rb +16 -0
  101. data/test/models/persistent_article_in_namespace.rb +12 -0
  102. data/test/models/persistent_article_with_casting.rb +28 -0
  103. data/test/models/persistent_article_with_defaults.rb +12 -0
  104. data/test/models/persistent_article_with_percolation.rb +5 -0
  105. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  106. data/test/models/supermodel_article.rb +17 -0
  107. data/test/models/validated_model.rb +11 -0
  108. data/test/test_helper.rb +118 -0
  109. data/test/unit/active_model_lint_test.rb +17 -0
  110. data/test/unit/configuration_test.rb +84 -0
  111. data/test/unit/count_test.rb +67 -0
  112. data/test/unit/http_client_test.rb +79 -0
  113. data/test/unit/http_response_test.rb +49 -0
  114. data/test/unit/index_alias_test.rb +335 -0
  115. data/test/unit/index_test.rb +1098 -0
  116. data/test/unit/logger_test.rb +125 -0
  117. data/test/unit/model_callbacks_test.rb +116 -0
  118. data/test/unit/model_import_test.rb +75 -0
  119. data/test/unit/model_initialization_test.rb +31 -0
  120. data/test/unit/model_persistence_test.rb +548 -0
  121. data/test/unit/model_search_test.rb +964 -0
  122. data/test/unit/multi_search_test.rb +304 -0
  123. data/test/unit/results_collection_test.rb +372 -0
  124. data/test/unit/results_item_test.rb +173 -0
  125. data/test/unit/rubyext_test.rb +66 -0
  126. data/test/unit/search_facet_test.rb +186 -0
  127. data/test/unit/search_filter_test.rb +42 -0
  128. data/test/unit/search_highlight_test.rb +46 -0
  129. data/test/unit/search_query_test.rb +419 -0
  130. data/test/unit/search_scan_test.rb +113 -0
  131. data/test/unit/search_script_field_test.rb +26 -0
  132. data/test/unit/search_sort_test.rb +50 -0
  133. data/test/unit/search_test.rb +556 -0
  134. data/test/unit/tire_test.rb +144 -0
  135. data/tire.gemspec +83 -0
  136. metadata +586 -0
@@ -0,0 +1,23 @@
1
+ module Tire
2
+ module Search
3
+
4
+ # http://www.elasticsearch.org/guide/reference/api/search/script-fields.html
5
+ # http://www.elasticsearch.org/guide/reference/modules/scripting.html
6
+
7
+ class ScriptField
8
+
9
+ def initialize(name, options)
10
+ @hash = { name => options }
11
+ end
12
+
13
+ def to_json(options={})
14
+ to_hash.to_json
15
+ end
16
+
17
+ def to_hash
18
+ @hash
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -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(options={})
20
+ @value.to_json
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,138 @@
1
+ require 'rake'
2
+ require 'benchmark'
3
+
4
+ namespace :tire do
5
+
6
+ full_comment_import = <<-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_import
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.2f", elapsed)} seconds"
28
+ when 60..hour-1
29
+ "#{(elapsed/60).floor} minutes and #{(elapsed % 60).floor} seconds"
30
+ when hour..day
31
+ "#{(elapsed/hour).floor} hours and #{(elapsed/60 % hour).floor} minutes"
32
+ else
33
+ "#{(elapsed/hour).round} hours"
34
+ end
35
+ end
36
+
37
+ if ENV['CLASS'].to_s == ''
38
+ puts '='*90, 'USAGE', '='*90, full_comment_import, ""
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 = MultiJson.encode(klass.tire.mapping_to_hash, :pretty => Tire::Configuration.pretty)
56
+ puts "[IMPORT] Creating index '#{index.name}' with mapping:", mapping
57
+ unless index.create( :mappings => klass.tire.mapping_to_hash, :settings => klass.tire.settings )
58
+ STDERR.puts "[ERROR] There has been an error when creating the index -- elasticsearch returned:",
59
+ index.response
60
+ exit(1)
61
+ end
62
+ end
63
+
64
+ STDOUT.sync = true
65
+ puts "[IMPORT] Starting import for the '#{ENV['CLASS']}' class"
66
+ tty_cols = 80
67
+ total = klass.count rescue nil
68
+ offset = (total.to_s.size*2)+8
69
+ done = 0
70
+
71
+ STDOUT.puts '-'*tty_cols
72
+ elapsed = Benchmark.realtime do
73
+
74
+ # Add Kaminari-powered "paginate" method
75
+ #
76
+ if defined?(Kaminari) && klass.respond_to?(:page)
77
+ klass.instance_eval do
78
+ def paginate(options = {})
79
+ page(options[:page]).per(options[:per_page]).to_a
80
+ end
81
+ end
82
+ end unless klass.respond_to?(:paginate)
83
+
84
+ # Import the documents
85
+ #
86
+ index.import(klass, params) do |documents|
87
+
88
+ if total
89
+ done += documents.to_a.size
90
+ # I CAN HAZ PROGREZ BAR LIEK HOMEBRU!
91
+ percent = ( (done.to_f / total) * 100 ).to_i
92
+ glyphs = ( percent * ( (tty_cols-offset).to_f/100 ) ).to_i
93
+ STDOUT.print( "#" * glyphs )
94
+ STDOUT.print( "\r"*tty_cols+"#{done}/#{total} | \e[1m#{percent}%\e[0m " )
95
+ end
96
+
97
+ # Don't forget to return the documents collection back!
98
+ documents
99
+ end
100
+ end
101
+
102
+ puts "", '='*80, "Import finished in #{elapsed_to_human(elapsed)}"
103
+ end
104
+
105
+ namespace :index do
106
+
107
+ full_comment_drop = <<-DESC.gsub(/ /, '')
108
+ Delete indices passed in the INDEX environment variable; separate multiple indices by comma.
109
+
110
+ Pass name of a single index to drop in the INDEX environmnet variable:
111
+ $ rake environment tire:index:drop INDEX=articles
112
+
113
+ Pass names of multiple indices to drop in the INDEX or INDICES environmnet variable:
114
+ $ rake environment tire:index:drop INDICES=articles-2011-01,articles-2011-02
115
+
116
+ DESC
117
+ desc full_comment_drop
118
+ task :drop do
119
+ index_names = (ENV['INDEX'] || ENV['INDICES']).to_s.split(/,\s*/)
120
+
121
+ if index_names.empty?
122
+ puts '='*90, 'USAGE', '='*90, full_comment_drop, ""
123
+ exit(1)
124
+ end
125
+
126
+ index_names.each do |name|
127
+ index = Tire::Index.new(name)
128
+ print "* Deleting index \e[1m#{index.name}\e[0m... "
129
+ puts index.delete ? "\e[32mOK\e[0m" : "\e[31mFAILED\e[0m | #{index.response.body}"
130
+ end
131
+
132
+ puts ""
133
+
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -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
@@ -0,0 +1,18 @@
1
+ module Tire
2
+ VERSION = "0.5.4"
3
+
4
+ CHANGELOG =<<-END
5
+ IMPORTANT CHANGES LATELY:
6
+
7
+ * Added the support for the Count API
8
+ * Escape single quotes in `to_curl` serialization
9
+ * Added JRuby compatibility
10
+ * Added proper `as_json` support for `Results::Collection` and `Results::Item` classes
11
+ * Added extracting the `routing` information in the `Index#store` method
12
+ * Refactored the `update_index` method for search and persistence integration
13
+ * Cast collection properties in Model::Persistence as empty Array by default
14
+ * Allow passing `:index` option to `MyModel.import`
15
+ * Update to Mocha ~> 0.13
16
+ * Update to MultiJson ~> 1.3
17
+ END
18
+ 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,620 @@
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
+ create_table :active_record_model_with_percolations do |t|
36
+ t.string :title
37
+ t.datetime :created_at, :default => 'NOW()'
38
+ end
39
+ end
40
+ end
41
+
42
+ context "ActiveRecord integration" do
43
+
44
+ setup do
45
+ ActiveRecordArticle.delete_all
46
+ Tire.index('active_record_articles').delete
47
+
48
+ load File.expand_path('../../models/active_record_models.rb', __FILE__)
49
+ end
50
+
51
+ teardown do
52
+ ActiveRecordArticle.delete_all
53
+ Tire.index('active_record_articles').delete
54
+ end
55
+
56
+ should "configure mapping" do
57
+ assert_equal 'snowball', ActiveRecordArticle.mapping[:title][:analyzer]
58
+ assert_equal 10, ActiveRecordArticle.mapping[:title][:boost]
59
+
60
+ assert_equal 'snowball', ActiveRecordArticle.index.mapping['active_record_article']['properties']['title']['analyzer']
61
+ end
62
+
63
+ should "save document into index on save and find it" do
64
+ a = ActiveRecordArticle.new :title => 'Test'
65
+ a.save!
66
+ id = a.id
67
+
68
+ a.index.refresh
69
+
70
+ results = ActiveRecordArticle.search 'test'
71
+
72
+ assert results.any?
73
+ assert_equal 1, results.count
74
+
75
+ assert_instance_of Results::Item, results.first
76
+ assert_not_nil results.first.id
77
+ assert_equal id.to_s, results.first.id.to_s
78
+ assert results.first.persisted?, "Record should be persisted"
79
+ assert_not_nil results.first._score
80
+ assert_equal 'Test', results.first.title
81
+ end
82
+
83
+ should "remove document from index on destroy" do
84
+ a = ActiveRecordArticle.new :title => 'Test remove...'
85
+ a.save!
86
+ assert_equal 1, ActiveRecordArticle.count
87
+
88
+ a.destroy
89
+ assert_equal 0, ActiveRecordArticle.all.size
90
+
91
+ a.index.refresh
92
+ results = ActiveRecordArticle.search 'test'
93
+ assert_equal 0, results.count
94
+ end
95
+
96
+ should "return documents with scores" do
97
+ ActiveRecordArticle.create! :title => 'foo'
98
+ ActiveRecordArticle.create! :title => 'bar'
99
+
100
+ ActiveRecordArticle.index.refresh
101
+ results = ActiveRecordArticle.search 'foo OR bar^100'
102
+ assert_equal 2, results.count
103
+
104
+ assert_equal 'bar', results.first.title
105
+ end
106
+
107
+ should "raise exception on invalid query" do
108
+ ActiveRecordArticle.create! :title => 'Test'
109
+
110
+ assert_raise Search::SearchRequestFailed do
111
+ ActiveRecordArticle.search '[x'
112
+ end
113
+ end
114
+
115
+ context "with eager loading" do
116
+ setup do
117
+ ActiveRecordArticle.destroy_all
118
+ 5.times { |n| ActiveRecordArticle.create! :title => "Test #{n+1}" }
119
+ ActiveRecordArticle.index.refresh
120
+ end
121
+
122
+ should "load records on query search" do
123
+ results = ActiveRecordArticle.search '"Test 1"', :load => true
124
+
125
+ assert results.any?
126
+ assert_equal ActiveRecordArticle.find(1), results.first
127
+ end
128
+
129
+ should "load records on block search" do
130
+ results = ActiveRecordArticle.search :load => true do
131
+ query { string '"Test 1"' }
132
+ end
133
+
134
+ assert_equal ActiveRecordArticle.find(1), results.first
135
+ end
136
+
137
+ should "load records with options on query search" do
138
+ assert_equal ActiveRecordArticle.find(['1'], :include => 'comments').first,
139
+ ActiveRecordArticle.search('"Test 1"',
140
+ :load => { :include => 'comments' }).results.first
141
+ end
142
+
143
+ should "return empty collection for nonmatching query" do
144
+ assert_nothing_raised do
145
+ results = ActiveRecordArticle.search :load => true do
146
+ query { string '"Hic Sunt Leones"' }
147
+ end
148
+ assert_equal 0, results.size
149
+ assert ! results.any?
150
+ end
151
+ end
152
+
153
+ should "iterate results with hits" do
154
+ results = ActiveRecordArticle.search :load => true do
155
+ query { string '"Test 1" OR "Test 2"' }
156
+ end
157
+ results.each_with_hit do |result, hit|
158
+ assert_instance_of ActiveRecordArticle, result
159
+ assert_instance_of Hash, hit
160
+ assert_match /Test \d/, result.title
161
+ assert_match /Test \d/, hit['_source']['title']
162
+ assert hit['_score'] > 0
163
+ end
164
+ end
165
+
166
+ should "provide access to highlighted fields in hit" do
167
+ results = ActiveRecordArticle.search :load => true do
168
+ query { string '"Test 1" OR "Test 2"' }
169
+ highlight :title
170
+ end
171
+ results.each_with_hit do |result, hit|
172
+ assert_equal 1, hit['highlight']['title'].size
173
+ end
174
+ end
175
+ end
176
+
177
+ context "with pagination" do
178
+ setup do
179
+ 1.upto(9) { |number| ActiveRecordArticle.create :title => "Test#{number}" }
180
+ ActiveRecordArticle.index.refresh
181
+ end
182
+
183
+ context "and parameter searches" do
184
+
185
+ should "find first page with five results" do
186
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 1
187
+ assert_equal 5, results.size
188
+
189
+ # WillPaginate
190
+ #
191
+ assert_equal 2, results.total_pages
192
+ assert_equal 1, results.current_page
193
+ assert_equal nil, results.previous_page
194
+ assert_equal 2, results.next_page
195
+
196
+ # Kaminari
197
+ #
198
+ assert_equal 5, results.limit_value
199
+ assert_equal 9, results.total_count
200
+ assert_equal 2, results.num_pages
201
+ assert_equal 0, results.offset_value
202
+
203
+ assert_equal 'Test1', results.first.title
204
+ end
205
+
206
+ should "find second page with four results" do
207
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 2
208
+ assert_equal 4, results.size
209
+
210
+ assert_equal 2, results.total_pages
211
+ assert_equal 2, results.current_page
212
+ assert_equal 1, results.previous_page
213
+ assert_equal nil, results.next_page
214
+
215
+ #kaminari
216
+ assert_equal 5, results.limit_value
217
+ assert_equal 9, results.total_count
218
+ assert_equal 2, results.num_pages
219
+ assert_equal 5, results.offset_value
220
+
221
+ assert_equal 'Test6', results.first.title
222
+ end
223
+
224
+ should "find not find missing (third) page" do
225
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 3
226
+ assert_equal 0, results.size
227
+
228
+ assert_equal 2, results.total_pages
229
+ assert_equal 3, results.current_page
230
+ assert_equal 2, results.previous_page
231
+ assert_equal nil, results.next_page
232
+
233
+ #kaminari
234
+ assert_equal 5, results.limit_value
235
+ assert_equal 9, results.total_count
236
+ assert_equal 2, results.num_pages
237
+ assert_equal 10, results.offset_value
238
+
239
+ assert_nil results.first
240
+ end
241
+
242
+ context "without an explicit per_page" do
243
+
244
+ should "not find a missing (second) page" do
245
+ results = ActiveRecordArticle.search 'test*', :sort => 'title', :page => 2
246
+ assert_equal 0, results.size
247
+
248
+ # WillPaginate
249
+ #
250
+ assert_equal 1, results.total_pages
251
+ assert_equal 2, results.current_page
252
+ assert_equal 1, results.previous_page
253
+ assert_equal nil, results.next_page
254
+
255
+ assert_nil results.first
256
+ end
257
+
258
+ end
259
+
260
+ end
261
+
262
+ context "and block searches" do
263
+ setup { @q = 'test*' }
264
+
265
+ context "with page/per_page" do
266
+
267
+ should "find first page with five results" do
268
+ results = ActiveRecordArticle.search :page => 1, :per_page => 5 do |search|
269
+ search.query { |query| query.string @q }
270
+ search.sort { by :title }
271
+ end
272
+ assert_equal 5, results.size
273
+
274
+ assert_equal 2, results.total_pages
275
+ assert_equal 1, results.current_page
276
+ assert_equal nil, results.previous_page
277
+ assert_equal 2, results.next_page
278
+
279
+ assert_equal 'Test1', results.first.title
280
+ end
281
+
282
+ should "find second page with four results" do
283
+ results = ActiveRecordArticle.search :page => 2, :per_page => 5 do |search|
284
+ search.query { |query| query.string @q }
285
+ search.sort { by :title }
286
+ end
287
+ assert_equal 4, results.size
288
+
289
+ assert_equal 2, results.total_pages
290
+ assert_equal 2, results.current_page
291
+ assert_equal 1, results.previous_page
292
+ assert_equal nil, results.next_page
293
+
294
+ assert_equal 'Test6', results.first.title
295
+ end
296
+
297
+ should "not find a missing (third) page" do
298
+ results = ActiveRecordArticle.search :page => 3, :per_page => 5 do |search|
299
+ search.query { |query| query.string @q }
300
+ search.sort { by :title }
301
+ end
302
+ assert_equal 0, results.size
303
+
304
+ assert_equal 2, results.total_pages
305
+ assert_equal 3, results.current_page
306
+ assert_equal 2, results.previous_page
307
+ assert_equal nil, results.next_page
308
+
309
+ assert_nil results.first
310
+ end
311
+
312
+ should "find second page with four loaded models" do
313
+ results = ActiveRecordArticle.search :load => true, :page => 2, :per_page => 5 do |search|
314
+ search.query { |query| query.string @q }
315
+ search.sort { by :title }
316
+ end
317
+ assert_equal 4, results.size
318
+ assert results.all? { |r| assert_instance_of ActiveRecordArticle, r }
319
+ assert_equal 'Test6', results.first.title
320
+ end
321
+
322
+ end
323
+
324
+ context "with from/size" do
325
+
326
+ should "find first page with five results" do
327
+ results = ActiveRecordArticle.search do |search|
328
+ search.query { |query| query.string @q }
329
+ search.sort { by :title }
330
+ search.from 0
331
+ search.size 5
332
+ end
333
+ assert_equal 5, results.size
334
+
335
+ assert_equal 2, results.total_pages
336
+ assert_equal 1, results.current_page
337
+ assert_equal nil, results.previous_page
338
+ assert_equal 2, results.next_page
339
+
340
+ assert_equal 'Test1', results.first.title
341
+ end
342
+
343
+ should "find second page with five results" do
344
+ results = ActiveRecordArticle.search do |search|
345
+ search.query { |query| query.string @q }
346
+ search.sort { by :title }
347
+ search.from 5
348
+ search.size 5
349
+ end
350
+ assert_equal 4, results.size
351
+
352
+ assert_equal 2, results.total_pages
353
+ assert_equal 2, results.current_page
354
+ assert_equal 1, results.previous_page
355
+ assert_equal nil, results.next_page
356
+
357
+ assert_equal 'Test6', results.first.title
358
+ end
359
+
360
+ should "not find a missing (third) page" do
361
+ results = ActiveRecordArticle.search do |search|
362
+ search.query { |query| query.string @q }
363
+ search.sort { by :title }
364
+ search.from 10
365
+ search.size 5
366
+ end
367
+ assert_equal 0, results.size
368
+
369
+ assert_equal 2, results.total_pages
370
+ assert_equal 3, results.current_page
371
+ assert_equal 2, results.previous_page
372
+ assert_equal nil, results.next_page
373
+
374
+ assert_nil results.first
375
+ end
376
+
377
+ end
378
+
379
+ end
380
+
381
+ end
382
+
383
+ context "with proxy" do
384
+
385
+ should "allow access to Tire instance methods" do
386
+ a = ActiveRecordClassWithTireMethods.create :title => 'One'
387
+ assert_equal "THIS IS MY INDEX!", a.index
388
+ assert_instance_of Tire::Index, a.tire.index
389
+ assert a.tire.index.exists?, "Index should exist"
390
+ end
391
+
392
+ should "allow access to Tire class methods" do
393
+ class ::ActiveRecordClassWithTireMethods < ActiveRecord::Base
394
+ def self.search(*)
395
+ "THIS IS MY SEARCH!"
396
+ end
397
+ end
398
+
399
+ ActiveRecordClassWithTireMethods.create :title => 'One'
400
+ ActiveRecordClassWithTireMethods.tire.index.refresh
401
+
402
+ assert_equal "THIS IS MY SEARCH!", ActiveRecordClassWithTireMethods.search
403
+
404
+ results = ActiveRecordClassWithTireMethods.tire.search 'one'
405
+
406
+ assert_equal 'One', results.first.title
407
+ end
408
+
409
+ end
410
+
411
+ context "with dynamic index name" do
412
+ setup do
413
+ @a = ActiveRecordClassWithDynamicIndexName.create! :title => 'Test'
414
+ @a.index.refresh
415
+ end
416
+
417
+ should "search in proper index" do
418
+ assert_equal 'dynamic_index', ActiveRecordClassWithDynamicIndexName.index.name
419
+ assert_equal 'dynamic_index', @a.index.name
420
+
421
+ results = ActiveRecordClassWithDynamicIndexName.search 'test'
422
+ assert_equal 'dynamic_index', results.first._index
423
+ end
424
+ end
425
+
426
+ context "within Rails" do
427
+
428
+ setup do
429
+ module ::Rails; end
430
+
431
+ a = ActiveRecordArticle.new :title => 'Test'
432
+ a.comments.build :author => 'fool', :body => 'Works!'
433
+ a.stats.build :pageviews => 12, :period => '2011-08'
434
+ a.save!
435
+ @id = a.id.to_s
436
+
437
+ a.index.refresh
438
+ @item = ActiveRecordArticle.search('test').first
439
+ end
440
+
441
+ should "have access to indexed properties" do
442
+ assert_equal 'Test', @item.title
443
+ assert_equal 'fool', @item.comments.first.author
444
+ assert_equal 12, @item.stats.first.pageviews
445
+ end
446
+
447
+ should "load the underlying models" do
448
+ assert_instance_of Results::Item, @item
449
+ assert_instance_of ActiveRecordArticle, @item.load
450
+ assert_equal 'Test', @item.load.title
451
+
452
+ assert_instance_of Results::Item, @item.comments.first
453
+ assert_instance_of ActiveRecordComment, @item.comments.first.load
454
+ assert_equal 'fool', @item.comments.first.load.author
455
+ end
456
+
457
+ should "load the underlying model with options" do
458
+ ActiveRecordArticle.expects(:find).with(@id, :include => 'comments')
459
+ @item.load(:include => 'comments')
460
+ end
461
+
462
+ end
463
+
464
+ context "with multiple class instances in one index" do
465
+ setup do
466
+ ActiveRecord::Schema.define do
467
+ create_table(:active_record_assets) { |t| t.string :title, :timestamp }
468
+ create_table(:active_record_model_one) { |t| t.string :title, :timestamp }
469
+ create_table(:active_record_model_two) { |t| t.string :title, :timestamp }
470
+ end
471
+
472
+ ActiveRecordModelOne.create :title => 'Title One', timestamp: Time.now.to_i
473
+ ActiveRecordModelTwo.create :title => 'Title Two', timestamp: Time.now.to_i
474
+ ActiveRecordModelOne.tire.index.refresh
475
+ ActiveRecordModelTwo.tire.index.refresh
476
+
477
+
478
+ ActiveRecordVideo.create! :title => 'Title One', timestamp: Time.now.to_i
479
+ ActiveRecordPhoto.create! :title => 'Title Two', timestamp: Time.now.to_i
480
+ ActiveRecordAsset.tire.index.refresh
481
+ end
482
+
483
+ teardown do
484
+ ActiveRecordModelOne.destroy_all
485
+ ActiveRecordModelTwo.destroy_all
486
+ ActiveRecordModelOne.tire.index.delete
487
+ ActiveRecordModelTwo.tire.index.delete
488
+
489
+ ActiveRecordAsset.destroy_all
490
+ ActiveRecordAsset.tire.index.delete
491
+ ActiveRecordModelOne.destroy_all
492
+ end
493
+
494
+ should "eagerly load instances of multiple classes, from multiple indices" do
495
+ s = Tire.search ['active_record_model_one', 'active_record_model_two'], :load => true do
496
+ query { string 'title' }
497
+ sort { by :timestamp }
498
+ end
499
+
500
+ # puts s.results[0].inspect
501
+
502
+ assert_equal 2, s.results.length
503
+ assert_instance_of ActiveRecordModelOne, s.results[0]
504
+ assert_instance_of ActiveRecordModelTwo, s.results[1]
505
+ end
506
+
507
+ should "eagerly load all STI descendant records" do
508
+ s = Tire.search('active_record_assets', :load => true) do
509
+ query { string 'title' }
510
+ sort { by :timestamp }
511
+ end
512
+
513
+ assert_equal 2, s.results.length
514
+ assert_instance_of ActiveRecordVideo, s.results[0]
515
+ assert_instance_of ActiveRecordPhoto, s.results[1]
516
+ end
517
+ end
518
+
519
+ context "with namespaced models" do
520
+ setup do
521
+ ActiveRecord::Schema.define { create_table(:active_record_namespace_my_models) { |t| t.string :title, :timestamp } }
522
+
523
+ ActiveRecordNamespace::MyModel.create :title => 'Test'
524
+ ActiveRecordNamespace::MyModel.tire.index.refresh
525
+ end
526
+
527
+ teardown do
528
+ ActiveRecordNamespace::MyModel.destroy_all
529
+ ActiveRecordNamespace::MyModel.tire.index.delete
530
+ end
531
+
532
+ should "save document into index on save and find it" do
533
+ results = ActiveRecordNamespace::MyModel.search 'test'
534
+
535
+ assert results.any?, "No results returned: #{results.inspect}"
536
+ assert_equal 1, results.count
537
+
538
+ assert_instance_of Results::Item, results.first
539
+ end
540
+
541
+ should "eagerly load the records from returned hits" do
542
+ results = ActiveRecordNamespace::MyModel.search 'test', :load => true
543
+
544
+ assert results.any?, "No results returned: #{results.inspect}"
545
+ assert_instance_of ActiveRecordNamespace::MyModel, results.first
546
+ assert_equal ActiveRecordNamespace::MyModel.find(1), results.first
547
+ end
548
+
549
+ end
550
+
551
+ context "multi search" do
552
+ setup do
553
+ # Tire.configure { logger STDERR }
554
+ ActiveRecordArticle.create! :title => 'Test'
555
+ ActiveRecordArticle.create! :title => 'Pest'
556
+ ActiveRecordArticle.index.refresh
557
+ end
558
+
559
+ should "return multiple result sets" do
560
+ results = ActiveRecordArticle.multi_search do
561
+ search do
562
+ query { match :title, 'test' }
563
+ end
564
+ search search_type: 'count' do
565
+ query { match :title, 'pest' }
566
+ end
567
+ search :articles, index: 'articles-test', type: 'article' do
568
+ query { all }
569
+ end
570
+ end
571
+
572
+ assert_equal 3, results.size
573
+
574
+ assert_equal 1, results[0].size
575
+ assert_equal 1, results[0].total
576
+
577
+ assert_equal 0, results[1].size
578
+ assert_equal 1, results[1].total
579
+
580
+ assert_equal 5, results[:articles].size
581
+ end
582
+
583
+ should "return model instances with the :load option" do
584
+ results = ActiveRecordArticle.multi_search do
585
+ search :items do
586
+ query { match :title, 'test' }
587
+ end
588
+ search :models, :load => true do
589
+ query { match :title, 'test' }
590
+ end
591
+ end
592
+
593
+ assert_instance_of Tire::Results::Item, results[:items].first
594
+ assert_instance_of ActiveRecordArticle, results[:models].first
595
+ end
596
+
597
+ end
598
+
599
+ context "percolated search" do
600
+ setup do
601
+ ActiveRecordModelWithPercolation.index.register_percolator_query('alert') { string 'warning' }
602
+ Tire.index('_percolator').refresh
603
+ end
604
+
605
+ should "return matching queries when percolating" do
606
+ a = ActiveRecordModelWithPercolation.new :title => 'Warning!'
607
+ assert_contains a.percolate, 'alert'
608
+ end
609
+
610
+ should "return matching queries when saving" do
611
+ a = ActiveRecordModelWithPercolation.create! :title => 'Warning!'
612
+ assert_contains a.tire_matches, 'alert'
613
+ end
614
+ end
615
+
616
+ end
617
+
618
+ end
619
+
620
+ end