tire 0.4.3 → 0.5.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/.gitignore +1 -1
- data/.yardopts +1 -0
- data/README.markdown +2 -2
- data/examples/rails-application-template.rb +20 -6
- data/lib/tire.rb +2 -0
- data/lib/tire/alias.rb +1 -1
- data/lib/tire/configuration.rb +8 -0
- data/lib/tire/dsl.rb +69 -2
- data/lib/tire/index.rb +33 -20
- data/lib/tire/model/indexing.rb +7 -1
- data/lib/tire/model/persistence.rb +7 -4
- data/lib/tire/model/persistence/attributes.rb +1 -1
- data/lib/tire/model/persistence/finders.rb +4 -16
- data/lib/tire/model/search.rb +21 -8
- data/lib/tire/multi_search.rb +263 -0
- data/lib/tire/results/collection.rb +78 -49
- data/lib/tire/results/item.rb +6 -3
- data/lib/tire/results/pagination.rb +15 -1
- data/lib/tire/rubyext/ruby_1_8.rb +1 -7
- data/lib/tire/rubyext/uri_escape.rb +74 -0
- data/lib/tire/search.rb +33 -11
- data/lib/tire/search/facet.rb +8 -3
- data/lib/tire/search/filter.rb +1 -1
- data/lib/tire/search/highlight.rb +1 -1
- data/lib/tire/search/queries/match.rb +40 -0
- data/lib/tire/search/query.rb +42 -6
- data/lib/tire/search/scan.rb +1 -1
- data/lib/tire/search/script_field.rb +1 -1
- data/lib/tire/search/sort.rb +1 -1
- data/lib/tire/tasks.rb +17 -14
- data/lib/tire/version.rb +26 -8
- data/test/integration/active_record_searchable_test.rb +248 -129
- data/test/integration/boosting_queries_test.rb +32 -0
- data/test/integration/custom_score_queries_test.rb +1 -0
- data/test/integration/dsl_search_test.rb +9 -1
- data/test/integration/facets_test.rb +19 -6
- data/test/integration/match_query_test.rb +79 -0
- data/test/integration/multi_search_test.rb +114 -0
- data/test/integration/persistent_model_test.rb +58 -0
- data/test/models/article.rb +1 -1
- data/test/models/persistent_article_in_index.rb +16 -0
- data/test/models/persistent_article_with_defaults.rb +4 -3
- data/test/test_helper.rb +3 -1
- data/test/unit/configuration_test.rb +10 -0
- data/test/unit/index_test.rb +69 -27
- data/test/unit/model_initialization_test.rb +31 -0
- data/test/unit/model_persistence_test.rb +21 -7
- data/test/unit/model_search_test.rb +56 -5
- data/test/unit/multi_search_test.rb +304 -0
- data/test/unit/results_collection_test.rb +42 -2
- data/test/unit/results_item_test.rb +4 -0
- data/test/unit/search_facet_test.rb +35 -11
- data/test/unit/search_query_test.rb +96 -0
- data/test/unit/search_test.rb +60 -3
- data/test/unit/tire_test.rb +14 -0
- data/tire.gemspec +0 -1
- metadata +75 -44
@@ -0,0 +1,263 @@
|
|
1
|
+
module Tire
|
2
|
+
module Search
|
3
|
+
|
4
|
+
module Multi
|
5
|
+
|
6
|
+
# Wraps the search definitions for Tire::Multi::Search
|
7
|
+
#
|
8
|
+
class SearchDefinitions
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
attr_reader :names
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@names = []
|
15
|
+
@searches = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def << value
|
19
|
+
@names << value[:name]
|
20
|
+
@searches << value[:search]
|
21
|
+
end
|
22
|
+
|
23
|
+
def [] name
|
24
|
+
@searches[ @names.index(name) ]
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(&block)
|
28
|
+
@searches.each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def size
|
32
|
+
@searches.size
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
@searches
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Wraps the search result sets for Tire::Multi::Search
|
41
|
+
#
|
42
|
+
class Results
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
def initialize(searches, results)
|
46
|
+
@searches = searches
|
47
|
+
@results = results
|
48
|
+
@collection = @results.zip(@searches.to_a).map do |results, search|
|
49
|
+
Tire::Results::Collection.new(results, search.options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a specific result sets
|
54
|
+
def [] name
|
55
|
+
if index = @searches.names.index(name)
|
56
|
+
@collection[ index ]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def each(&block)
|
61
|
+
@collection.each(&block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def each_pair(&block)
|
65
|
+
@searches.names.zip(@collection).each(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def size
|
69
|
+
@results.size
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the multi-search result sets as a Hash with the search name
|
73
|
+
# as key and the results as value.
|
74
|
+
#
|
75
|
+
def to_hash
|
76
|
+
result = {}
|
77
|
+
each_pair { |name,results| result[name] = results }
|
78
|
+
result
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Build and perform a [multi-search](http://elasticsearch.org/guide/reference/api/multi-search.html)
|
83
|
+
# request.
|
84
|
+
#
|
85
|
+
# s = Tire::Search::Multi::Search.new 'my-index' do
|
86
|
+
# search :names do
|
87
|
+
# query { match :name, 'john' }
|
88
|
+
# end
|
89
|
+
# search :counts, search_type: 'count' do
|
90
|
+
# query { match :_all, 'john' }
|
91
|
+
# end
|
92
|
+
# search :other, index: 'other-index' do
|
93
|
+
# query { string "first_name:john" }
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# You can optionally pass an index and type to the constructor, using them as defaults
|
98
|
+
# for searches which don't define them.
|
99
|
+
#
|
100
|
+
# Use the {#search} method to add a search definition to the request, passing it search options
|
101
|
+
# as a Hash and the search definition itself using Tire's DSL.
|
102
|
+
#
|
103
|
+
class Search
|
104
|
+
|
105
|
+
attr_reader :indices, :types, :path
|
106
|
+
|
107
|
+
def initialize(indices=nil, options={}, &block)
|
108
|
+
@indices = Array(indices)
|
109
|
+
@types = Array(options.delete(:type)).map { |type| Utils.escape(type) }
|
110
|
+
@options = options
|
111
|
+
@path = ['/', @indices.join(','), @types.join(','), '_msearch'].compact.join('/').squeeze('/')
|
112
|
+
@searches = Tire::Search::Multi::SearchDefinitions.new
|
113
|
+
|
114
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Add a search definition to the multi-search request.
|
118
|
+
#
|
119
|
+
# The usual search options such as `search_type`, `routing`, etc. can be passed as a Hash,
|
120
|
+
# and the search definition itself should be passed as a block using the Tire DSL.
|
121
|
+
#
|
122
|
+
def search(*args, &block)
|
123
|
+
name_or_options = args.pop
|
124
|
+
|
125
|
+
if name_or_options.is_a?(Hash)
|
126
|
+
options = name_or_options
|
127
|
+
name = args.pop
|
128
|
+
else
|
129
|
+
name = name_or_options
|
130
|
+
end
|
131
|
+
|
132
|
+
name ||= @searches.size
|
133
|
+
options ||= {}
|
134
|
+
indices = options.delete(:index) || options.delete(:indices)
|
135
|
+
|
136
|
+
@searches << { :name => name, :search => Tire::Search::Search.new(indices, options, &block) }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Without argument, returns the collection of search definitions.
|
140
|
+
# With argument, returns a search definition by name or order.
|
141
|
+
#
|
142
|
+
def searches(name=nil)
|
143
|
+
name ? @searches[ name ] : @searches
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns and array of search definition names
|
147
|
+
#
|
148
|
+
def names
|
149
|
+
@searches.names
|
150
|
+
end
|
151
|
+
|
152
|
+
# Serializes the search definitions as an array of JSON definitions
|
153
|
+
#
|
154
|
+
def to_array
|
155
|
+
@searches.map do |search|
|
156
|
+
header = {}
|
157
|
+
header.update(:index => search.indices.join(',')) unless search.indices.empty?
|
158
|
+
header.update(:type => search.types.join(',')) unless search.types.empty?
|
159
|
+
header.update(:search_type => search.options[:search_type]) if search.options[:search_type]
|
160
|
+
header.update(:routing => search.options[:routing]) if search.options[:routing]
|
161
|
+
header.update(:preference => search.options[:preference]) if search.options[:preference]
|
162
|
+
body = search.to_hash
|
163
|
+
[ header, body ]
|
164
|
+
end.flatten
|
165
|
+
end
|
166
|
+
|
167
|
+
# Serializes the search definitions as a multi-line string payload
|
168
|
+
#
|
169
|
+
def to_payload
|
170
|
+
to_array.map { |line| MultiJson.encode(line) }.join("\n") + "\n"
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the request URL
|
174
|
+
#
|
175
|
+
def url
|
176
|
+
[ Configuration.url, @path ].join
|
177
|
+
end
|
178
|
+
|
179
|
+
# Serializes the request URL parameters
|
180
|
+
#
|
181
|
+
def params
|
182
|
+
options = @options.dup
|
183
|
+
options.empty? ? '' : '?' + options.to_param
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns an enumerable collection of result sets.
|
187
|
+
#
|
188
|
+
# You can simply iterate over them:
|
189
|
+
#
|
190
|
+
# search.results.each do |results|
|
191
|
+
# puts results.each.map(&:name)
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# To iterate over named result sets, use the `each_pair` method:
|
195
|
+
#
|
196
|
+
# search.results.each_pair do |name,results|
|
197
|
+
# puts "Search #{name} got #{results.size} results"
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# To get a specific result set:
|
201
|
+
#
|
202
|
+
# search.results[:myname]
|
203
|
+
#
|
204
|
+
def results
|
205
|
+
@results || perform and @results
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the HTTP response
|
209
|
+
#
|
210
|
+
def response
|
211
|
+
@response || perform and @response
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the raw JSON as a Hash
|
215
|
+
#
|
216
|
+
def json
|
217
|
+
@json || perform and @json
|
218
|
+
end
|
219
|
+
|
220
|
+
def perform
|
221
|
+
@responses = Configuration.client.get(url + params, to_payload)
|
222
|
+
if @responses.failure?
|
223
|
+
STDERR.puts "[REQUEST FAILED] #{to_curl}\n"
|
224
|
+
raise SearchRequestFailed, @responses.to_s
|
225
|
+
end
|
226
|
+
@json = MultiJson.decode(@responses.body)
|
227
|
+
@results = Tire::Search::Multi::Results.new @searches, @json['responses']
|
228
|
+
return self
|
229
|
+
ensure
|
230
|
+
logged
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_curl
|
234
|
+
%Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '\n#{to_payload}'|
|
235
|
+
end
|
236
|
+
|
237
|
+
def logged(endpoint='_msearch')
|
238
|
+
if Configuration.logger
|
239
|
+
|
240
|
+
Configuration.logger.log_request endpoint, indices, to_curl
|
241
|
+
|
242
|
+
took = @json['took'] rescue nil
|
243
|
+
code = @response.code rescue nil
|
244
|
+
|
245
|
+
if Configuration.logger.level.to_s == 'debug'
|
246
|
+
body = if @json
|
247
|
+
MultiJson.encode( @json, :pretty => Configuration.pretty)
|
248
|
+
else
|
249
|
+
MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue ''
|
250
|
+
end
|
251
|
+
else
|
252
|
+
body = ''
|
253
|
+
end
|
254
|
+
|
255
|
+
Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A'
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
@@ -5,62 +5,26 @@ module Tire
|
|
5
5
|
include Enumerable
|
6
6
|
include Pagination
|
7
7
|
|
8
|
-
attr_reader :time, :total, :options, :facets
|
8
|
+
attr_reader :time, :total, :options, :facets, :max_score
|
9
9
|
|
10
10
|
def initialize(response, options={})
|
11
|
-
@response
|
12
|
-
@options
|
13
|
-
@time
|
14
|
-
@total
|
15
|
-
@facets
|
16
|
-
@
|
11
|
+
@response = response
|
12
|
+
@options = options
|
13
|
+
@time = response['took'].to_i
|
14
|
+
@total = response['hits']['total'].to_i rescue nil
|
15
|
+
@facets = response['facets']
|
16
|
+
@max_score = response['hits']['max_score'].to_f rescue nil
|
17
|
+
@wrapper = options[:wrapper] || Configuration.wrapper
|
17
18
|
end
|
18
19
|
|
19
20
|
def results
|
21
|
+
return [] if failure?
|
20
22
|
@results ||= begin
|
21
23
|
hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
|
22
|
-
|
23
24
|
unless @options[:load]
|
24
|
-
|
25
|
-
hits
|
26
|
-
else
|
27
|
-
hits.map do |h|
|
28
|
-
document = {}
|
29
|
-
|
30
|
-
# Update the document with content and ID
|
31
|
-
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
|
32
|
-
document.update( {'id' => h['_id']} )
|
33
|
-
|
34
|
-
# Update the document with meta information
|
35
|
-
['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
|
36
|
-
|
37
|
-
# Return an instance of the "wrapper" class
|
38
|
-
@wrapper.new(document)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
25
|
+
__get_results_without_load(hits)
|
42
26
|
else
|
43
|
-
|
44
|
-
|
45
|
-
records = {}
|
46
|
-
@response['hits']['hits'].group_by { |item| item['_type'] }.each do |type, items|
|
47
|
-
raise NoMethodError, "You have tried to eager load the model instances, " +
|
48
|
-
"but Tire cannot find the model class because " +
|
49
|
-
"document has no _type property." unless type
|
50
|
-
|
51
|
-
begin
|
52
|
-
klass = type.camelize.constantize
|
53
|
-
rescue NameError => e
|
54
|
-
raise NameError, "You have tried to eager load the model instances, but " +
|
55
|
-
"Tire cannot find the model class '#{type.camelize}' " +
|
56
|
-
"based on _type '#{type}'.", e.backtrace
|
57
|
-
end
|
58
|
-
ids = items.map { |h| h['_id'] }
|
59
|
-
records[type] = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
|
60
|
-
end
|
61
|
-
|
62
|
-
# Reorder records to preserve order from search results
|
63
|
-
@response['hits']['hits'].map { |item| records[item['_type']].detect { |record| record.id.to_s == item['_id'].to_s } }
|
27
|
+
__get_results_with_load(hits)
|
64
28
|
end
|
65
29
|
end
|
66
30
|
end
|
@@ -78,8 +42,21 @@ module Tire
|
|
78
42
|
end
|
79
43
|
alias :length :size
|
80
44
|
|
81
|
-
def
|
82
|
-
results
|
45
|
+
def slice(*args)
|
46
|
+
results.slice(*args)
|
47
|
+
end
|
48
|
+
alias :[] :slice
|
49
|
+
|
50
|
+
def error
|
51
|
+
@response['error']
|
52
|
+
end
|
53
|
+
|
54
|
+
def success?
|
55
|
+
error.to_s.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def failure?
|
59
|
+
! success?
|
83
60
|
end
|
84
61
|
|
85
62
|
def to_ary
|
@@ -108,6 +85,58 @@ module Tire
|
|
108
85
|
fields
|
109
86
|
end
|
110
87
|
|
88
|
+
def __get_results_without_load(hits)
|
89
|
+
if @wrapper == Hash
|
90
|
+
hits
|
91
|
+
else
|
92
|
+
hits.map do |h|
|
93
|
+
document = {}
|
94
|
+
|
95
|
+
# Update the document with content and ID
|
96
|
+
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
|
97
|
+
document.update( {'id' => h['_id']} )
|
98
|
+
|
99
|
+
# Update the document with meta information
|
100
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
|
101
|
+
|
102
|
+
# Return an instance of the "wrapper" class
|
103
|
+
@wrapper.new(document)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def __get_results_with_load(hits)
|
109
|
+
return [] if hits.empty?
|
110
|
+
|
111
|
+
records = {}
|
112
|
+
@response['hits']['hits'].group_by { |item| item['_type'] }.each do |type, items|
|
113
|
+
raise NoMethodError, "You have tried to eager load the model instances, " +
|
114
|
+
"but Tire cannot find the model class because " +
|
115
|
+
"document has no _type property." unless type
|
116
|
+
|
117
|
+
begin
|
118
|
+
klass = type.camelize.constantize
|
119
|
+
rescue NameError => e
|
120
|
+
raise NameError, "You have tried to eager load the model instances, but " +
|
121
|
+
"Tire cannot find the model class '#{type.camelize}' " +
|
122
|
+
"based on _type '#{type}'.", e.backtrace
|
123
|
+
end
|
124
|
+
|
125
|
+
records[type] = __find_records_by_ids klass, items.map { |h| h['_id'] }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Reorder records to preserve the order from search results
|
129
|
+
@response['hits']['hits'].map do |item|
|
130
|
+
records[item['_type']].detect do |record|
|
131
|
+
record.id.to_s == item['_id'].to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def __find_records_by_ids(klass, ids)
|
137
|
+
@options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
|
138
|
+
end
|
139
|
+
|
111
140
|
end
|
112
141
|
|
113
142
|
end
|
data/lib/tire/results/item.rb
CHANGED
@@ -20,11 +20,14 @@ module Tire
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
# Delegate method to a key in underlying hash, if present,
|
24
|
-
# otherwise return +nil+.
|
23
|
+
# Delegate method to a key in underlying hash, if present, otherwise return +nil+.
|
25
24
|
#
|
26
25
|
def method_missing(method_name, *arguments)
|
27
|
-
@attributes
|
26
|
+
@attributes[method_name.to_sym]
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to?(method_name)
|
30
|
+
@attributes.has_key?(method_name.to_sym) || super
|
28
31
|
end
|
29
32
|
|
30
33
|
def [](key)
|
@@ -5,12 +5,17 @@ module Tire
|
|
5
5
|
#
|
6
6
|
module Pagination
|
7
7
|
|
8
|
+
def default_per_page
|
9
|
+
10
|
10
|
+
end
|
11
|
+
module_function :default_per_page
|
12
|
+
|
8
13
|
def total_entries
|
9
14
|
@total
|
10
15
|
end
|
11
16
|
|
12
17
|
def per_page
|
13
|
-
(@options[:per_page] || @options[:size] ||
|
18
|
+
(@options[:per_page] || @options[:size] || default_per_page ).to_i
|
14
19
|
end
|
15
20
|
|
16
21
|
def total_pages
|
@@ -48,6 +53,15 @@ module Tire
|
|
48
53
|
alias :num_pages :total_pages
|
49
54
|
alias :offset_value :offset
|
50
55
|
|
56
|
+
def first_page?
|
57
|
+
current_page == 1
|
58
|
+
end
|
59
|
+
|
60
|
+
def last_page?
|
61
|
+
current_page == total_pages
|
62
|
+
end
|
63
|
+
|
64
|
+
|
51
65
|
end
|
52
66
|
|
53
67
|
end
|