tire 0.4.3 → 0.5.0

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.
Files changed (57) hide show
  1. data/.gitignore +1 -1
  2. data/.yardopts +1 -0
  3. data/README.markdown +2 -2
  4. data/examples/rails-application-template.rb +20 -6
  5. data/lib/tire.rb +2 -0
  6. data/lib/tire/alias.rb +1 -1
  7. data/lib/tire/configuration.rb +8 -0
  8. data/lib/tire/dsl.rb +69 -2
  9. data/lib/tire/index.rb +33 -20
  10. data/lib/tire/model/indexing.rb +7 -1
  11. data/lib/tire/model/persistence.rb +7 -4
  12. data/lib/tire/model/persistence/attributes.rb +1 -1
  13. data/lib/tire/model/persistence/finders.rb +4 -16
  14. data/lib/tire/model/search.rb +21 -8
  15. data/lib/tire/multi_search.rb +263 -0
  16. data/lib/tire/results/collection.rb +78 -49
  17. data/lib/tire/results/item.rb +6 -3
  18. data/lib/tire/results/pagination.rb +15 -1
  19. data/lib/tire/rubyext/ruby_1_8.rb +1 -7
  20. data/lib/tire/rubyext/uri_escape.rb +74 -0
  21. data/lib/tire/search.rb +33 -11
  22. data/lib/tire/search/facet.rb +8 -3
  23. data/lib/tire/search/filter.rb +1 -1
  24. data/lib/tire/search/highlight.rb +1 -1
  25. data/lib/tire/search/queries/match.rb +40 -0
  26. data/lib/tire/search/query.rb +42 -6
  27. data/lib/tire/search/scan.rb +1 -1
  28. data/lib/tire/search/script_field.rb +1 -1
  29. data/lib/tire/search/sort.rb +1 -1
  30. data/lib/tire/tasks.rb +17 -14
  31. data/lib/tire/version.rb +26 -8
  32. data/test/integration/active_record_searchable_test.rb +248 -129
  33. data/test/integration/boosting_queries_test.rb +32 -0
  34. data/test/integration/custom_score_queries_test.rb +1 -0
  35. data/test/integration/dsl_search_test.rb +9 -1
  36. data/test/integration/facets_test.rb +19 -6
  37. data/test/integration/match_query_test.rb +79 -0
  38. data/test/integration/multi_search_test.rb +114 -0
  39. data/test/integration/persistent_model_test.rb +58 -0
  40. data/test/models/article.rb +1 -1
  41. data/test/models/persistent_article_in_index.rb +16 -0
  42. data/test/models/persistent_article_with_defaults.rb +4 -3
  43. data/test/test_helper.rb +3 -1
  44. data/test/unit/configuration_test.rb +10 -0
  45. data/test/unit/index_test.rb +69 -27
  46. data/test/unit/model_initialization_test.rb +31 -0
  47. data/test/unit/model_persistence_test.rb +21 -7
  48. data/test/unit/model_search_test.rb +56 -5
  49. data/test/unit/multi_search_test.rb +304 -0
  50. data/test/unit/results_collection_test.rb +42 -2
  51. data/test/unit/results_item_test.rb +4 -0
  52. data/test/unit/search_facet_test.rb +35 -11
  53. data/test/unit/search_query_test.rb +96 -0
  54. data/test/unit/search_test.rb +60 -3
  55. data/test/unit/tire_test.rb +14 -0
  56. data/tire.gemspec +0 -1
  57. metadata +75 -44
