tire 0.5.8 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -471,6 +471,10 @@ In this case, just wrap the `mapping` method in a `settings` one, passing it the
471
471
  end
472
472
  ```
473
473
 
474
+ Note, that the index will be created with settings and mappings only when it doesn't exist yet.
475
+ To re-create the index with correct configuration, delete it first: `URL.index.delete` and
476
+ create it afterwards: `URL.create_elasticsearch_index`.
477
+
474
478
  It may well be reasonable to wrap the index creation logic declared with `Tire.index('urls').create`
475
479
  in a class method of your model, in a module method, etc, to have better control on index creation when
476
480
  bootstrapping the application with Rake tasks or when setting up the test suite.
@@ -573,6 +577,24 @@ control on how the documents are added to or removed from the index:
573
577
  end
574
578
  ```
575
579
 
580
+ Sometimes, you might want to have complete control about the indexing process. In such situations,
581
+ just drop down one layer and use the `Tire::Index#store` and `Tire::Index#remove` methods directly:
582
+
583
+ ```ruby
584
+ class Article < ActiveRecord::Base
585
+ acts_as_paranoid
586
+ include Tire::Model::Search
587
+
588
+ after_save do
589
+ if deleted_at.nil?
590
+ self.index.store self
591
+ else
592
+ self.index.remove self
593
+ end
594
+ end
595
+ end
596
+ ```
597
+
576
598
  When you're integrating _Tire_ with ActiveRecord models, you should use the `after_commit`
577
599
  and `after_rollback` hooks to keep the index in sync with your database.
578
600
 
@@ -662,11 +684,11 @@ Are we saying you have to fiddle with this thing in a `rails console` or silly R
662
684
  Just call the included _Rake_ task on the command line:
663
685
 
664
686
  ```bash
665
- $ rake environment tire:import CLASS='Article'
687
+ $ rake environment tire:import:all
666
688
  ```
667
689
 
668
- You can also force-import the data by deleting the index first (and creating it with mapping
669
- provided by the `mapping` block in your model):
690
+ You can also force-import the data by deleting the index first (and creating it with
691
+ correct settings and/or mappings provided by the `mapping` block in your model):
670
692
 
