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 +25 -3
- data/lib/tire.rb +1 -0
- data/lib/tire/dsl.rb +16 -7
- data/lib/tire/http/client.rb +4 -0
- data/lib/tire/http/clients/curb.rb +4 -0
- data/lib/tire/http/clients/faraday.rb +7 -3
- data/lib/tire/index.rb +23 -10
- data/lib/tire/model/import.rb +13 -4
- data/lib/tire/model/indexing.rb +2 -1
- data/lib/tire/results/collection.rb +10 -5
- data/lib/tire/results/item.rb +3 -0
- data/lib/tire/search/facet.rb +5 -5
- data/lib/tire/search/queries/custom_filters_score.rb +128 -0
- data/lib/tire/search/query.rb +8 -10
- data/lib/tire/tasks.rb +1 -11
- data/lib/tire/version.rb +13 -7
- data/test/integration/bulk_test.rb +13 -0
- data/test/integration/custom_filters_score_queries_test.rb +105 -0
- data/test/integration/dsl_search_test.rb +32 -0
- data/test/integration/facets_test.rb +47 -0
- data/test/integration/results_test.rb +31 -1
- data/test/unit/http_client_test.rb +8 -0
- data/test/unit/index_test.rb +37 -9
- data/test/unit/model_import_test.rb +17 -0
- data/test/unit/model_initialization_test.rb +43 -4
- data/test/unit/results_collection_test.rb +62 -0
- data/test/unit/results_item_test.rb +39 -1
- data/test/unit/search_facet_test.rb +15 -0
- data/test/unit/search_query_test.rb +129 -14
- data/test/unit/tire_test.rb +13 -0
- data/tire.gemspec +1 -0
- metadata +30 -10
- data/test/integration/text_query_test.rb +0 -39
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
|
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
|
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,
|
8
|
+
def search(indices=nil, params={}, &block)
|
9
9
|
if block_given?
|
10
|
-
Search::Search.new(indices,
|
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 #{
|
13
|
-
unless
|
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
|
-
|
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
|
-
|
27
|
-
|
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
|
|
data/lib/tire/http/client.rb
CHANGED
@@ -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) &&
|
184
|
-
meta =
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
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
|
|
data/lib/tire/model/import.rb
CHANGED
@@ -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
|
-
|
66
|
-
|
67
|
-
|
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
|
data/lib/tire/model/indexing.rb
CHANGED
@@ -112,7 +112,8 @@ module Tire
|
|
112
112
|
result
|
113
113
|
end
|
114
114
|
end
|
115
|
-
|
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
|
-
|
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
|
110
|
-
document
|
111
|
-
document.update(
|
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
|
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)
|
data/lib/tire/results/item.rb
CHANGED
data/lib/tire/search/facet.rb
CHANGED
@@ -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
|