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
data/.gitignore CHANGED
@@ -11,4 +11,4 @@ examples/*.html
11
11
  *.log
12
12
  .rvmrc
13
13
  .rbenv-version
14
- tags
14
+ tmp/
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/README.markdown CHANGED
@@ -247,7 +247,7 @@ The best thing about `boolean` queries is that we can easily save these partial
247
247
  to mix and reuse them later. So, we may define a query for the _tags_ property:
248
248
 
249
249
  ```ruby
250
- tags_query = lambda do
250
+ tags_query = lambda do |boolean|
251
251
  boolean.should { string 'tags:ruby' }
252
252
  boolean.should { string 'tags:java' }
253
253
  end
@@ -256,7 +256,7 @@ to mix and reuse them later. So, we may define a query for the _tags_ property:
256
256
  And a query for the _published_on_ property:
257
257
 
258
258
  ```ruby
259
- published_on_query = lambda do
259
+ published_on_query = lambda do |boolean|
260
260
  boolean.must { string 'published_on:[2011-01-01 TO 2011-01-02]' }
261
261
  end
262
262
  ```
@@ -105,7 +105,7 @@ puts
105
105
  say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
106
106
  puts '-'*80, ''; sleep 1
107
107
 
108
- gem 'tire'
108
+ gem 'tire', :git => 'git://github.com/karmi/tire.git'
109
109
  gem 'will_paginate', '~> 3.0'
110
110
 
111
111
  git :add => '.'
@@ -137,7 +137,7 @@ say_status "Database", "Seeding the database with data...", :yellow
137
137
  puts '-'*80, ''; sleep 0.25
138
138
 
139
139
  run "rm -f db/seeds.rb"
140
- file 'db/seeds.rb', <<-CODE
140
+ file 'db/seeds.rb', %q{
141
141
  contents = [
142
142
  'Lorem ipsum dolor sit amet.',
143
143
  'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
@@ -149,11 +149,23 @@ contents = [
149
149
  puts "Deleting all articles..."
150
150
  Article.delete_all
151
151
 
152
- puts "Creating articles..."
153
- %w[ One Two Three Four Five ].each_with_index do |title, i|
154
- Article.create :title => title, :content => contents[i], :published_on => i.days.ago.utc
152
+ unless ENV['COUNT']
153
+
154
+ puts "Creating articles..."
155
+ %w[ One Two Three Four Five ].each_with_index do |title, i|
156
+ Article.create :title => title, :content => contents[i], :published_on => i.days.ago.utc
157
+ end
158
+
159
+ else
160
+
161
+ puts "Creating 10,000 articles..."
162
+ (1..ENV['COUNT'].to_i).each_with_index do |title, i|
163
+ Article.create :title => "Title #{title}", :content => 'Lorem', :published_on => i.days.ago.utc
164
+ print '.'
165
+ end
166
+
155
167
  end
156
- CODE
168
+ }
157
169
 
158
170
  rake "db:seed"
159
171
 
@@ -169,6 +181,8 @@ file 'app/models/article.rb', <<-CODE
169
181
  class Article < ActiveRecord::Base
170
182
  include Tire::Model::Search
171
183
  include Tire::Model::Callbacks
184
+
185
+ attr_accessible :title, :content, :published_on
172
186
  end
173
187
  CODE
174
188
 
data/lib/tire.rb CHANGED
@@ -20,12 +20,14 @@ require 'tire/http/response'
20
20
  require 'tire/http/client'
21
21
  require 'tire/search'
22
22
  require 'tire/search/query'
23
+ require 'tire/search/queries/match'
23
24
  require 'tire/search/sort'
24
25
  require 'tire/search/facet'
25
26
  require 'tire/search/filter'
26
27
  require 'tire/search/highlight'
27
28
  require 'tire/search/scan'
28
29
  require 'tire/search/script_field'
30
+ require 'tire/multi_search'
29
31
  require 'tire/results/pagination'
30
32
  require 'tire/results/collection'
31
33
  require 'tire/results/item'
data/lib/tire/alias.rb CHANGED
@@ -209,7 +209,7 @@ module Tire
209
209
 
210
210
  if Configuration.logger.level.to_s == 'debug'
211
211
  body = if @response
212
- defined?(Yajl) ? Yajl::Encoder.encode(@response.body, :pretty => true) : MultiJson.encode(@response.body)
212
+ MultiJson.encode(@response.body, :pretty => Configuration.pretty)
213
213
  else
214
214
  error.message rescue ''
215
215
  end
@@ -19,6 +19,14 @@ module Tire
19
19
  @logger || nil
20
20
  end
21
21
 
22
+ def self.pretty(value=nil, options={})
23
+ if value === false
24
+ return @pretty = false
25
+ else
26
+ @pretty.nil? ? true : @pretty
27
+ end
28
+ end
29
+
22
30
  def self.reset(*properties)
23
31
  reset_variables = properties.empty? ? instance_variables : instance_variables.map { |p| p.to_s} & \
24
32
  properties.map { |p| "@#{p}" }
data/lib/tire/dsl.rb CHANGED
@@ -18,8 +18,11 @@ module Tire
18
18
  options
19
19
  else raise ArgumentError, "Please pass a Ruby Hash or String with JSON"
20
20
  end
21
-
22
- Search::Search.new(indices, :payload => payload)
21
+ unless options.empty?
22
+ Search::Search.new(indices, :payload => payload)
23
+ else
24
+ Search::Search.new(indices)
25
+ end
23
26
  end
24
27
  rescue Exception => error
25
28
  STDERR.puts "[REQUEST FAILED] #{error.class} #{error.message rescue nil}\n"
@@ -27,6 +30,70 @@ module Tire
27
30
  ensure
28
31
  end
29
32
 
33
+ # Build and perform a [multi-search](http://elasticsearch.org/guide/reference/api/multi-search.html)
34
+ # request.
35
+ #
36
+ # s = Tire.search 'clients' do
37
+ # search :names do
38
+ # query { match :name, 'carpenter' }
39
+ # end
40
+ # search :counts, search_type: 'count' do
41
+ # query { match [:name, :street, :occupation], 'carpenter' }
42
+ # end
43
+ # search :vip, index: 'clients-vip' do
44
+ # query { string "last_name:carpenter" }
45
+ # end
46
+ # search() { query {all} }
47
+ # end
48
+ #
49
+ # The DSL allows you to perform multiple searches and get corresponding results
50
+ # in a single HTTP request, saving network roundtrips.
51
+ #
52
+ # Use the `search` method in the block to define a search request with the
53
+ # regular Tire's DSL (`query`, `facet`, etc).
54
+ #
55
+ # You can pass options such as `search_type`, `routing`, etc.,
56
+ # as well as a different `index` and/or `type` to individual searches.
57
+ #
58
+ # You can give single searches names, to be able to refer to them later.
59
+ #
60
+ # The results are returned as an enumerable collection of {Tire::Results::Collection} instances.
61
+ #
62
+ # You may simply iterate over them with `each`:
63
+ #
64
+ # s.results.each do |results|
65
+ # puts results.map(&:name)
66
+ # end
67
+ #
68
+ # To iterate over named results, use the `each_pair` method:
69
+ #
70
+ # s.results.each_pair do |name,results|
71
+ # puts "Search #{name} got #{results.size} results"
72
+ # end
73
+ #
74
+ # You can get a specific named result:
75
+ #
76
+ # search.results[:vip]
77
+ #
78
+ # You can mix & match named and non-named searches in the definition; the non-named
79
+ # searches will be zero-based numbered, so you can refer to them:
80
+ #
81
+ # search.results[3] # Results for the last query
82
+ #
83
+ # To log the multi-search request, use the standard `to_curl` method (or set up a logger):
84
+ #
85
+ # print search.to_curl
86
+ #
87
+ def multi_search(indices=nil, options={}, &block)
88
+ Search::Multi::Search.new(indices, options, &block)
89
+ rescue Exception => error
90
+ STDERR.puts "[REQUEST FAILED] #{error.class} #{error.message rescue nil}\n"
91
+ raise
92
+ ensure
93
+ end
94
+ alias :multisearch :multi_search
95
+ alias :msearch :multi_search
96
+
30
97
  def index(name, &block)
31
98
  Index.new(name, &block)
32
99
  end
data/lib/tire/index.rb CHANGED
@@ -36,7 +36,7 @@ module Tire
36
36
  @response.success? ? @response : false
37
37
 
38
38
  ensure
39
- curl = %Q|curl -X POST #{url} -d '#{MultiJson.encode(options)}'|
39
+ curl = %Q|curl -X POST #{url} -d '#{MultiJson.encode(options, :pretty => Configuration.pretty)}'|
40
40
  logged('CREATE', curl)
41
41
  end
42
42
 
@@ -64,22 +64,27 @@ module Tire
64
64
 
65
65
  def store(*args)
66
66
  document, options = args
67
- type = get_type_from_document(document)
68
-
69
- if options
70
- percolate = options[:percolate]
71
- percolate = "*" if percolate === true
72
- end
73
67
 
74
68
  id = get_id_from_document(document)
69
+ type = get_type_from_document(document)
75
70
  document = convert_document_to_json(document)
76
71
 
77
- url = id ? "#{self.url}/#{type}/#{id}" : "#{self.url}/#{type}/"
78
- url += "?percolate=#{percolate}" if percolate
72
+ options ||= {}
73
+ params = {}
74
+
75
+ if options[:percolate]
76
+ params[:percolate] = options[:percolate]
77
+ params[:percolate] = "*" if params[:percolate] === true
78
+ end
79
+
80
+ params[:parent] = options[:parent] if options[:parent]
81
+
82
+ params_encoded = params.empty? ? '' : "?#{params.to_param}"
83
+
84
+ url = id ? "#{self.url}/#{type}/#{id}#{params_encoded}" : "#{self.url}/#{type}/#{params_encoded}"
79
85
 
80
86
  @response = Configuration.client.post url, document
81
87
  MultiJson.decode(@response.body)
82
-
83
88
  ensure
84
89
  curl = %Q|curl -X POST "#{url}" -d '#{document}'|
85
90
  logged([type, id].join('/'), curl)
@@ -117,7 +122,7 @@ module Tire
117
122
  end
118
123
 
119
124
  ensure
120
- curl = %Q|curl -X POST "#{url}/_bulk" -d '{... data omitted ...}'|
125
+ curl = %Q|curl -X POST "#{url}/_bulk" --data-binary '{... data omitted ...}'|
121
126
  logged('BULK', curl)
122
127
  end
123
128
  end
@@ -184,20 +189,28 @@ module Tire
184
189
  logged(id, curl)
185
190
  end
186
191
 
187
- def retrieve(type, id)
192
+ def retrieve(type, id, options={})
188
193
  raise ArgumentError, "Please pass a document ID" unless id
189
194
 
190
195
  type = Utils.escape(type)
191
196
  url = "#{self.url}/#{type}/#{id}"
192
- @response = Configuration.client.get url
197
+
198
+ params = {}
199
+ params[:routing] = options[:routing] if options[:routing]
200
+ params[:fields] = options[:fields] if options[:fields]
201
+ params[:preference] = options[:preference] if options[:preference]
202
+ params_encoded = params.empty? ? '' : "?#{params.to_param}"
203
+
204
+ @response = Configuration.client.get "#{url}#{params_encoded}"
193
205
 
194
206
  h = MultiJson.decode(@response.body)
195
- if Configuration.wrapper == Hash then h
207
+ wrapper = options[:wrapper] || Configuration.wrapper
208
+ if wrapper == Hash then h
196
209
  else
197
210
  return nil if h['exists'] == false
198
211
  document = h['_source'] || h['fields'] || {}
199
212
  document.update('id' => h['_id'], '_type' => h['_type'], '_index' => h['_index'], '_version' => h['_version'])
200
- Configuration.wrapper.new(document)
213
+ wrapper.new(document)
201
214
  end
202
215
 
203
216
  ensure
@@ -217,7 +230,7 @@ module Tire
217
230
  MultiJson.decode(@response.body)
218
231
 
219
232
  ensure
220
- curl = %Q|curl -X POST "#{url}" -d '#{MultiJson.encode(payload)}'|
233
+ curl = %Q|curl -X POST "#{url}" -d '#{MultiJson.encode(payload, :pretty => Configuration.pretty)}'|
221
234
  logged(id, curl)
222
235
  end
223
236
 
@@ -266,7 +279,7 @@ module Tire
266
279
  MultiJson.decode(@response.body)['ok']
267
280
 
268
281
  ensure
269
- curl = %Q|curl -X PUT "#{Configuration.url}/_percolator/#{@name}/#{name}?pretty=1" -d '#{MultiJson.encode(options)}'|
282
+ curl = %Q|curl -X PUT "#{Configuration.url}/_percolator/#{@name}/#{name}?pretty" -d '#{MultiJson.encode(options, :pretty => Configuration.pretty)}'|
270
283
  logged('_percolator', curl)
271
284
  end
272
285
 
@@ -294,7 +307,7 @@ module Tire
294
307
  MultiJson.decode(@response.body)['matches']
295
308
 
296
309
  ensure
297
- curl = %Q|curl -X GET "#{url}/#{type}/_percolate?pretty=1" -d '#{payload.to_json}'|
310
+ curl = %Q|curl -X GET "#{url}/#{type}/_percolate?pretty" -d '#{MultiJson.encode(payload, :pretty => Configuration.pretty)}'|
298
311
  logged('_percolate', curl)
299
312
  end
300
313
 
@@ -308,9 +321,9 @@ module Tire
308
321
 
309
322
  if Configuration.logger.level.to_s == 'debug'
310
323
  body = if @response
311
- defined?(Yajl) ? Yajl::Encoder.encode(@response.body, :pretty => true) : MultiJson.encode(@response.body)
324
+ MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty)
312
325
  else
313
- error.message rescue ''
326
+ MultiJson.encode( MultiJson.load(error.message), :pretty => Configuration.pretty) rescue ''
314
327
  end
315
328
  else
316
329
  body = ''
@@ -105,11 +105,17 @@ module Tire
105
105
  #
106
106
  def create_elasticsearch_index
107
107
  unless index.exists?
108
- index.create :mappings => mapping_to_hash, :settings => settings
108
+ new_index = index
109
+ unless result = new_index.create(:mappings => mapping_to_hash, :settings => settings)
110
+ STDERR.puts "[ERROR] There has been an error when creating the index -- elasticsearch returned:",
111
+ new_index.response
112
+ result
113
+ end
109
114
  end
110
115
  rescue Errno::ECONNREFUSED => e
111
116
  STDERR.puts "Skipping index creation, cannot connect to ElasticSearch",
112
117
  "(The original exception was: #{e.inspect})"
118
+ false
113
119
  end
114
120
 
115
121
  def mapping_options
@@ -11,7 +11,7 @@ module Tire
11
11
  #
12
12
  # class Article
13
13
  # include Tire::Model::Persistence
14
- #
14
+ #
15
15
  # property :title
16
16
  # end
17
17
  #
@@ -54,11 +54,14 @@ module Tire
54
54
  args.last.update(:wrapper => self, :version => true) if args.last.is_a? Hash
55
55
  args << { :wrapper => self, :version => true } unless args.any? { |a| a.is_a? Hash }
56
56
 
57
- self.__search_without_persistence(*args, &block)
57
+ self.tire.search(*args, &block)
58
58
  end
59
59
 
60
- def self.__search_without_persistence(*args, &block)
61
- self.tire.search(*args, &block)
60
+ def self.multi_search(*args, &block)
61
+ args.last.update(:wrapper => self, :version => true) if args.last.is_a? Hash
62
+ args << { :wrapper => self, :version => true } unless args.any? { |a| a.is_a? Hash }
63
+
64
+ self.tire.multi_search(*args, &block)
62
65
  end
63
66
 
64
67
  end
@@ -46,7 +46,7 @@ module Tire
46
46
 
47
47
  # Save property default value (when relevant):
48
48
  unless (default_value = options.delete(:default)).nil?
49
- property_defaults[name.to_sym] = default_value
49
+ property_defaults[name.to_sym] = default_value.respond_to?(:call) ? default_value.call : default_value
50
50
  end
51
51
 
52
52
  # Save property casting (when relevant):
@@ -11,12 +11,10 @@ module Tire
11
11
 
12
12
  def find *args
13
13
  # TODO: Options like `sort`
14
- old_wrapper = Tire::Configuration.wrapper
15
- Tire::Configuration.wrapper self
16
14
  options = args.pop if args.last.is_a?(Hash)
17
15
  args.flatten!
18
16
  if args.size > 1
19
- Tire::Search::Search.new(index.name) do |search|
17
+ Tire::Search::Search.new(index.name, :wrapper => self) do |search|
20
18
  search.query do |query|
21
19
  query.ids(args, document_type)
22
20
  end
@@ -25,35 +23,25 @@ module Tire
25
23
  else
26
24
  case args = args.pop
27
25
  when Fixnum, String
28
- index.retrieve document_type, args
26
+ index.retrieve document_type, args, :wrapper => self
29
27
  when :all, :first
30
28
  send(args)
31
29
  else
32
30
  raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument"
33
31
  end
34
32
  end
35
- ensure
36
- Tire::Configuration.wrapper old_wrapper
37
33
  end
38
34
 
39
35
  def all
40
36
  # TODO: Options like `sort`; Possibly `filters`
41
- old_wrapper = Tire::Configuration.wrapper
42
- Tire::Configuration.wrapper self
43
- s = Tire::Search::Search.new(index.name).query { all }
37
+ s = Tire::Search::Search.new(index.name, :type => document_type, :wrapper => self).query { all }
44
38
  s.version(true).results
45
- ensure
46
- Tire::Configuration.wrapper old_wrapper
47
39
  end
48
40
 
49
41
  def first
50
42
  # TODO: Options like `sort`; Possibly `filters`
51
- old_wrapper = Tire::Configuration.wrapper
52
- Tire::Configuration.wrapper self
53
- s = Tire::Search::Search.new(index.name).query { all }.size(1)
43
+ s = Tire::Search::Search.new(index.name, :type => document_type, :wrapper => self).query { all }.size(1)
54
44
  s.version(true).results.first
55
- ensure
56
- Tire::Configuration.wrapper old_wrapper
57
45
  end
58
46
 
59
47
  end
@@ -72,20 +72,26 @@ module Tire
72
72
  options ||= {}
73
73
  end
74
74
 
75
- sort = Array( options[:order] || options[:sort] )
76
75
  options = default_options.update(options)
76
+ sort = Array( options.delete(:order) || options.delete(:sort) )
77
77
 
78
78
  s = Tire::Search::Search.new(options.delete(:index), options)
79
- s.size( options[:per_page].to_i ) if options[:per_page]
80
- s.from( options[:page].to_i <= 1 ? 0 : (options[:per_page].to_i * (options[:page].to_i-1)) ) if options[:page] && options[:per_page]
79
+
80
+ page = options.delete(:page)
81
+ per_page = options.delete(:per_page) || Tire::Results::Pagination::default_per_page
82
+
83
+ s.size( per_page.to_i ) if per_page
84
+ s.from( page.to_i <= 1 ? 0 : (per_page.to_i * (page.to_i-1)) ) if page && per_page
85
+
81
86
  s.sort do
82
87
  sort.each do |t|
83
- field_name, direction = t.split(' ')
88
+ field_name, direction = t.split(':')
84
89
  by field_name, direction
85
90
  end
86
91
  end unless sort.empty?
87
92
 
88
- if version = options.delete(:version); s.version(version); end
93
+ version = options.delete(:version)
94
+ s.version(version) if version
89
95
 
90
96
  if block_given?
91
97
  block.arity < 1 ? s.instance_eval(&block) : block.call(s)
@@ -99,6 +105,12 @@ module Tire
99
105
  s.results
100
106
  end
101
107
 
108
+ def multi_search(options={}, &block)
109
+ default_options = {:type => document_type}
110
+ options = default_options.update(options)
111
+ Tire::Search::Multi::Search.new(index.name, options, &block).results
112
+ end
113
+
102
114
  # Returns a Tire::Index instance for this model.
103
115
  #
104
116
  # Example usage: `Article.index.refresh`.
@@ -154,7 +166,7 @@ module Tire
154
166
  # declared in the mapping are serialized.
155
167
  #
156
168
  # For properties declared with the `:as` option, the passed String or Proc
157
- # is evaluated in the instance context.
169
+ # is evaluated in the instance context. Other objects are indexed "as is".
158
170
  #
159
171
  def to_indexed_json
160
172
  if instance.class.tire.mapping.empty?
@@ -172,7 +184,9 @@ module Tire
172
184
  hash[key] = instance.instance_eval(options[:as])
173
185
  when Proc
174
186
  hash[key] = instance.instance_eval(&options[:as])
175
- end
187
+ else
188
+ hash[key] = options[:as]
189
+ end if options[:as]
176
190
  end
177
191
 
178
192
  hash.to_json
@@ -300,7 +314,6 @@ module Tire
300
314
  Results::Item.send :include, Loader
301
315
  end
302
316
 
303
-
304
317
  end
305
318
 
306
319
  end