671
693
  ```bash
672
694
  $ rake environment tire:import CLASS='Article' FORCE=true
data/lib/tire.rb CHANGED
@@ -23,6 +23,7 @@ require 'tire/http/client'
23
23
  require 'tire/search'
24
24
  require 'tire/search/query'
25
25
  require 'tire/search/queries/match'
26
+ require 'tire/search/queries/custom_filters_score'
26
27
  require 'tire/search/sort'
27
28
  require 'tire/search/facet'
28
29
  require 'tire/search/filter'
data/lib/tire/dsl.rb CHANGED
@@ -5,26 +5,35 @@ module Tire
5
5
  Configuration.class_eval(&block)
6
6
  end
7
7
 
8
- def search(indices=nil, payload={}, &block)
8
+ def search(indices=nil, params={}, &block)
9
9
  if block_given?
10
- Search::Search.new(indices, payload, &block)
10
+ Search::Search.new(indices, params, &block)
11
11
  else
12
- raise ArgumentError, "Please pass a Ruby Hash or an object with `to_hash` method, not #{payload.class}" \
13
- unless payload.respond_to?(:to_hash)
12
+ raise ArgumentError, "Please pass a Ruby Hash or an object with `to_hash` method, not #{params.class}" \
13
+ unless params.respond_to?(:to_hash)
14
+
15
+ params = params.to_hash
16
+
17
+ if payload = params.delete(:payload)
18
+ options = params
19
+ else
20
+ payload = params
21
+ end
14
22
 
15
23
  # Extract URL parameters from payload
16
24
  #
17
25
  search_params = %w| search_type routing scroll from size timeout |
18
26
 
19
- options = search_params.inject({}) do |sum,item|
27
+ search_options = search_params.inject({}) do |sum,item|
20
28
  if param = (payload.delete(item) || payload.delete(item.to_sym))
21
29
  sum[item.to_sym] = param
22
30
  end
23
31
  sum
24
32
  end
25
33
 
26
- options.update(:payload => payload) unless payload.empty?
27
- Search::Search.new(indices, options)
34
+ search_options.update(options) if options && !options.empty?
35
+ search_options.update(:payload => payload) unless payload.empty?
36
+ Search::Search.new(indices, search_options)
28
37
  end
29
38
  end
30
39
 
@@ -47,6 +47,10 @@ module Tire
47
47
  Response.new e.http_body, e.http_code
48
48
  end
49
49
 
50
+ def self.__host_unreachable_exceptions
51
+ [Errno::ECONNREFUSED, ::RestClient::ServerBrokeConnection, ::RestClient::RequestTimeout]
52
+ end
53
+
50
54
  private
51
55
 
52
56
  def self.perform(response)
@@ -56,6 +56,10 @@ module Tire
56
56
  Response.new client.body_str, client.response_code
57
57
  end
58
58
 
59
+ def self.__host_unreachable_exceptions
60
+ [::Curl::Err::HostResolutionError, ::Curl::Err::ConnectionFailedError]
61
+ end
62
+
59
63
  end
60
64
 
61
65
  end
@@ -13,14 +13,14 @@ require 'faraday'
13
13
  # require 'tire/http/clients/faraday'
14
14
  #
15
15
  # Tire.configure do |config|
16
- #
16
+ #
17
17
  # # Unless specified, tire will use Faraday.default_adapter and no middleware
18
18
  # Tire::HTTP::Client::Faraday.faraday_middleware = Proc.new do |builder|
19
19
  # builder.adapter :typhoeus
20
20
  # end
21
- #
21
+ #
22
22
  # config.client(Tire::HTTP::Client::Faraday)
23
- #
23
+ #
24
24
  # end
25
25
  #
26
26
  #
@@ -58,6 +58,10 @@ module Tire
58
58
  request(:head, url)
59
59
  end
60
60
 
61
+ def __host_unreachable_exceptions
62
+ [::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError]
63
+ end
64
+
61
65
  private
62
66
  def request(method, url, data = nil)
63
67
  conn = ::Faraday.new( &(faraday_middleware || DEFAULT_MIDDLEWARE) )
data/lib/tire/index.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  module Tire
2
2
  class Index
3
3
 
4
+ SUPPORTED_META_PARAMS_FOR_BULK = [:_routing, :_ttl, :_version, :_version_type, :_percolate, :_parent, :_timestamp]
5
+
4
6
  attr_reader :name, :response
5
7
 
6
8
  def initialize(name, &block)
@@ -135,6 +137,7 @@ module Tire
135
137
  params[:parent] = options[:parent] if options[:parent]
136
138
  params[:routing] = options[:routing] if options[:routing]
137
139
  params[:replication] = options[:replication] if options[:replication]
140
+ params[:version] = options[:version] if options[:version]
138
141
 
139
142
  params_encoded = params.empty? ? '' : "?#{params.to_param}"
140
143
 
@@ -180,15 +183,24 @@ module Tire
180
183
 
181
184
  header = { action.to_sym => { :_index => name, :_type => type, :_id => id } }
182
185
 
183
- if document.respond_to?(:to_hash) && hash = document.to_hash
184
- meta = {}
185
- meta[:_version] = hash.delete(:_version)
186
- meta[:_routing] = hash.delete(:_routing)
187
- meta[:_percolate] = hash.delete(:_percolate)
188
- meta[:_parent] = hash.delete(:_parent)
189
- meta[:_timestamp] = hash.delete(:_timestamp)
190
- meta[:_ttl] = hash.delete(:_ttl)
191
- meta = meta.reject { |name,value| !value || value.empty? }
186
+ if document.respond_to?(:to_hash) && doc_hash = document.to_hash
187
+ meta = doc_hash.select do |k,v|
188
+ [ :_parent,
189
+ :_percolate,
190
+ :_routing,
191
+ :_timestamp,
192
+ :_ttl,
193
+ :_version,
194
+ :_version_type].include?(k)
195
+ end
196
+ # Normalize Ruby 1.8 and Ruby 1.9 Hash#select behaviour
197
+ meta = Hash[meta] unless meta.is_a?(Hash)
198
+
199
+ # meta = SUPPORTED_META_PARAMS_FOR_BULK.inject({}) { |hash, param|
200
+ # value = doc_hash.delete(param)
201
+ # hash[param] = value unless !value || value.empty?
202
+ # hash
203
+ # }
192
204
  header[action.to_sym].update(meta)
193
205
  end
194
206
 
@@ -223,7 +235,8 @@ module Tire
223
235
  end
224
236
 
225
237
  ensure
226
- curl = %Q|curl -X POST "#{url}/_bulk" --data-binary '{... data omitted ...}'|
238
+ data = Configuration.logger && Configuration.logger.level.to_s == 'verbose' ? payload.join("\n") : '... data omitted ...'
239
+ curl = %Q|curl -X POST "#{url}/_bulk" --data-binary '#{data}'|
227
240
  logged('_bulk', curl)
228
241
  end
229
242
 
@@ -8,6 +8,8 @@ module Tire
8
8
  # Two dedicated strategies for popular pagination libraries are also provided: WillPaginate and Kaminari.
9
9
  # These could be used in situations where your model is neither ActiveRecord nor Mongoid based.
10
10
  #
11
+ # You can implement your own custom strategy and pass it via the `:strategy` option.
12
+ #
11
13
  # Note, that it's always possible to use the `Tire::Index#import` method directly.