@@ -1,7 +1 @@
1
- require 'rubygems'
2
-
3
- # Require URI escape/unescape compatibility layer from Rack
4
- #
5
- # See <http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-encode_www_form_component>
6
- #
7
- require 'rack/backports/uri/common_18'
1
+ require 'tire/rubyext/uri_escape'
@@ -0,0 +1,74 @@
1
+ # Steal the URI escape/unescape compatibility layer from Rack
2
+ #
3
+ # See <http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-encode_www_form_component>
4
+
5
+ # :stopdoc:
6
+
7
+ # Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x
8
+ #
9
+ # https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
10
+ #
11
+ #
12
+
13
+ module URI
14
+ TBLENCWWWCOMP_ = {} # :nodoc:
15
+ TBLDECWWWCOMP_ = {} # :nodoc:
16
+
17
+ # Encode given +s+ to URL-encoded form data.
18
+ #
19
+ # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
20
+ # (ASCII space) to + and converts others to %XX.
21
+ #
22
+ # This is an implementation of
23
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
24
+ #
25
+ # See URI.decode_www_form_component, URI.encode_www_form
26
+ def self.encode_www_form_component(s)
27
+ str = s.to_s
28
+ if RUBY_VERSION < "1.9" && $KCODE =~ /u/i
29
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/) do
30
+ '%' + $1.unpack('H2' * Rack::Utils.bytesize($1)).join('%').upcase
31
+ end.tr(' ', '+')
32
+ else
33
+ if TBLENCWWWCOMP_.empty?
34
+ tbl = {}
35
+ 256.times do |i|
36
+ tbl[i.chr] = '%%%02X' % i
37
+ end
38
+ tbl[' '] = '+'
39
+ begin
40
+ TBLENCWWWCOMP_.replace(tbl)
41
+ TBLENCWWWCOMP_.freeze
42
+ rescue
43
+ end
44
+ end
45
+ str.gsub(/[^*\-.0-9A-Z_a-z]/) {|m| TBLENCWWWCOMP_[m]}
46
+ end
47
+ end
48
+
49
+ # Decode given +str+ of URL-encoded form data.
50
+ #
51
+ # This decods + to SP.
52
+ #
53
+ # See URI.encode_www_form_component, URI.decode_www_form
54
+ def self.decode_www_form_component(str, enc=nil)
55
+ if TBLDECWWWCOMP_.empty?
56
+ tbl = {}
57
+ 256.times do |i|
58
+ h, l = i>>4, i&15
59
+ tbl['%%%X%X' % [h, l]] = i.chr
60
+ tbl['%%%x%X' % [h, l]] = i.chr
61
+ tbl['%%%X%x' % [h, l]] = i.chr
62
+ tbl['%%%x%x' % [h, l]] = i.chr
63
+ end
64
+ tbl['+'] = ' '
65
+ begin
66
+ TBLDECWWWCOMP_.replace(tbl)
67
+ TBLDECWWWCOMP_.freeze
68
+ rescue
69
+ end
70
+ end
71
+ raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%[0-9a-fA-F]{2}|[^%])*\z/ =~ str
72
+ str.gsub(/\+|%[0-9a-fA-F]{2}/) {|m| TBLDECWWWCOMP_[m]}
73
+ end
74
+ end
data/lib/tire/search.rb CHANGED
@@ -4,7 +4,7 @@ module Tire
4
4
 
5
5
  class Search
6
6
 
7
- attr_reader :indices, :query, :facets, :filters, :options, :explain, :script_fields
7
+ attr_reader :indices, :types, :query, :facets, :filters, :options, :explain, :script_fields
8
8
 
9
9
  def initialize(indices=nil, options={}, &block)
10
10
  if indices.is_a?(Hash)
@@ -48,7 +48,8 @@ module Tire
48
48
  end
49
49
 
50
50
  def params
51
- @options.empty? ? '' : '?' + @options.to_param
51
+ options = @options.except(:wrapper)
52
+ options.empty? ? '' : '?' + options.to_param
52
53
  end
53
54
 
54
55
  def query(&block)
@@ -106,6 +107,11 @@ module Tire
106
107
  self
107
108
  end
108
109
 
110
+ def partial_field(name, options)
111
+ @partial_fields ||= {}
112
+ @partial_fields[name] = options
113
+ end
114
+
109
115
  def explain(value)
110
116
  @explain = value
111
117
  self
@@ -116,6 +122,16 @@ module Tire
116
122
  self
117
123
  end
118
124
 
125
+ def min_score(value)
126
+ @min_score = value
127
+ self
128
+ end
129
+
130
+ def track_scores(value)
131
+ @track_scores = value
132
+ self
133
+ end
134
+
119
135
  def perform
120
136
  @response = Configuration.client.get(self.url + self.params, self.to_json)
121
137
  if @response.failure?
@@ -130,11 +146,11 @@ module Tire
130
146
  end
131
147
 
