elastic_searchable 2.0.1 → 2.0.2
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/elastic_searchable.gemspec +1 -0
- data/lib/elastic_searchable/active_record_extensions.rb +260 -57
- data/lib/elastic_searchable/version.rb +1 -1
- data/lib/elastic_searchable.rb +5 -1
- data/test/test_elastic_searchable.rb +2 -0
- metadata +32 -24
- data/lib/elastic_searchable/callbacks.rb +0 -22
- data/lib/elastic_searchable/index.rb +0 -140
- data/lib/elastic_searchable/queries.rb +0 -59
data/elastic_searchable.gemspec
CHANGED
@@ -1,79 +1,282 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
require 'backgrounded'
|
3
|
-
require 'elastic_searchable/queries'
|
4
|
-
require 'elastic_searchable/callbacks'
|
5
|
-
require 'elastic_searchable/index'
|
6
3
|
require 'elastic_searchable/paginator'
|
7
4
|
|
8
5
|
module ElasticSearchable
|
9
6
|
module ActiveRecordExtensions
|
10
|
-
|
11
|
-
# :type (optional) configue type to store data in. default to model table name
|
12
|
-
# :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
|
13
|
-
# :if (optional) reference symbol/proc condition to only index when condition is true
|
14
|
-
# :unless (optional) reference symbol/proc condition to skip indexing when condition is true
|
15
|
-
# :json (optional) configure the json document to be indexed (see http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json for available options)
|
16
|
-
#
|
17
|
-
# Available callbacks:
|
18
|
-
# after_index
|
19
|
-
# called after the object is indexed in elasticsearch
|
20
|
-
# (optional) :on => :create/:update can be used to only fire callback when object is created or updated
|
21
|
-
#
|
22
|
-
# after_percolate
|
23
|
-
# called after object is indexed in elasticsearch
|
24
|
-
# only fires if the update index call returns a non-empty set of registered percolations
|
25
|
-
# use the "percolations" instance method from within callback to inspect what percolations were returned
|
26
|
-
def elastic_searchable(options = {})
|
27
|
-
cattr_accessor :elastic_options
|
28
|
-
self.elastic_options = options.symbolize_keys.merge(:unless => Array.wrap(options[:unless]).push(:elasticsearch_offline?))
|
29
|
-
|
30
|
-
if self.elastic_options[:index_options]
|
31
|
-
ActiveSupport::Deprecation.warn ":index_options has been deprecated. Use ElasticSearchable.index_settings instead.", caller
|
32
|
-
end
|
33
|
-
if self.elastic_options[:index]
|
34
|
-
ActiveSupport::Deprecation.warn ":index has been deprecated. Use ElasticSearchable.index_name instead.", caller
|
35
|
-
end
|
7
|
+
extend ActiveSupport::Concern
|
36
8
|
|
37
|
-
|
38
|
-
|
9
|
+
included do
|
10
|
+
define_model_callbacks :index, :percolate, :only => :after
|
11
|
+
end
|
39
12
|
|
40
|
-
|
41
|
-
|
13
|
+
module ClassMethods
|
14
|
+
# Valid options:
|
15
|
+
# :type (optional) configue type to store data in. default to model table name
|
16
|
+
# :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
|
17
|
+
# :if (optional) reference symbol/proc condition to only index when condition is true
|
18
|
+
# :unless (optional) reference symbol/proc condition to skip indexing when condition is true
|
19
|
+
# :json (optional) configure the json document to be indexed (see http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json for available options)
|
20
|
+
#
|
21
|
+
# after_percolate
|
22
|
+
# called after object is indexed in elasticsearch
|
23
|
+
# only fires if the update index call returns a non-empty set of registered percolations
|
24
|
+
# use the "percolations" instance method from within callback to inspect what percolations were returned
|
25
|
+
def elastic_searchable(options = {})
|
26
|
+
include ElasticSearchable::ActiveRecordExtensions::LocalMethods
|
42
27
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
28
|
+
cattr_accessor :elastic_options
|
29
|
+
self.elastic_options = options.symbolize_keys.merge(:unless => Array.wrap(options[:unless]).push(:elasticsearch_offline?))
|
30
|
+
attr_reader :hit
|
31
|
+
attr_accessor :index_lifecycle
|
47
32
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
after_commit :delete_from_index, :unless => :elasticsearch_offline?, :on => :destroy
|
54
|
-
|
55
|
-
class_eval do
|
56
|
-
# retuns list of percolation matches found during indexing
|
57
|
-
def percolations
|
58
|
-
@percolations || []
|
33
|
+
if self.elastic_options[:index_options]
|
34
|
+
ActiveSupport::Deprecation.warn ":index_options has been deprecated. Use ElasticSearchable.index_settings instead.", caller
|
35
|
+
end
|
36
|
+
if self.elastic_options[:index]
|
37
|
+
ActiveSupport::Deprecation.warn ":index has been deprecated. Use ElasticSearchable.index_name instead.", caller
|
59
38
|
end
|
60
39
|
|
40
|
+
backgrounded :update_index_on_create => ElasticSearchable.backgrounded_options, :update_index_on_update => ElasticSearchable.backgrounded_options
|
61
41
|
class << self
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
42
|
+
backgrounded :delete_id_from_index => ElasticSearchable.backgrounded_options
|
43
|
+
end
|
44
|
+
|
45
|
+
after_commit :update_index_on_create_backgrounded, :if => :should_index?, :on => :create
|
46
|
+
after_commit :update_index_on_update_backgrounded, :if => :should_index?, :on => :update
|
47
|
+
after_commit :delete_from_index, :unless => :elasticsearch_offline?, :on => :destroy
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module LocalMethods
|
52
|
+
extend ActiveSupport::Concern
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
PER_PAGE_DEFAULT = 20
|
56
|
+
|
57
|
+
# Available callback method after indexing is complete
|
58
|
+
# called after the object is indexed in elasticsearch
|
59
|
+
# (optional) :on => :create/:update can be used to only fire callback when object is created or updated
|
60
|
+
# override default after_index callback definition to support :on option
|
61
|
+
# see ActiveRecord::Transactions::ClassMethods#after_commit for example
|
62
|
+
def after_index(*args, &block)
|
63
|
+
options = args.last
|
64
|
+
if options.is_a?(Hash) && options[:on]
|
65
|
+
options[:if] = Array.wrap(options[:if])
|
66
|
+
options[:if] << "self.index_lifecycle == :#{options[:on]}"
|
67
|
+
end
|
68
|
+
set_callback(:index, :after, *args, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
# default number of search results for this model
|
72
|
+
# can be overridden by implementing classes
|
73
|
+
def per_page
|
74
|
+
PER_PAGE_DEFAULT
|
75
|
+
end
|
76
|
+
|
77
|
+
# reindex all records using bulk api
|
78
|
+
# see http://www.elasticsearch.org/guide/reference/api/bulk.html
|
79
|
+
# options:
|
80
|
+
# :scope - scope to use for looking up records to reindex. defaults to self (all)
|
81
|
+
# :page - page/batch to begin indexing at. defaults to 1
|
82
|
+
# :per_page - number of records to index per batch. defaults to 1000
|
83
|
+
#
|
84
|
+
# TODO: move this to AREL relation to remove the options scope param
|
85
|
+
def reindex(options = {})
|
86
|
+
self.create_mapping
|
87
|
+
options.reverse_merge! :page => 1, :per_page => 1000
|
88
|
+
scope = options.delete(:scope) || self
|
89
|
+
page = options[:page]
|
90
|
+
per_page = options[:per_page]
|
91
|
+
records = scope.limit(per_page).offset(per_page * (page -1)).all
|
92
|
+
while records.any? do
|
93
|
+
ElasticSearchable.logger.debug "reindexing batch ##{page}..."
|
94
|
+
actions = []
|
95
|
+
records.each do |record|
|
96
|
+
next unless record.should_index?
|
97
|
+
begin
|
98
|
+
doc = ElasticSearchable.encode_json(record.as_json_for_index)
|
99
|
+
actions << ElasticSearchable.encode_json({:index => {'_index' => ElasticSearchable.index_name, '_type' => index_type, '_id' => record.id}})
|
100
|
+
actions << doc
|
101
|
+
rescue => e
|
102
|
+
ElasticSearchable.logger.warn "Unable to bulk index record: #{record.inspect} [#{e.message}]"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
begin
|
106
|
+
ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
|
107
|
+
rescue ElasticError => e
|
108
|
+
ElasticSearchable.logger.warn "Error indexing batch ##{page}: #{e.message}"
|
109
|
+
ElasticSearchable.logger.warn e
|
69
110
|
end
|
70
|
-
|
111
|
+
|
112
|
+
page += 1
|
113
|
+
records = scope.limit(per_page).offset(per_page* (page-1)).all
|
71
114
|
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# search returns a will_paginate collection of ActiveRecord objects for the search results
|
118
|
+
# supported options:
|
119
|
+
# :page - page of results to search for
|
120
|
+
# :per_page - number of results per page
|
121
|
+
#
|
122
|
+
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/search/
|
123
|
+
def search(query, options = {})
|
124
|
+
page = (options.delete(:page) || 1).to_i
|
125
|
+
options[:fields] ||= '_id'
|
126
|
+
options[:size] ||= per_page_for_search(options)
|
127
|
+
options[:from] ||= options[:size] * (page - 1)
|
128
|
+
options[:query] ||= if query.is_a?(Hash)
|
129
|
+
query
|
130
|
+
else
|
131
|
+
{}.tap do |q|
|
132
|
+
q[:query_string] = { :query => query }
|
133
|
+
q[:query_string][:default_operator] = options.delete(:default_operator) if options.has_key?(:default_operator)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
query = {}
|
137
|
+
case sort = options.delete(:sort)
|
138
|
+
when Array,Hash
|
139
|
+
options[:sort] = sort
|
140
|
+
when String
|
141
|
+
query[:sort] = sort
|
142
|
+
end
|
143
|
+
|
144
|
+
response = ElasticSearchable.request :get, index_mapping_path('_search'), :query => query, :json_body => options
|
145
|
+
hits = response['hits']
|
146
|
+
ids = hits['hits'].collect {|h| h['_id'].to_i }
|
147
|
+
results = self.find(ids).sort_by {|result| ids.index(result.id) }
|
148
|
+
|
149
|
+
results.each do |result|
|
150
|
+
result.instance_variable_set '@hit', hits['hits'][ids.index(result.id)]
|
151
|
+
end
|
152
|
+
|
153
|
+
ElasticSearchable::Paginator.handler.new(results, page, options[:size], hits['total'])
|
154
|
+
end
|
72
155
|
|
156
|
+
def index_type
|
157
|
+
self.elastic_options[:type] || self.table_name
|
158
|
+
end
|
159
|
+
|
160
|
+
# helper method to generate elasticsearch url for this object type
|
161
|
+
def index_mapping_path(action = nil)
|
162
|
+
ElasticSearchable.request_path [index_type, action].compact.join('/')
|
163
|
+
end
|
164
|
+
|
165
|
+
# delete all documents of this type in the index
|
166
|
+
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_mapping/
|
167
|
+
def delete_mapping
|
168
|
+
ElasticSearchable.request :delete, index_mapping_path
|
169
|
+
end
|
170
|
+
|
171
|
+
# configure the index for this type
|
172
|
+
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/put_mapping/
|
173
|
+
def create_mapping
|
174
|
+
return unless self.elastic_options[:mapping]
|
175
|
+
ElasticSearchable.request :put, index_mapping_path('_mapping'), :json_body => {index_type => self.elastic_options[:mapping]}
|
176
|
+
end
|
177
|
+
|
178
|
+
# delete one record from the index
|
179
|
+
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
|
180
|
+
def delete_id_from_index(id)
|
181
|
+
ElasticSearchable.request :delete, index_mapping_path(id)
|
182
|
+
rescue ElasticSearchable::ElasticError => e
|
183
|
+
ElasticSearchable.logger.warn e
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
# determine the number of search results per page
|
188
|
+
# supports will_paginate configuration by using:
|
189
|
+
# Model.per_page
|
190
|
+
# Model.max_per_page
|
191
|
+
def per_page_for_search(options = {})
|
192
|
+
per_page = (options.delete(:per_page) || self.per_page).to_i
|
193
|
+
per_page = [per_page, self.max_per_page].min if self.respond_to?(:max_per_page)
|
194
|
+
per_page
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# retuns list of percolation matches found during indexing
|
199
|
+
# usable when the model is configured with an :after_index callback
|
200
|
+
def percolations
|
201
|
+
@percolations || []
|
202
|
+
end
|
203
|
+
|
204
|
+
# reindex the object in elasticsearch
|
205
|
+
# fires after_index callbacks after operation is complete
|
206
|
+
# see http://www.elasticsearch.org/guide/reference/api/index_.html
|
207
|
+
def reindex(lifecycle = nil)
|
208
|
+
query = {}
|
209
|
+
query[:percolate] = "*" if _percolate_callbacks.any?
|
210
|
+
response = ElasticSearchable.request :put, self.class.index_mapping_path(self.id), :query => query, :json_body => self.as_json_for_index
|
211
|
+
|
212
|
+
self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
|
213
|
+
_run_index_callbacks
|
214
|
+
|
215
|
+
@percolations = response['matches'] || []
|
216
|
+
_run_percolate_callbacks if @percolations.any?
|
217
|
+
end
|
218
|
+
|
219
|
+
# document to index in elasticsearch
|
220
|
+
# can be overridden by implementing class to customize the content
|
221
|
+
def as_json_for_index
|
222
|
+
original_include_root_in_json = self.class.include_root_in_json
|
223
|
+
self.class.include_root_in_json = false
|
224
|
+
return self.as_json self.class.elastic_options[:json]
|
225
|
+
ensure
|
226
|
+
self.class.include_root_in_json = original_include_root_in_json
|
227
|
+
end
|
228
|
+
|
229
|
+
# flag to tell if this instance should be indexed
|
230
|
+
def should_index?
|
231
|
+
[self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
|
232
|
+
![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
|
233
|
+
end
|
234
|
+
|
235
|
+
# percolate this object to see what registered searches match
|
236
|
+
# can be done on transient/non-persisted objects!
|
237
|
+
# can be done automatically when indexing using :percolate => true config option
|
238
|
+
# http://www.elasticsearch.org/blog/2011/02/08/percolator.html
|
239
|
+
def percolate(percolator_query = nil)
|
240
|
+
body = {:doc => self.as_json_for_index}
|
241
|
+
body[:query] = percolator_query if percolator_query
|
242
|
+
response = ElasticSearchable.request :get, self.class.index_mapping_path('_percolate'), :json_body => body
|
243
|
+
@percolations = response['matches'] || []
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
def delete_from_index
|
248
|
+
self.class.delete_id_from_index_backgrounded self.id
|
249
|
+
end
|
250
|
+
def update_index_on_create
|
251
|
+
reindex :create
|
252
|
+
end
|
253
|
+
def update_index_on_update
|
254
|
+
reindex :update
|
255
|
+
end
|
256
|
+
def elasticsearch_offline?
|
257
|
+
ElasticSearchable.offline?
|
258
|
+
end
|
259
|
+
# ripped from activesupport
|
260
|
+
def evaluate_elastic_condition(method)
|
261
|
+
case method
|
262
|
+
when Symbol
|
263
|
+
self.send method
|
264
|
+
when String
|
265
|
+
eval(method, self.instance_eval { binding })
|
266
|
+
when Proc, Method
|
267
|
+
method.call
|
268
|
+
else
|
269
|
+
if method.respond_to?(kind)
|
270
|
+
method.send kind
|
271
|
+
else
|
272
|
+
raise ArgumentError,
|
273
|
+
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
|
274
|
+
"a block to be invoked, or an object responding to the callback method."
|
275
|
+
end
|
73
276
|
end
|
74
277
|
end
|
75
278
|
end
|
76
279
|
end
|
77
280
|
end
|
78
281
|
|
79
|
-
ActiveRecord::Base.send(:
|
282
|
+
ActiveRecord::Base.send(:include, ElasticSearchable::ActiveRecordExtensions)
|
data/lib/elastic_searchable.rb
CHANGED
@@ -43,6 +43,11 @@ module ElasticSearchable
|
|
43
43
|
response
|
44
44
|
end
|
45
45
|
|
46
|
+
# options for automatic active record indexing
|
47
|
+
def backgrounded_options
|
48
|
+
{:queue => 'elasticsearch'}
|
49
|
+
end
|
50
|
+
|
46
51
|
# escape lucene special characters
|
47
52
|
def escape_query(string)
|
48
53
|
string.to_s.gsub(/([\(\)\[\]\{\}\?\\\"!\^\+\-\*:~])/,'\\\\\1')
|
@@ -92,4 +97,3 @@ ElasticSearchable.logger.level = Logger::INFO
|
|
92
97
|
# configure default index to be elastic_searchable
|
93
98
|
# one index can hold many object 'types'
|
94
99
|
ElasticSearchable.index_name = 'elastic_searchable'
|
95
|
-
|
@@ -388,6 +388,7 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
388
388
|
end
|
389
389
|
context "when index has configured percolation" do
|
390
390
|
setup do
|
391
|
+
ElasticSearchable.request :delete, '/_percolator'
|
391
392
|
ElasticSearchable.request :put, '/_percolator/elastic_searchable/myfilter', :json_body => {:query => {:query_string => {:query => 'foo' }}}
|
392
393
|
ElasticSearchable.request :post, '/_percolator/_refresh'
|
393
394
|
end
|
@@ -416,6 +417,7 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
416
417
|
end
|
417
418
|
context "with multiple percolators" do
|
418
419
|
setup do
|
420
|
+
ElasticSearchable.request :delete, '/_percolator'
|
419
421
|
ElasticSearchable.request :put, '/_percolator/elastic_searchable/greenfilter', :json_body => { :color => 'green', :query => {:query_string => {:query => 'foo' }}}
|
420
422
|
ElasticSearchable.request :put, '/_percolator/elastic_searchable/bluefilter', :json_body => { :color => 'blue', :query => {:query_string => {:query => 'foo' }}}
|
421
423
|
ElasticSearchable.request :post, '/_percolator/_refresh'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elastic_searchable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-04-23 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
|
-
requirement: &
|
16
|
+
requirement: &2151881900 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.0.5
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2151881900
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: httparty
|
27
|
-
requirement: &
|
27
|
+
requirement: &2151881240 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.6.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2151881240
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: backgrounded
|
38
|
-
requirement: &
|
38
|
+
requirement: &2151880600 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 0.7.0
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *2151880600
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: multi_json
|
49
|
-
requirement: &
|
49
|
+
requirement: &2151880000 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 1.0.0
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *2151880000
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rake
|
60
|
-
requirement: &
|
60
|
+
requirement: &2151879320 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - =
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 0.9.2.2
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *2151879320
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: sqlite3
|
71
|
-
requirement: &
|
71
|
+
requirement: &2151894960 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - =
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: 1.3.4
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *2151894960
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: pry
|
82
|
-
requirement: &
|
82
|
+
requirement: &2151894160 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - =
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: 0.9.6.2
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *2151894160
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: shoulda
|
93
|
-
requirement: &
|
93
|
+
requirement: &2151893480 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - =
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: 2.11.3
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *2151893480
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: mocha
|
104
|
-
requirement: &
|
104
|
+
requirement: &2151892820 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - =
|
@@ -109,7 +109,18 @@ dependencies:
|
|
109
109
|
version: 0.10.0
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *2151892820
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: pry
|
115
|
+
requirement: &2151892120 !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - =
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 0.9.9.3
|
121
|
+
type: :development
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: *2151892120
|
113
124
|
description: integrate the elastic search engine with rails
|
114
125
|
email:
|
115
126
|
- ryan@codecrate.com
|
@@ -128,12 +139,9 @@ files:
|
|
128
139
|
- elastic_searchable.gemspec
|
129
140
|
- lib/elastic_searchable.rb
|
130
141
|
- lib/elastic_searchable/active_record_extensions.rb
|
131
|
-
- lib/elastic_searchable/callbacks.rb
|
132
|
-
- lib/elastic_searchable/index.rb
|
133
142
|
- lib/elastic_searchable/pagination/kaminari.rb
|
134
143
|
- lib/elastic_searchable/pagination/will_paginate.rb
|
135
144
|
- lib/elastic_searchable/paginator.rb
|
136
|
-
- lib/elastic_searchable/queries.rb
|
137
145
|
- lib/elastic_searchable/version.rb
|
138
146
|
- test/database.yml
|
139
147
|
- test/helper.rb
|
@@ -153,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
161
|
version: '0'
|
154
162
|
segments:
|
155
163
|
- 0
|
156
|
-
hash:
|
164
|
+
hash: 2660925478965356473
|
157
165
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
166
|
none: false
|
159
167
|
requirements:
|
@@ -162,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
170
|
version: '0'
|
163
171
|
segments:
|
164
172
|
- 0
|
165
|
-
hash:
|
173
|
+
hash: 2660925478965356473
|
166
174
|
requirements: []
|
167
175
|
rubyforge_project: elastic_searchable
|
168
176
|
rubygems_version: 1.8.15
|
@@ -1,22 +0,0 @@
|
|
1
|
-
module ElasticSearchable
|
2
|
-
module Callbacks
|
3
|
-
class << self
|
4
|
-
def backgrounded_options
|
5
|
-
{:queue => 'elasticsearch'}
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
module InstanceMethods
|
10
|
-
private
|
11
|
-
def delete_from_index
|
12
|
-
self.class.delete_id_from_index_backgrounded self.id
|
13
|
-
end
|
14
|
-
def update_index_on_create
|
15
|
-
reindex :create
|
16
|
-
end
|
17
|
-
def update_index_on_update
|
18
|
-
reindex :update
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,140 +0,0 @@
|
|
1
|
-
module ElasticSearchable
|
2
|
-
module Indexing
|
3
|
-
module ClassMethods
|
4
|
-
# delete all documents of this type in the index
|
5
|
-
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_mapping/
|
6
|
-
def delete_mapping
|
7
|
-
ElasticSearchable.request :delete, index_mapping_path
|
8
|
-
end
|
9
|
-
|
10
|
-
# configure the index for this type
|
11
|
-
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/put_mapping/
|
12
|
-
def create_mapping
|
13
|
-
return unless self.elastic_options[:mapping]
|
14
|
-
ElasticSearchable.request :put, index_mapping_path('_mapping'), :json_body => {index_type => self.elastic_options[:mapping]}
|
15
|
-
end
|
16
|
-
|
17
|
-
# delete one record from the index
|
18
|
-
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
|
19
|
-
def delete_id_from_index(id)
|
20
|
-
ElasticSearchable.request :delete, index_mapping_path(id)
|
21
|
-
rescue ElasticSearchable::ElasticError => e
|
22
|
-
ElasticSearchable.logger.warn e
|
23
|
-
end
|
24
|
-
|
25
|
-
# helper method to generate elasticsearch url for this object type
|
26
|
-
def index_mapping_path(action = nil)
|
27
|
-
ElasticSearchable.request_path [index_type, action].compact.join('/')
|
28
|
-
end
|
29
|
-
|
30
|
-
# reindex all records using bulk api
|
31
|
-
# see http://www.elasticsearch.org/guide/reference/api/bulk.html
|
32
|
-
# options:
|
33
|
-
# :scope - scope to use for looking up records to reindex. defaults to self (all)
|
34
|
-
# :page - page/batch to begin indexing at. defaults to 1
|
35
|
-
# :per_page - number of records to index per batch. defaults to 1000
|
36
|
-
#
|
37
|
-
# TODO: move this to AREL relation to remove the options scope param
|
38
|
-
def reindex(options = {})
|
39
|
-
self.create_mapping
|
40
|
-
options.reverse_merge! :page => 1, :per_page => 1000
|
41
|
-
scope = options.delete(:scope) || self
|
42
|
-
page = options[:page]
|
43
|
-
per_page = options[:per_page]
|
44
|
-
records = scope.limit(per_page).offset(per_page * (page -1)).all
|
45
|
-
while records.any? do
|
46
|
-
ElasticSearchable.logger.debug "reindexing batch ##{page}..."
|
47
|
-
actions = []
|
48
|
-
records.each do |record|
|
49
|
-
next unless record.should_index?
|
50
|
-
begin
|
51
|
-
doc = ElasticSearchable.encode_json(record.as_json_for_index)
|
52
|
-
actions << ElasticSearchable.encode_json({:index => {'_index' => ElasticSearchable.index_name, '_type' => index_type, '_id' => record.id}})
|
53
|
-
actions << doc
|
54
|
-
rescue => e
|
55
|
-
ElasticSearchable.logger.warn "Unable to bulk index record: #{record.inspect} [#{e.message}]"
|
56
|
-
end
|
57
|
-
end
|
58
|
-
begin
|
59
|
-
ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
|
60
|
-
rescue ElasticError => e
|
61
|
-
ElasticSearchable.logger.warn "Error indexing batch ##{page}: #{e.message}"
|
62
|
-
ElasticSearchable.logger.warn e
|
63
|
-
end
|
64
|
-
|
65
|
-
page += 1
|
66
|
-
records = scope.limit(per_page).offset(per_page* (page-1)).all
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
def index_type
|
72
|
-
self.elastic_options[:type] || self.table_name
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
module InstanceMethods
|
77
|
-
# reindex the object in elasticsearch
|
78
|
-
# fires after_index callbacks after operation is complete
|
79
|
-
# see http://www.elasticsearch.org/guide/reference/api/index_.html
|
80
|
-
def reindex(lifecycle = nil)
|
81
|
-
query = {}
|
82
|
-
query[:percolate] = "*" if _percolate_callbacks.any?
|
83
|
-
response = ElasticSearchable.request :put, self.class.index_mapping_path(self.id), :query => query, :json_body => self.as_json_for_index
|
84
|
-
|
85
|
-
self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
|
86
|
-
_run_index_callbacks
|
87
|
-
|
88
|
-
self.percolations = response['matches'] || []
|
89
|
-
_run_percolate_callbacks if self.percolations.any?
|
90
|
-
end
|
91
|
-
# document to index in elasticsearch
|
92
|
-
def as_json_for_index
|
93
|
-
original_include_root_in_json = self.class.include_root_in_json
|
94
|
-
self.class.include_root_in_json = false
|
95
|
-
return self.as_json self.class.elastic_options[:json]
|
96
|
-
ensure
|
97
|
-
self.class.include_root_in_json = original_include_root_in_json
|
98
|
-
end
|
99
|
-
def should_index?
|
100
|
-
[self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
|
101
|
-
![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
|
102
|
-
end
|
103
|
-
# percolate this object to see what registered searches match
|
104
|
-
# can be done on transient/non-persisted objects!
|
105
|
-
# can be done automatically when indexing using :percolate => true config option
|
106
|
-
# http://www.elasticsearch.org/blog/2011/02/08/percolator.html
|
107
|
-
def percolate(percolator_query = nil)
|
108
|
-
body = {:doc => self.as_json_for_index}
|
109
|
-
body[:query] = percolator_query if percolator_query
|
110
|
-
response = ElasticSearchable.request :get, self.class.index_mapping_path('_percolate'), :json_body => body
|
111
|
-
self.percolations = response['matches'] || []
|
112
|
-
self.percolations
|
113
|
-
end
|
114
|
-
|
115
|
-
private
|
116
|
-
def elasticsearch_offline?
|
117
|
-
ElasticSearchable.offline?
|
118
|
-
end
|
119
|
-
# ripped from activesupport
|
120
|
-
def evaluate_elastic_condition(method)
|
121
|
-
case method
|
122
|
-
when Symbol
|
123
|
-
self.send method
|
124
|
-
when String
|
125
|
-
eval(method, self.instance_eval { binding })
|
126
|
-
when Proc, Method
|
127
|
-
method.call
|
128
|
-
else
|
129
|
-
if method.respond_to?(kind)
|
130
|
-
method.send kind
|
131
|
-
else
|
132
|
-
raise ArgumentError,
|
133
|
-
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
|
134
|
-
"a block to be invoked, or an object responding to the callback method."
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
module ElasticSearchable
|
2
|
-
module Queries
|
3
|
-
PER_PAGE_DEFAULT = 20
|
4
|
-
|
5
|
-
def per_page
|
6
|
-
PER_PAGE_DEFAULT
|
7
|
-
end
|
8
|
-
|
9
|
-
# search returns a will_paginate collection of ActiveRecord objects for the search results
|
10
|
-
# supported options:
|
11
|
-
# :page - page of results to search for
|
12
|
-
# :per_page - number of results per page
|
13
|
-
#
|
14
|
-
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/search/
|
15
|
-
def search(query, options = {})
|
16
|
-
page = (options.delete(:page) || 1).to_i
|
17
|
-
options[:fields] ||= '_id'
|
18
|
-
options[:size] ||= per_page_for_search(options)
|
19
|
-
options[:from] ||= options[:size] * (page - 1)
|
20
|
-
options[:query] ||= if query.is_a?(Hash)
|
21
|
-
query
|
22
|
-
else
|
23
|
-
{}.tap do |q|
|
24
|
-
q[:query_string] = { :query => query }
|
25
|
-
q[:query_string][:default_operator] = options.delete(:default_operator) if options.has_key?(:default_operator)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
query = {}
|
29
|
-
case sort = options.delete(:sort)
|
30
|
-
when Array,Hash
|
31
|
-
options[:sort] = sort
|
32
|
-
when String
|
33
|
-
query[:sort] = sort
|
34
|
-
end
|
35
|
-
|
36
|
-
response = ElasticSearchable.request :get, index_mapping_path('_search'), :query => query, :json_body => options
|
37
|
-
hits = response['hits']
|
38
|
-
ids = hits['hits'].collect {|h| h['_id'].to_i }
|
39
|
-
results = self.find(ids).sort_by {|result| ids.index(result.id) }
|
40
|
-
|
41
|
-
results.each do |result|
|
42
|
-
result.instance_variable_set '@hit', hits['hits'][ids.index(result.id)]
|
43
|
-
end
|
44
|
-
|
45
|
-
ElasticSearchable::Paginator.handler.new(results, page, options[:size], hits['total'])
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
# determine the number of search results per page
|
50
|
-
# supports will_paginate configuration by using:
|
51
|
-
# Model.per_page
|
52
|
-
# Model.max_per_page
|
53
|
-
def per_page_for_search(options = {})
|
54
|
-
per_page = (options.delete(:per_page) || (self.respond_to?(:per_page) ? self.per_page : nil) || ElasticSearchable::Queries::PER_PAGE_DEFAULT).to_i
|
55
|
-
per_page = [per_page, self.max_per_page].min if self.respond_to?(:max_per_page)
|
56
|
-
per_page
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|