12
14
  #
13
15
  # @note See `Tire::Import::Strategy`.
@@ -22,10 +24,12 @@ module Tire
22
24
  end
23
25
 
24
26
  # Importing strategies for common persistence frameworks (ActiveModel, Mongoid), as well as
25
- # pagination libraries (WillPaginate, Kaminari).
27
+ # pagination libraries (WillPaginate, Kaminari), or a custom strategy.
26
28
  #
27
29
  module Strategy
28
30
  def self.from_class(klass, options={})
31
+ return const_get(options[:strategy]).new(klass, options) if options[:strategy]
32
+
29
33
  case
30
34
  when defined?(::ActiveRecord) && klass.ancestors.include?(::ActiveRecord::Base)
31
35
  ActiveRecord.new klass, options
@@ -62,10 +66,15 @@ module Tire
62
66
  class Mongoid
63
67
  include Base
64
68
  def import &block
65
- 0.step(klass.count, options[:per_page]) do |offset|
66
- items = klass.limit(options[:per_page]).skip(offset)
67
- index.import items.to_a, options, &block
69
+ items = []
70
+ klass.all.each do |item|
71
+ items << item
72
+ if items.length % options[:per_page] == 0
73
+ index.import items, options, &block
74
+ items = []
75
+ end
68
76
  end
77
+ index.import items, options, &block unless items.empty?
69
78
  self
70
79
  end
71
80
  end
@@ -112,7 +112,8 @@ module Tire
112
112
  result
113
113
  end
114
114
  end
115
- rescue Errno::ECONNREFUSED => e
115
+
116
+ rescue *Tire::Configuration.client.__host_unreachable_exceptions => e
116
117
  STDERR.puts "Skipping index creation, cannot connect to Elasticsearch",
117
118
  "(The original exception was: #{e.inspect})"
118
119
  false
@@ -58,7 +58,7 @@ module Tire
58
58
  alias :[] :slice
59
59
 
60
60
  def to_ary
61
- self
61
+ results
62
62
  end
63
63
 
64
64
  def as_json(options=nil)
@@ -106,12 +106,17 @@ module Tire
106
106
  hits.map do |h|
107
107
  document = {}
108
108
 
109
- # Update the document with content and ID
110
- document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
111
- document.update( {'id' => h['_id']} )
109
+ # Update the document with fields and/or source
110
+ document.update h['_source'] if h['_source']
111
+ document.update __parse_fields__(h['fields']) if h['fields']
112
+
113
+ # Set document ID
114
+ document['id'] = h['_id']
112
115
 
113
116
  # Update the document with meta information
