tire 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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