132
148
  def to_curl
133
- %Q|curl -X GET "#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty=true" -d '#{to_json}'|
149
+ %Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '#{to_json}'|
134
150
  end
135
151
 
136
152
  def to_hash
137
- @options.delete(:payload) || begin
153
+ @options[:payload] || begin
138
154
  request = {}
139
155
  request.update( { :indices_boost => @indices_boost } ) if @indices_boost
140
156
  request.update( { :query => @query.to_hash } ) if @query
@@ -146,33 +162,39 @@ module Tire
146
162
  request.update( { :size => @size } ) if @size
147
163
  request.update( { :from => @from } ) if @from
148
164
  request.update( { :fields => @fields } ) if @fields
165
+ request.update( { :partial_fields => @partial_fields } ) if @partial_fields
149
166
  request.update( { :script_fields => @script_fields } ) if @script_fields
150
167
  request.update( { :version => @version } ) if @version
151
168
  request.update( { :explain => @explain } ) if @explain
169
+ request.update( { :min_score => @min_score } ) if @min_score
170
+ request.update( { :track_scores => @track_scores } ) if @track_scores
152
171
  request
153
172
  end
154
173
  end
155
174
 
156
- def to_json
175
+ def to_json(options={})
157
176
  payload = to_hash
158
177
  # TODO: Remove when deprecated interface is removed
159
- payload.is_a?(String) ? payload : payload.to_json
178
+ if payload.is_a?(String)
179
+ payload
180
+ else
181
+ MultiJson.encode(payload, :pretty => Configuration.pretty)
182
+ end
160
183
  end
161
184
 
162
- def logged(error=nil)
185
+ def logged(endpoint='_search')
163
186
  if Configuration.logger
164
187
 
165
- Configuration.logger.log_request '_search', indices, to_curl
188
+ Configuration.logger.log_request endpoint, indices, to_curl
166
189
 
167
190
  took = @json['took'] rescue nil
168
191
  code = @response.code rescue nil
169
192
 
170
193
  if Configuration.logger.level.to_s == 'debug'
171
- # FIXME: Depends on RestClient implementation
172
194
  body = if @json
173
- defined?(Yajl) ? Yajl::Encoder.encode(@json, :pretty => true) : MultiJson.encode(@json)
195
+ MultiJson.encode( @json, :pretty => Configuration.pretty)
174
196
  else
175
- @response.body rescue nil
197
+ MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue ''
176
198
  end
177
199
  else
178
200
  body = ''
@@ -55,12 +55,17 @@ module Tire
55
55
  @value = { :query => Query.new(&block).to_hash }
56
56
  end
57
57
 
58
- def filter(field, value, options={})
59
- @value = { :filter => { :term => { field => value }}.update(options) }
58
+ def filter(type, options={})
59
+ @value = { :filter => Filter.new(type, options) }
60
60
  self
61
61
  end
62
62
 
63
- def to_json
63
+ def facet_filter(type, *options)
64
+ @value[:facet_filter] = Filter.new(type, *options).to_hash
65
+ self
66
+ end
67
+
68
+ def to_json(options={})
64
69
  to_hash.to_json
65
70
  end
66
71
 
@@ -15,7 +15,7 @@ module Tire
15
15
  @hash = { type => value }
16
16
  end
17
17
 
18
- def to_json
18
+ def to_json(options={})
19
19
  to_hash.to_json
20
20
  end
21
21
 
@@ -13,7 +13,7 @@ module Tire
13
13
  end
14
14
  end
15
15
 
16
- def to_json
16
+ def to_json(options={})
17
17
  to_hash.to_json
18
18
  end
19
19
 