114
- ['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
117
+ ['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each do |key|
118
+ document.update key => h[key]
119
+ end
115
120
 
116
121
  # Return an instance of the "wrapper" class
117
122
  @wrapper.new(document)
@@ -34,6 +34,9 @@ module Tire
34
34
  @attributes[key.to_sym]
35
35
  end
36
36
 
37
+ alias :read_attribute_for_serialization :[]
38
+
39
+
37
40
  def id
38
41
  @attributes[:_id] || @attributes[:id]
39
42
  end
@@ -1,11 +1,6 @@
1
1
  module Tire
2
2
  module Search
3
3
 
4
- #--
5
- # TODO: Implement all elastic search facets (geo, histogram, range, etc)
6
- # http://elasticsearch.org/guide/reference/api/search/facets/
7
- #++
8
-
9
4
  class Facet
10
5
 
11
6
  def initialize(name, options={}, &block)
@@ -48,6 +43,11 @@ module Tire
48
43
  self
49
44
  end
50
45
 
46
+ def geo_distance(field, point, ranges=[], options={})
47
+ @value[:geo_distance] = { field => point, :ranges => ranges }.update(options)
48
+ self
49
+ end
50
+
51
51
  def terms_stats(key_field, value_field, options={})
52
52
  @value[:terms_stats] = {:key_field => key_field, :value_field => value_field}.update(options)
53
53
  self
@@ -0,0 +1,128 @@
1
+ module Tire
2
+ module Search
3
+
4
+ # Custom Filters Score
5
+ # ==============
6
+ #
7
+ # Author: Jerry Luk <jerryluk@gmail.com>
8
+ #
9
+ #
10
+ # Adds support for "custom_filters_score" queries in Tire DSL.
11
+ #
12
+ # It hooks into the Query class and inserts the custom_filters_score query types.
13
+ #
14
+ #
15
+ # Usage:
16
+ # ------
17
+ #
18
+ # Require the component:
19
+ #
20
+ # require 'tire/queries/custom_filters_score'
21
+ #
22
+ # Example:
23
+ # -------
24
+ #
25
+ # Tire.search 'articles' do
26
+ # query do
27
+ # custom_filters_score do
28
+ # query { term :title, 'Harry Potter' }
29
+ # filter do
30
+ # filter :match_all
31
+ # boost 1.1
32
+ # end
33
+ # filter do
34
+ # filter :term, :author => 'Rowling',
35
+ # script '2.0'
36
+ # end
37
+ # score_mode 'total'
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # For available options for these queries see:
43
+ #
44
+ # * <http://www.elasticsearch.org/guide/reference/query-dsl/custom-filters-score-query.html>
45
+ #
46
+ #
47
+ class Query
48
+
49
+ def custom_filters_score(&block)
50
+ @custom_filters_score = CustomFiltersScoreQuery.new
51
+ block.arity < 1 ? @custom_filters_score.instance_eval(&block) : block.call(@custom_filters_score) if
52
+ block_given?
53
+ @value[:custom_filters_score] = @custom_filters_score.to_hash
54
+ @value
55
+ end
56
+
57
+ class CustomFiltersScoreQuery
58
+ class CustomFilter
59
+ def initialize(&block)
60
+ @value = {}
61
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
62
+ end
63
+
64
+ def filter(type, *options)
65
+ @value[:filter] = Filter.new(type, *options).to_hash
66
+ @value
67
+ end
68
+
69
+ def boost(value)
70
+ @value[:boost] = value
71
+ @value
72
+ end
73
+
74
+ def script(value)
75
+ @value[:script] = value
76
+ @value
77
+ end
78
+
79
+ def to_hash
80
+ @value
81
+ end
82
+
83
+ def to_json
84
+ to_hash.to_json
85
+ end
86
+ end
87
+
88
+ def initialize(&block)
89
+ @value = {}
90
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
91
+ end
92
+
93
+ def query(options={}, &block)
94
+ @value[:query] = Query.new(&block).to_hash
95
+ @value
96
+ end
97
+
98
+ def filter(&block)
99
+ custom_filter = CustomFilter.new
100
+ block.arity < 1 ? custom_filter.instance_eval(&block) : block.call(custom_filter) if block_given?
101
+ @value[:filters] ||= []
102
+ @value[:filters] << custom_filter.to_hash
103
+ @value
104
+ end
105
+
106
+ def score_mode(value)
107
+ @value[:score_mode] = value
108
+ @value
109
+ end
110
+
111
+ def params(value)
112
+ @value[:params] = value
113
+ @value
114
+ end
115
+
116
+ def to_hash
117
+ @value[:filters] ?
118
+ @value :
119
+ @value.merge(:filters => [CustomFilter.new{ filter(:match_all); boost(1) }.to_hash]) # Needs at least one filter
120
+ end
121
+
122
+ def to_json
123
+ to_hash.to_json
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end