tire 0.5.8 → 0.6.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.
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