@@ -0,0 +1,40 @@
1
+ module Tire
2
+ module Search
3
+ class Query
4
+
5
+ def match(field, value, options={})
6
+ if @value.empty?
7
+ @value = MatchQuery.new(field, value, options).to_hash
8
+ else
9
+ MatchQuery.add(self, field, value, options)
10
+ end
11
+ @value
12
+ end
13
+ end
14
+
15
+ class MatchQuery
16
+ def initialize(field, value, options={})
17
+ query_options = { :query => value }.merge(options)
18
+
19
+ if field.is_a?(Array)
20
+ @value = { :multi_match => query_options.merge( :fields => field ) }
21
+ else
22
+ @value = { :match => { field => query_options } }
23
+ end
24
+ end
25
+
26
+ def self.add(query, field, value, options={})
27
+ unless query.value[:bool]
28
+ original_value = query.value.dup
29
+ query.value = { :bool => {} }
30
+ (query.value[:bool][:must] ||= []) << original_value
31
+ end
32
+ query.value[:bool][:must] << MatchQuery.new(field, value, options).to_hash
33
+ end
34
+
35
+ def to_hash
36
+ @value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,13 +2,19 @@ module Tire
2
2
  module Search
3
3
 
4
4
  class Query
5
+ attr_accessor :value
6
+
5
7
  def initialize(&block)
6
8
  @value = {}
7
9
  block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
8
10
  end
9
11
 
10
12
  def term(field, value, options={})
11
- query = { field => { :term => value }.update(options) }
13
+ query = if value.is_a?(Hash)
14
+ { field => value.to_hash }
15
+ else
16
+ { field => { :term => value }.update(options) }
17
+ end
12
18
  @value = { :term => query }
13
19
  end
14
20
 
@@ -23,6 +29,7 @@ module Tire
23
29
  end
24
30
 
25
31
  def text(field, value, options={})
32
+ Tire.warn "The 'text' query has been deprecated, please use a 'match' query."
26
33
  query_options = { :query => value }.update(options)
27
34
  @value = { :text => { field => query_options } }
28
35
  @value
@@ -75,8 +82,8 @@ module Tire
75
82
  @value
76
83
  end
77
84
 
78
- def all
79
- @value = { :match_all => {} }
85
+ def all(options = {})
86
+ @value = { :match_all => options }
80
87
  @value
81
88
  end
82
89
 
@@ -84,11 +91,18 @@ module Tire
84
91
  @value = { :ids => { :values => values, :type => type } }
85
92
  end
86
93
 
94
+ def boosting(options={}, &block)
95
+ @boosting ||= BoostingQuery.new(options)
96
+ block.arity < 1 ? @boosting.instance_eval(&block) : block.call(@boosting) if block_given?
97
+ @value[:boosting] = @boosting.to_hash
98
+ @value
99
+ end
100
+
87
101
  def to_hash
88
102
  @value
89
103
  end
90
104
 
91
- def to_json
105
+ def to_json(options={})
92
106
  to_hash.to_json
93
107
  end
94
108
 
@@ -156,7 +170,7 @@ module Tire
156
170
  @value
157
171
  end
158
172
 
159
- def to_json
173
+ def to_json(options={})
160
174
  to_hash.to_json
161
175
  end
162
176
  end
@@ -177,10 +191,32 @@ module Tire
177
191
  @value.update(@options)
178
192
  end
179
193
 
180
- def to_json
194
+ def to_json(options={})
181
195
  to_hash.to_json
182
196
  end
183
197
  end
184
198
 
199
+ class BoostingQuery
200
+ def initialize(options={}, &block)
201
+ @options = options
202
+ @value = {}
203
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
204
+ end
205
+
206
+ def positive(&block)
207
+ (@value[:positive] ||= []) << Query.new(&block).to_hash
208
+ @value
209
+ end
210
+
211
+ def negative(&block)
212
+ (@value[:negative] ||= []) << Query.new(&block).to_hash
213
+ @value
214
+ end
215
+
216
+ def to_hash
217
+ @value.update(@options)
218
+ end
219
+ end
220
+
185
221
  end
186
222
  end
@@ -94,7 +94,7 @@ module Tire
94
94
  end
95
95
 
96
96
  def to_a; results; end; alias :to_ary :to_a
97
- def to_curl; %Q|curl -X GET "#{url}?pretty=true" -d '#{@scroll_id}'|; end
97
+ def to_curl; %Q|curl -X GET '#{url}?pretty' -d '#{@scroll_id}'|; end
98
98
 
99
99
  def __logged(error=nil)
100
100
  if Configuration.logger
@@ -10,7 +10,7 @@ module Tire
10
10
  @hash = { name => options }
11
11
  end
12
12
 
13
- def to_json
13
+ def to_json(options={})
14
14
  to_hash.to_json
