ssickles-tire 0.4.2.7 → 0.4.3
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/lib/tire.rb +18 -3
- data/lib/tire/alias.rb +11 -35
- data/lib/tire/index.rb +34 -76
- data/lib/tire/model/callbacks.rb +40 -0
- data/lib/tire/model/import.rb +26 -0
- data/lib/tire/model/indexing.rb +128 -0
- data/lib/tire/model/naming.rb +100 -0
- data/lib/tire/model/percolate.rb +99 -0
- data/lib/tire/model/persistence.rb +72 -0
- data/lib/tire/model/persistence/attributes.rb +143 -0
- data/lib/tire/model/persistence/finders.rb +66 -0
- data/lib/tire/model/persistence/storage.rb +71 -0
- data/lib/tire/model/search.rb +305 -0
- data/lib/tire/results/collection.rb +38 -13
- data/lib/tire/results/item.rb +19 -0
- data/lib/tire/rubyext/hash.rb +8 -0
- data/lib/tire/rubyext/ruby_1_8.rb +54 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +7 -8
- data/lib/tire/search/scan.rb +8 -8
- data/lib/tire/search/sort.rb +1 -1
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +7 -38
- data/test/integration/active_model_indexing_test.rb +51 -0
- data/test/integration/active_model_searchable_test.rb +114 -0
- data/test/integration/active_record_searchable_test.rb +446 -0
- data/test/integration/mongoid_searchable_test.rb +309 -0
- data/test/integration/persistent_model_test.rb +117 -0
- data/test/integration/reindex_test.rb +2 -2
- data/test/integration/scan_test.rb +1 -1
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_document_type.rb +7 -0
- data/test/models/active_model_article_with_custom_index_name.rb +7 -0
- data/test/models/active_record_models.rb +122 -0
- data/test/models/mongoid_models.rb +97 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +17 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +27 -3
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/index_alias_test.rb +3 -17
- data/test/unit/index_test.rb +30 -18
- data/test/unit/model_callbacks_test.rb +116 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +516 -0
- data/test/unit/model_search_test.rb +899 -0
- data/test/unit/results_collection_test.rb +60 -0
- data/test/unit/results_item_test.rb +37 -0
- data/test/unit/rubyext_test.rb +3 -3
- data/test/unit/search_test.rb +1 -6
- data/test/unit/tire_test.rb +15 -0
- data/tire.gemspec +30 -13
- metadata +153 -41
- data/lib/tire/rubyext/to_json.rb +0 -21
@@ -0,0 +1,71 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
# Provides infrastructure for storing records in _ElasticSearch_.
|
7
|
+
#
|
8
|
+
module Storage
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
|
12
|
+
base.class_eval do
|
13
|
+
extend ClassMethods
|
14
|
+
include InstanceMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def create(args={})
|
22
|
+
document = new(args)
|
23
|
+
return false unless document.valid?
|
24
|
+
document.save
|
25
|
+
document
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
|
32
|
+
def update_attribute(name, value)
|
33
|
+
__update_attributes name => value
|
34
|
+
save
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_attributes(attributes={})
|
38
|
+
__update_attributes attributes
|
39
|
+
save
|
40
|
+
end
|
41
|
+
|
42
|
+
def save
|
43
|
+
return false unless valid?
|
44
|
+
run_callbacks :save do
|
45
|
+
# Document#id is set in the +update_elasticsearch_index+ method,
|
46
|
+
# where we have access to the JSON response
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
run_callbacks :destroy do
|
53
|
+
@destroyed = true
|
54
|
+
end
|
55
|
+
self.freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO: Implement `new_record?` and clean up
|
59
|
+
|
60
|
+
def destroyed?; !!@destroyed; end
|
61
|
+
|
62
|
+
def persisted?; !!id; end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,305 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Main module containing the search infrastructure for ActiveModel classes.
|
5
|
+
#
|
6
|
+
# By including this module, you'll provide the model with facilities to
|
7
|
+
# perform searches against index, define index settings and mappings,
|
8
|
+
# access the index object, etc.
|
9
|
+
#
|
10
|
+
# All the _Tire_ methods are accessible via the "proxy" class and instance
|
11
|
+
# methods of the model, named `tire`, eg. `Article.tire.search 'foo'`.
|
12
|
+
#
|
13
|
+
# When there's no clash with a method in the class (your own, defined by another gem, etc)
|
14
|
+
# _Tire_ will bring these methods to the top-level namespace of the class,
|
15
|
+
# eg. `Article.search 'foo'`.
|
16
|
+
#
|
17
|
+
# You'll find the relevant methods in the ClassMethods and InstanceMethods module.
|
18
|
+
#
|
19
|
+
#
|
20
|
+
module Search
|
21
|
+
|
22
|
+
# Alias for Tire::Model::Naming::ClassMethods.index_prefix
|
23
|
+
#
|
24
|
+
def self.index_prefix(*args)
|
25
|
+
Naming::ClassMethods.index_prefix(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
|
30
|
+
# Returns search results for a given query.
|
31
|
+
#
|
32
|
+
# Query can be passed simply as a String:
|
33
|
+
#
|
34
|
+
# Article.search 'love'
|
35
|
+
#
|
36
|
+
# Any options, such as pagination or sorting, can be passed as a second argument:
|
37
|
+
#
|
38
|
+
# Article.search 'love', :per_page => 25, :page => 2
|
39
|
+
# Article.search 'love', :sort => 'title'
|
40
|
+
#
|
41
|
+
# For more powerful query definition, use the query DSL passed as a block:
|
42
|
+
#
|
43
|
+
# Article.search do
|
44
|
+
# query { terms :tags, ['ruby', 'python'] }
|
45
|
+
# facet 'tags' { terms :tags }
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# You can pass options as the first argument, in this case:
|
49
|
+
#
|
50
|
+
# Article.search :per_page => 25, :page => 2 do
|
51
|
+
# query { string 'love' }
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# This methods returns a Tire::Results::Collection instance, containing instances
|
55
|
+
# of Tire::Results::Item, populated by the data available in _ElasticSearch, by default.
|
56
|
+
#
|
57
|
+
# If you'd like to load the "real" models from the database, you may use the `:load` option:
|
58
|
+
#
|
59
|
+
# Article.search 'love', :load => true
|
60
|
+
#
|
61
|
+
# You can pass options as a Hash to the model's `find` method:
|
62
|
+
#
|
63
|
+
# Article.search :load => { :include => 'comments' } do ... end
|
64
|
+
#
|
65
|
+
def search(*args, &block)
|
66
|
+
default_options = {:type => document_type, :index => index.name}
|
67
|
+
|
68
|
+
if block_given?
|
69
|
+
options = args.shift || {}
|
70
|
+
else
|
71
|
+
query, options = args
|
72
|
+
options ||= {}
|
73
|
+
end
|
74
|
+
|
75
|
+
sort = Array( options[:order] || options[:sort] )
|
76
|
+
options = default_options.update(options)
|
77
|
+
|
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]
|
81
|
+
s.sort do
|
82
|
+
sort.each do |t|
|
83
|
+
field_name, direction = t.split(' ')
|
84
|
+
by field_name, direction
|
85
|
+
end
|
86
|
+
end unless sort.empty?
|
87
|
+
|
88
|
+
if block_given?
|
89
|
+
block.arity < 1 ? s.instance_eval(&block) : block.call(s)
|
90
|
+
else
|
91
|
+
s.query { string query }
|
92
|
+
# TODO: Actualy, allow passing all the valid options from
|
93
|
+
# <http://www.elasticsearch.org/guide/reference/api/search/uri-request.html>
|
94
|
+
s.fields Array(options[:fields]) if options[:fields]
|
95
|
+
end
|
96
|
+
|
97
|
+
s.results
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a Tire::Index instance for this model.
|
101
|
+
#
|
102
|
+
# Example usage: `Article.index.refresh`.
|
103
|
+
#
|
104
|
+
def index
|
105
|
+
name = index_name.respond_to?(:to_proc) ? klass.instance_eval(&index_name) : index_name
|
106
|
+
@index = Index.new(name)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
module InstanceMethods
|
112
|
+
|
113
|
+
# Returns a Tire::Index instance for this instance of the model.
|
114
|
+
#
|
115
|
+
# Example usage: `@article.index.refresh`.
|
116
|
+
#
|
117
|
+
def index
|
118
|
+
instance.class.tire.index
|
119
|
+
end
|
120
|
+
|
121
|
+
# Updates the index in _ElasticSearch_.
|
122
|
+
#
|
123
|
+
# On model instance create or update, it will store its serialized representation in the index.
|
124
|
+
#
|
125
|
+
# On model destroy, it will remove the corresponding document from the index.
|
126
|
+
#
|
127
|
+
# It will also execute any `<after|before>_update_elasticsearch_index` callback hooks.
|
128
|
+
#
|
129
|
+
def update_index
|
130
|
+
#instance.send :_run_update_elasticsearch_index_callbacks do
|
131
|
+
if instance.destroyed?
|
132
|
+
index.remove instance
|
133
|
+
else
|
134
|
+
response = index.store( instance, {:percolate => percolator} )
|
135
|
+
instance.id ||= response['_id'] if instance.respond_to?(:id=)
|
136
|
+
instance._index = response['_index'] if instance.respond_to?(:_index=)
|
137
|
+
instance._type = response['_type'] if instance.respond_to?(:_type=)
|
138
|
+
instance._version = response['_version'] if instance.respond_to?(:_version=)
|
139
|
+
instance.matches = response['matches'] if instance.respond_to?(:matches=)
|
140
|
+
self
|
141
|
+
end
|
142
|
+
#end
|
143
|
+
end
|
144
|
+
alias :update_elasticsearch_index :update_index
|
145
|
+
alias :update_elastic_search_index :update_index
|
146
|
+
|
147
|
+
# The default JSON serialization of the model, based on its `#to_hash` representation.
|
148
|
+
#
|
149
|
+
# If you don't define any mapping, the model is serialized as-is.
|
150
|
+
#
|
151
|
+
# If you do define the mapping for _ElasticSearch_, only attributes
|
152
|
+
# declared in the mapping are serialized.
|
153
|
+
#
|
154
|
+
# For properties declared with the `:as` option, the passed String or Proc
|
155
|
+
# is evaluated in the instance context.
|
156
|
+
#
|
157
|
+
def to_indexed_json
|
158
|
+
if instance.class.tire.mapping.empty?
|
159
|
+
# Reject the id and type keys
|
160
|
+
instance.to_hash.reject {|key,_| key.to_s == 'id' || key.to_s == 'type' }.to_json
|
161
|
+
else
|
162
|
+
mapping = instance.class.tire.mapping
|
163
|
+
# Reject keys not declared in mapping
|
164
|
+
hash = instance.to_hash.reject { |key, value| ! mapping.keys.map(&:to_s).include?(key.to_s) }
|
165
|
+
|
166
|
+
# Evalute the `:as` options
|
167
|
+
mapping.each do |key, options|
|
168
|
+
case options[:as]
|
169
|
+
when String
|
170
|
+
hash[key] = instance.instance_eval(options[:as])
|
171
|
+
when Proc
|
172
|
+
hash[key] = instance.instance_eval(&options[:as])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
hash.to_json
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def matches
|
181
|
+
@attributes['matches']
|
182
|
+
end
|
183
|
+
|
184
|
+
def matches=(value)
|
185
|
+
@attributes ||= {}; @attributes['matches'] = value
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
module Loader
|
191
|
+
|
192
|
+
# Load the "real" model from the database via the corresponding model's `find` method.
|
193
|
+
#
|
194
|
+
# Notice that there's an option to eagerly load models with the `:load` option
|
195
|
+
# for the search method.
|
196
|
+
#
|
197
|
+
def load(options=nil)
|
198
|
+
options ? self.class.find(self.id, options) : self.class.find(self.id)
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
# An object containing _Tire's_ model class methods, accessed as `Article.tire`.
|
204
|
+
#
|
205
|
+
class ClassMethodsProxy
|
206
|
+
include Tire::Model::Naming::ClassMethods
|
207
|
+
include Tire::Model::Import::ClassMethods
|
208
|
+
include Tire::Model::Indexing::ClassMethods
|
209
|
+
include Tire::Model::Percolate::ClassMethods
|
210
|
+
include ClassMethods
|
211
|
+
|
212
|
+
INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
|
213
|
+
|
214
|
+
attr_reader :klass
|
215
|
+
def initialize(klass)
|
216
|
+
@klass = klass
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
# An object containing _Tire's_ model instance methods, accessed as `@article.tire`.
|
222
|
+
#
|
223
|
+
class InstanceMethodsProxy
|
224
|
+
include Tire::Model::Naming::InstanceMethods
|
225
|
+
include Tire::Model::Percolate::InstanceMethods
|
226
|
+
include InstanceMethods
|
227
|
+
|
228
|
+
INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
|
229
|
+
|
230
|
+
attr_reader :instance
|
231
|
+
def initialize(instance)
|
232
|
+
@instance = instance
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# A hook triggered by the `include Tire::Model::Search` statement in the model.
|
237
|
+
#
|
238
|
+
def self.included(base)
|
239
|
+
base.class_eval do
|
240
|
+
|
241
|
+
# Returns proxy to the _Tire's_ class methods.
|
242
|
+
#
|
243
|
+
def self.tire &block
|
244
|
+
@__tire__ ||= ClassMethodsProxy.new(self)
|
245
|
+
|
246
|
+
@__tire__.instance_eval(&block) if block_given?
|
247
|
+
@__tire__
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns proxy to the _Tire's_ instance methods.
|
251
|
+
#
|
252
|
+
def tire &block
|
253
|
+
@__tire__ ||= InstanceMethodsProxy.new(self)
|
254
|
+
|
255
|
+
@__tire__.instance_eval(&block) if block_given?
|
256
|
+
@__tire__
|
257
|
+
end
|
258
|
+
|
259
|
+
# Define _Tire's_ callbacks (<after|before>_update_elasticsearch_index).
|
260
|
+
#
|
261
|
+
#define_model_callbacks(:update_elasticsearch_index, :only => [:after, :before]) if \
|
262
|
+
# respond_to?(:define_model_callbacks)
|
263
|
+
|
264
|
+
# Serialize the model as a Hash.
|
265
|
+
#
|
266
|
+
# Uses `serializable_hash` representation of the model,
|
267
|
+
# unless implemented in the model already.
|
268
|
+
#
|
269
|
+
def to_hash
|
270
|
+
self.serializable_hash
|
271
|
+
end unless instance_methods.map(&:to_sym).include?(:to_hash)
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
# Alias _Tire's_ class methods in the top-level namespace of the model,
|
276
|
+
# unless there's a conflict with existing method.
|
277
|
+
#
|
278
|
+
ClassMethodsProxy::INTERFACE.each do |method|
|
279
|
+
base.class_eval <<-"end;", __FILE__, __LINE__ unless base.public_methods.map(&:to_sym).include?(method.to_sym)
|
280
|
+
def self.#{method}(*args, &block) # def search(*args, &block)
|
281
|
+
tire.__send__(#{method.inspect}, *args, &block) # tire.__send__(:search, *args, &block)
|
282
|
+
end # end
|
283
|
+
end;
|
284
|
+
end
|
285
|
+
|
286
|
+
# Alias _Tire's_ instance methods in the top-level namespace of the model,
|
287
|
+
# unless there's a conflict with existing method
|
288
|
+
InstanceMethodsProxy::INTERFACE.each do |method|
|
289
|
+
base.class_eval <<-"end;", __FILE__, __LINE__ unless base.instance_methods.map(&:to_sym).include?(method.to_sym)
|
290
|
+
def #{method}(*args, &block) # def to_indexed_json(*args, &block)
|
291
|
+
tire.__send__(#{method.inspect}, *args, &block) # tire.__send__(:to_indexed_json, *args, &block)
|
292
|
+
end # end
|
293
|
+
end;
|
294
|
+
end
|
295
|
+
|
296
|
+
# Include the `load` functionality in Results::Item
|
297
|
+
#
|
298
|
+
Results::Item.send :include, Loader
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
end
|
@@ -18,24 +18,49 @@ module Tire
|
|
18
18
|
|
19
19
|
def results
|
20
20
|
@results ||= begin
|
21
|
-
hits = @response['hits']['hits'].map { |d| d.update '_type' =>
|
21
|
+
hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
unless @options[:load]
|
24
|
+
if @wrapper == Hash
|
25
|
+
hits
|
26
|
+
else
|
27
|
+
hits.map do |h|
|
28
|
+
document = {}
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
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']} )
|
32
33
|
|
33
|
-
|
34
|
-
|
34
|
+
# Update the document with meta information
|
35
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
# Return an instance of the "wrapper" class
|
38
|
+
@wrapper.new(document)
|
39
|
+
end
|
38
40
|
end
|
41
|
+
|
42
|
+
else
|
43
|
+
return [] if hits.empty?
|
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 } }
|
39
64
|
end
|
40
65
|
end
|
41
66
|
end
|
data/lib/tire/results/item.rb
CHANGED
@@ -2,6 +2,9 @@ module Tire
|
|
2
2
|
module Results
|
3
3
|
|
4
4
|
class Item
|
5
|
+
#extend ActiveModel::Naming
|
6
|
+
#include ActiveModel::Conversion
|
7
|
+
|
5
8
|
# Create new instance, recursively converting all Hashes to Item
|
6
9
|
# and leaving everything else alone.
|
7
10
|
#
|
@@ -40,6 +43,14 @@ module Tire
|
|
40
43
|
!!id
|
41
44
|
end
|
42
45
|
|
46
|
+
def errors
|
47
|
+
ActiveModel::Errors.new(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
43
54
|
def to_key
|
44
55
|
persisted? ? [id] : nil
|
45
56
|
end
|
@@ -48,6 +59,14 @@ module Tire
|
|
48
59
|
@attributes
|
49
60
|
end
|
50
61
|
|
62
|
+
# Let's pretend we're someone else in Rails
|
63
|
+
#
|
64
|
+
def class
|
65
|
+
defined?(::Rails) && @attributes[:_type] ? @attributes[:_type].camelize.constantize : super
|
66
|
+
rescue NameError
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
51
70
|
def inspect
|
52
71
|
s = []; @attributes.each { |k,v| s << "#{k}: #{v.inspect}" }
|
53
72
|
%Q|<Item#{self.class.to_s == 'Tire::Results::Item' ? '' : " (#{self.class})"} #{s.join(', ')}>|
|