15
15
  end
16
16
 
@@ -16,7 +16,7 @@ module Tire
16
16
  @value
17
17
  end
18
18
 
19
- def to_json
19
+ def to_json(options={})
20
20
  @value.to_json
21
21
  end
22
22
  end
data/lib/tire/tasks.rb CHANGED
@@ -3,7 +3,7 @@ require 'benchmark'
3
3
 
4
4
  namespace :tire do
5
5
 
6
- full_comment = <<-DESC.gsub(/ /, '')
6
+ full_comment_import = <<-DESC.gsub(/ /, '')
7
7
  Import data from your model using paginate: rake environment tire:import CLASS='MyModel'.
8
8
 
9
9
  Pass params for the `paginate` method:
@@ -15,7 +15,7 @@ namespace :tire do
15
15
  Set target index name:
16
16
  $ rake environment tire:import CLASS='Article' INDEX='articles-new'
17
17
  DESC
18
- desc full_comment
18
+ desc full_comment_import
19
19
  task :import do |t|
20
20
 
21
21
  def elapsed_to_human(elapsed)
@@ -24,18 +24,18 @@ namespace :tire do
24
24
 
25
25
  case elapsed
26
26
  when 0..59
27
- "#{sprintf("%1.5f", elapsed)} seconds"
27
+ "#{sprintf("%1.2f", elapsed)} seconds"
28
28
  when 60..hour-1
29
- "#{elapsed/60} minutes and #{elapsed % 60} seconds"
29
+ "#{(elapsed/60).floor} minutes and #{(elapsed % 60).floor} seconds"
30
30
  when hour..day
31
- "#{elapsed/hour} hours and #{elapsed % hour} minutes"
31
+ "#{(elapsed/hour).floor} hours and #{(elapsed/60 % hour).floor} minutes"
32
32
  else
33
- "#{elapsed/hour} hours"
33
+ "#{(elapsed/hour).round} hours"
34
34
  end
35
35
  end
36
36
 
37
37
  if ENV['CLASS'].to_s == ''
38
- puts '='*90, 'USAGE', '='*90, full_comment, ""
38
+ puts '='*90, 'USAGE', '='*90, full_comment_import, ""
39
39
  exit(1)
40
40
  end
41
41
 
@@ -52,16 +52,19 @@ namespace :tire do
52
52
  end
53
53
 
54
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)
55
+ mapping = MultiJson.encode(klass.tire.mapping_to_hash, :pretty => Tire::Configuration.pretty)
57
56
  puts "[IMPORT] Creating index '#{index.name}' with mapping:", mapping
58
- index.create :mappings => klass.tire.mapping_to_hash, :settings => klass.tire.settings
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
59
62
  end
60
63
 
61
64
  STDOUT.sync = true
62
65
  puts "[IMPORT] Starting import for the '#{ENV['CLASS']}' class"
63
66
  tty_cols = 80
64
- total = klass.all.count rescue nil
67
+ total = klass.count rescue nil
65
68
  offset = (total.to_s.size*2)+8
66
69
  done = 0
67
70
 
@@ -101,7 +104,7 @@ namespace :tire do
101
104
 
102
105
  namespace :index do
103
106
 
104
- full_comment = <<-DESC.gsub(/ /, '')
107
+ full_comment_drop = <<-DESC.gsub(/ /, '')
105
108
  Delete indices passed in the INDEX environment variable; separate multiple indices by comma.
106
109
 
107
110
  Pass name of a single index to drop in the INDEX environmnet variable:
@@ -111,12 +114,12 @@ namespace :tire do
111
114
  $ rake environment tire:index:drop INDICES=articles-2011-01,articles-2011-02
112
115
 
113
116
  DESC
114
- desc full_comment
117
+ desc full_comment_drop
115
118
  task :drop do
116
119
  index_names = (ENV['INDEX'] || ENV['INDICES']).to_s.split(/,\s*/)
117
120
 
118
121
  if index_names.empty?
119
- puts '='*90, 'USAGE', '='*90, full_comment, ""
122
+ puts '='*90, 'USAGE', '='*90, full_comment_drop, ""
120
123
  exit(1)
121
124
  end
122
125