elastic_searchable 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -7
- data/.rvmrc +4 -0
- data/README.rdoc +4 -7
- data/elastic_searchable.gemspec +2 -3
- data/lib/elastic_searchable/active_record_extensions.rb +72 -0
- data/lib/elastic_searchable/index.rb +36 -22
- data/lib/elastic_searchable/version.rb +1 -1
- data/lib/elastic_searchable.rb +23 -19
- data/test/helper.rb +2 -5
- data/test/setup_database.rb +48 -0
- data/test/test_elastic_searchable.rb +129 -63
- metadata +30 -30
- data/VERSION +0 -1
- data/lib/elastic_searchable/active_record.rb +0 -67
data/.gitignore
CHANGED
@@ -19,6 +19,15 @@ pkg
|
|
19
19
|
test/*.log
|
20
20
|
test/*.sqlite3
|
21
21
|
|
22
|
+
# For vim:
|
23
|
+
*.swp
|
24
|
+
|
25
|
+
# For MacOS:
|
26
|
+
.DS_Store
|
27
|
+
|
28
|
+
# git files
|
29
|
+
*.orig
|
30
|
+
|
22
31
|
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
23
32
|
#
|
24
33
|
# * Create a file at ~/.gitignore
|
@@ -30,10 +39,6 @@ test/*.sqlite3
|
|
30
39
|
#
|
31
40
|
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
32
41
|
#
|
33
|
-
# For MacOS:
|
34
|
-
#
|
35
|
-
#.DS_Store
|
36
|
-
#
|
37
42
|
# For TextMate
|
38
43
|
#*.tmproj
|
39
44
|
#tmtags
|
@@ -42,6 +47,4 @@ test/*.sqlite3
|
|
42
47
|
#*~
|
43
48
|
#\#*
|
44
49
|
#.\#*
|
45
|
-
|
46
|
-
# For vim:
|
47
|
-
#*.swp
|
50
|
+
|
data/.rvmrc
ADDED
data/README.rdoc
CHANGED
@@ -27,15 +27,12 @@ Integrate the elasticsearch library into Rails.
|
|
27
27
|
#defaults to localhost:9200
|
28
28
|
ElasticSearchable.base_uri = 'server:9200'
|
29
29
|
|
30
|
-
== Contributing
|
30
|
+
== Contributing
|
31
31
|
|
32
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
33
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
34
32
|
* Fork the project
|
35
|
-
*
|
36
|
-
*
|
37
|
-
*
|
38
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
33
|
+
* Fix the issue
|
34
|
+
* Add unit tests
|
35
|
+
* Submit pull request on github
|
39
36
|
|
40
37
|
== Copyright
|
41
38
|
|
data/elastic_searchable.gemspec
CHANGED
@@ -14,10 +14,10 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
s.rubyforge_project = "elastic_searchable"
|
16
16
|
|
17
|
-
s.add_runtime_dependency(%q<activerecord>, ["
|
17
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 2.3.11"])
|
18
|
+
s.add_runtime_dependency(%q<will_paginate>, [">= 2.3.0"])
|
18
19
|
s.add_runtime_dependency(%q<httparty>, ["~> 0.6.0"])
|
19
20
|
s.add_runtime_dependency(%q<backgrounded>, ["~> 0.7.0"])
|
20
|
-
s.add_runtime_dependency(%q<will_paginate>, [">= 2.3.15"])
|
21
21
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
22
22
|
s.add_development_dependency(%q<mocha>, [">= 0"])
|
23
23
|
s.add_development_dependency(%q<bundler>, [">= 0"])
|
@@ -25,7 +25,6 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.add_development_dependency(%q<sqlite3-ruby>, ["~> 1.3.2"])
|
26
26
|
s.add_development_dependency(%q<ruby-debug>, [">= 0"])
|
27
27
|
|
28
|
-
|
29
28
|
s.files = `git ls-files`.split("\n")
|
30
29
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
31
30
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'backgrounded'
|
3
|
+
require 'elastic_searchable/queries'
|
4
|
+
require 'elastic_searchable/callbacks'
|
5
|
+
require 'elastic_searchable/index'
|
6
|
+
|
7
|
+
module ElasticSearchable
|
8
|
+
module ActiveRecordExtensions
|
9
|
+
# Valid options:
|
10
|
+
# :index (optional) configure index to store data in. default to ElasticSearchable.default_index
|
11
|
+
# :type (optional) configue type to store data in. default to model table name
|
12
|
+
# :index_options (optional) configure index properties (ex: tokenizer)
|
13
|
+
# :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
|
14
|
+
# :if (optional) reference symbol/proc condition to only index when condition is true
|
15
|
+
# :unless (optional) reference symbol/proc condition to skip indexing when condition is true
|
16
|
+
# :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)
|
17
|
+
#
|
18
|
+
# Available callbacks:
|
19
|
+
# after_index
|
20
|
+
# called after the object is indexed in elasticsearch
|
21
|
+
# (optional) :on => :create/:update can be used to only fire callback when object is created or updated
|
22
|
+
#
|
23
|
+
# after_percolate
|
24
|
+
# called after object is indexed in elasticsearch
|
25
|
+
# only fires if the update index call returns a non-empty set of registered percolations
|
26
|
+
# use the "percolations" instance method from within callback to inspect what percolations were returned
|
27
|
+
def elastic_searchable(options = {})
|
28
|
+
cattr_accessor :elastic_options
|
29
|
+
self.elastic_options = options.symbolize_keys
|
30
|
+
|
31
|
+
extend ElasticSearchable::Indexing::ClassMethods
|
32
|
+
extend ElasticSearchable::Queries
|
33
|
+
|
34
|
+
include ElasticSearchable::Indexing::InstanceMethods
|
35
|
+
include ElasticSearchable::Callbacks::InstanceMethods
|
36
|
+
|
37
|
+
backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
|
38
|
+
class << self
|
39
|
+
backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_accessor :index_lifecycle, :percolations
|
43
|
+
define_model_callbacks :index, :percolate, :only => :after
|
44
|
+
after_commit :update_index_on_create_backgrounded, :if => :should_index?, :unless => :elasticsearch_offline?, :on => :create
|
45
|
+
after_commit :update_index_on_update_backgrounded, :if => :should_index?, :unless => :elasticsearch_offline?, :on => :update
|
46
|
+
after_commit :delete_from_index, :unless => :elasticsearch_offline?, :on => :destroy
|
47
|
+
|
48
|
+
class_eval do
|
49
|
+
# retuns list of percolation matches found during indexing
|
50
|
+
def percolations
|
51
|
+
@percolations || []
|
52
|
+
end
|
53
|
+
|
54
|
+
class << self
|
55
|
+
# override default after_index callback definition to support :on option
|
56
|
+
# see ActiveRecord::Transactions::ClassMethods#after_commit for example
|
57
|
+
def after_index(*args, &block)
|
58
|
+
options = args.last
|
59
|
+
if options.is_a?(Hash) && options[:on]
|
60
|
+
options[:if] = Array.wrap(options[:if])
|
61
|
+
options[:if] << "self.index_lifecycle == :#{options[:on]}"
|
62
|
+
end
|
63
|
+
set_callback(:index, :after, *args, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ActiveRecord::Base.send(:extend, ElasticSearchable::ActiveRecordExtensions)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'will_paginate'
|
2
|
+
|
1
3
|
module ElasticSearchable
|
2
4
|
module Indexing
|
3
5
|
module ClassMethods
|
@@ -16,11 +18,12 @@ module ElasticSearchable
|
|
16
18
|
end
|
17
19
|
|
18
20
|
# create the index
|
19
|
-
# http://www.elasticsearch.
|
21
|
+
# http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html
|
20
22
|
def create_index
|
21
|
-
options =
|
22
|
-
|
23
|
-
self.
|
23
|
+
options = {}
|
24
|
+
options.merge! :settings => self.elastic_options[:index_options] if self.elastic_options[:index_options]
|
25
|
+
options.merge! :mappings => {index_type => self.elastic_options[:mapping]} if self.elastic_options[:mapping]
|
26
|
+
ElasticSearchable.request :put, index_path, :body => options.to_json
|
24
27
|
end
|
25
28
|
|
26
29
|
# explicitly refresh the index, making all operations performed since the last refresh
|
@@ -41,6 +44,8 @@ module ElasticSearchable
|
|
41
44
|
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
|
42
45
|
def delete_id_from_index(id)
|
43
46
|
ElasticSearchable.request :delete, index_type_path(id)
|
47
|
+
rescue ElasticSearchable::ElasticError => e
|
48
|
+
ElasticSearchable.logger.warn e
|
44
49
|
end
|
45
50
|
|
46
51
|
# helper method to generate elasticsearch url for this object type
|
@@ -54,20 +59,22 @@ module ElasticSearchable
|
|
54
59
|
end
|
55
60
|
|
56
61
|
# reindex all records using bulk api
|
57
|
-
# options:
|
58
|
-
# :scope - scope the find_in_batches to only a subset of records
|
59
|
-
# :batch - counter to start indexing at
|
60
|
-
# :include - passed to find_in_batches to hydrate objects
|
61
62
|
# see http://www.elasticsearch.org/guide/reference/api/bulk.html
|
63
|
+
# options:
|
64
|
+
# :scope - scope to use for looking up records to reindex. defaults to self (all)
|
65
|
+
# :page - page/batch to begin indexing at. defaults to 1
|
66
|
+
# :per_page - number of records to index per batch. defaults to 1000
|
67
|
+
#
|
68
|
+
# TODO: move this to AREL relation to remove the options scope param
|
62
69
|
def reindex(options = {})
|
63
70
|
self.update_index_mapping
|
64
|
-
|
65
|
-
options[:batch_size] ||= 1000
|
66
|
-
options[:start] ||= (batch - 1) * options[:batch_size]
|
71
|
+
options.reverse_merge! :page => 1, :per_page => 1000, :total_entries => 1
|
67
72
|
scope = options.delete(:scope) || self
|
68
|
-
|
69
|
-
|
70
|
-
|
73
|
+
|
74
|
+
records = scope.paginate(options)
|
75
|
+
while records.any? do
|
76
|
+
ElasticSearchable.logger.debug "reindexing batch ##{records.current_page}..."
|
77
|
+
|
71
78
|
actions = []
|
72
79
|
records.each do |record|
|
73
80
|
next unless record.should_index?
|
@@ -82,9 +89,12 @@ module ElasticSearchable
|
|
82
89
|
begin
|
83
90
|
ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
|
84
91
|
rescue ElasticError => e
|
85
|
-
ElasticSearchable.logger.warn "Error indexing batch ##{
|
92
|
+
ElasticSearchable.logger.warn "Error indexing batch ##{options[:page]}: #{e.message}"
|
86
93
|
ElasticSearchable.logger.warn e
|
87
94
|
end
|
95
|
+
|
96
|
+
options.merge! :page => (records.current_page + 1)
|
97
|
+
records = scope.paginate(options)
|
88
98
|
end
|
89
99
|
end
|
90
100
|
|
@@ -106,19 +116,19 @@ module ElasticSearchable
|
|
106
116
|
query.merge! :percolate => "*" if _percolate_callbacks.any?
|
107
117
|
response = ElasticSearchable.request :put, self.class.index_type_path(self.id), :query => query, :body => self.as_json_for_index.to_json
|
108
118
|
|
109
|
-
|
119
|
+
self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
|
110
120
|
_run_index_callbacks
|
111
121
|
|
112
|
-
|
113
|
-
_run_percolate_callbacks if
|
122
|
+
self.percolations = response['matches'] || []
|
123
|
+
_run_percolate_callbacks if self.percolations.any?
|
114
124
|
end
|
115
125
|
# document to index in elasticsearch
|
116
126
|
def as_json_for_index
|
117
|
-
original_include_root_in_json =
|
118
|
-
|
127
|
+
original_include_root_in_json = self.class.include_root_in_json
|
128
|
+
self.class.include_root_in_json = false
|
119
129
|
return self.as_json self.class.elastic_options[:json]
|
120
130
|
ensure
|
121
|
-
|
131
|
+
self.class.include_root_in_json = original_include_root_in_json
|
122
132
|
end
|
123
133
|
def should_index?
|
124
134
|
[self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
|
@@ -130,10 +140,14 @@ module ElasticSearchable
|
|
130
140
|
# http://www.elasticsearch.org/blog/2011/02/08/percolator.html
|
131
141
|
def percolate
|
132
142
|
response = ElasticSearchable.request :get, self.class.index_type_path('_percolate'), :body => {:doc => self.as_json_for_index}.to_json
|
133
|
-
response['matches']
|
143
|
+
self.percolations = response['matches'] || []
|
144
|
+
self.percolations
|
134
145
|
end
|
135
146
|
|
136
147
|
private
|
148
|
+
def elasticsearch_offline?
|
149
|
+
ElasticSearchable.offline?
|
150
|
+
end
|
137
151
|
# ripped from activesupport
|
138
152
|
def evaluate_elastic_condition(method)
|
139
153
|
case method
|
data/lib/elastic_searchable.rb
CHANGED
@@ -1,34 +1,31 @@
|
|
1
1
|
require 'httparty'
|
2
2
|
require 'logger'
|
3
|
-
require 'elastic_searchable/
|
3
|
+
require 'elastic_searchable/active_record_extensions'
|
4
4
|
|
5
5
|
module ElasticSearchable
|
6
|
+
DEFAULT_INDEX = 'elastic_searchable'
|
6
7
|
include HTTParty
|
7
8
|
format :json
|
8
9
|
base_uri 'localhost:9200'
|
9
|
-
#debug_output
|
10
10
|
|
11
11
|
class ElasticError < StandardError; end
|
12
12
|
class << self
|
13
|
-
|
14
|
-
# one index can hold many object 'types'
|
15
|
-
@@default_index = nil
|
16
|
-
def default_index=(index)
|
17
|
-
@@default_index = index
|
18
|
-
end
|
19
|
-
def default_index
|
20
|
-
@@default_index || 'elastic_searchable'
|
21
|
-
end
|
13
|
+
attr_accessor :logger, :default_index, :offline
|
22
14
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
15
|
+
# execute a block of work without reindexing objects
|
16
|
+
def offline(&block)
|
17
|
+
@offline = true
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
@offline = false
|
27
21
|
end
|
28
|
-
def
|
29
|
-
|
22
|
+
def offline?
|
23
|
+
!!@offline
|
30
24
|
end
|
31
|
-
#perform a request to the elasticsearch server
|
25
|
+
# perform a request to the elasticsearch server
|
26
|
+
# configuration:
|
27
|
+
# ElasticSearchable.base_uri 'host:port' controls where to send request to
|
28
|
+
# ElasticSearchable.debug_output outputs all http traffic to console
|
32
29
|
def request(method, url, options = {})
|
33
30
|
response = self.send(method, url, options)
|
34
31
|
logger.debug "elasticsearch request: #{method} #{url} #{"took #{response['took']}ms" if response['took']}"
|
@@ -46,4 +43,11 @@ module ElasticSearchable
|
|
46
43
|
end
|
47
44
|
end
|
48
45
|
|
49
|
-
|
46
|
+
# configure default logger to standard out with info log level
|
47
|
+
ElasticSearchable.logger = Logger.new STDOUT
|
48
|
+
ElasticSearchable.logger.level = Logger::INFO
|
49
|
+
|
50
|
+
# configure default index to be elastic_searchable
|
51
|
+
# one index can hold many object 'types'
|
52
|
+
ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
|
53
|
+
|
data/test/helper.rb
CHANGED
@@ -10,15 +10,12 @@ end
|
|
10
10
|
require 'test/unit'
|
11
11
|
require 'shoulda'
|
12
12
|
require 'mocha'
|
13
|
-
require
|
13
|
+
require 'ruby-debug'
|
14
14
|
|
15
15
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
16
16
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
17
17
|
require 'elastic_searchable'
|
18
|
-
|
19
|
-
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
20
|
-
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
21
|
-
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
|
18
|
+
require 'setup_database'
|
22
19
|
|
23
20
|
class Test::Unit::TestCase
|
24
21
|
def delete_index
|
@@ -0,0 +1,48 @@
|
|
1
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
2
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
3
|
+
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
|
4
|
+
|
5
|
+
ActiveRecord::Schema.define(:version => 1) do
|
6
|
+
create_table :posts, :force => true do |t|
|
7
|
+
t.column :title, :string
|
8
|
+
t.column :body, :string
|
9
|
+
t.column :name, :string
|
10
|
+
end
|
11
|
+
create_table :blogs, :force => true do |t|
|
12
|
+
t.column :title, :string
|
13
|
+
t.column :body, :string
|
14
|
+
end
|
15
|
+
create_table :users, :force => true do |t|
|
16
|
+
t.column :name, :string
|
17
|
+
end
|
18
|
+
create_table :friends, :force => true do |t|
|
19
|
+
t.column :name, :string
|
20
|
+
t.column :favorite_color, :string
|
21
|
+
t.belongs_to :book
|
22
|
+
end
|
23
|
+
create_table :books, :force => true do |t|
|
24
|
+
t.column :title, :string
|
25
|
+
t.column :isbn, :string
|
26
|
+
end
|
27
|
+
create_table :max_page_size_classes, :force => true do |t|
|
28
|
+
t.column :name, :string
|
29
|
+
end
|
30
|
+
create_table :parents, :force => true do |t|
|
31
|
+
t.column :name, :string
|
32
|
+
t.column :description, :string
|
33
|
+
end
|
34
|
+
create_table :children, :force => true do |t|
|
35
|
+
t.column :name, :string
|
36
|
+
t.column :description, :string
|
37
|
+
t.belongs_to :parent
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if WillPaginate.respond_to?(:enable_activerecord)
|
42
|
+
puts 'configuring will_paginate v2.x'
|
43
|
+
WillPaginate.enable_activerecord
|
44
|
+
else
|
45
|
+
puts 'configuring will_paginate v3.x'
|
46
|
+
require 'will_paginate/finders/active_record'
|
47
|
+
WillPaginate::Finders::ActiveRecord.enable!
|
48
|
+
end
|
@@ -1,40 +1,38 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'helper')
|
2
2
|
|
3
3
|
class TestElasticSearchable < Test::Unit::TestCase
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
def setup
|
5
|
+
delete_index
|
6
|
+
end
|
7
|
+
ElasticSearchable.debug_output
|
8
|
+
SINGLE_NODE_CLUSTER_CONFIG = {'number_of_replicas' => 0, 'number_of_shards' => 1}
|
9
|
+
|
10
|
+
context 'non elastic activerecord class' do
|
11
|
+
class Parent < ActiveRecord::Base
|
8
12
|
end
|
9
|
-
|
10
|
-
|
11
|
-
t.column :body, :string
|
13
|
+
setup do
|
14
|
+
@clazz = Parent
|
12
15
|
end
|
13
|
-
|
14
|
-
|
16
|
+
should 'not respond to elastic_options' do
|
17
|
+
assert !@clazz.respond_to?(:elastic_options)
|
15
18
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
+
end
|
20
|
+
context 'instance of non-elastic_searchable activerecord class' do
|
21
|
+
class Parent < ActiveRecord::Base
|
19
22
|
end
|
20
|
-
|
21
|
-
|
23
|
+
setup do
|
24
|
+
@instance = Parent.new
|
22
25
|
end
|
23
|
-
|
24
|
-
|
26
|
+
should 'not respond to percolations' do
|
27
|
+
assert !@instance.respond_to?(:percolations)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
28
|
-
def setup
|
29
|
-
delete_index
|
30
|
-
end
|
31
|
-
def teardown
|
32
|
-
delete_index
|
33
|
-
end
|
34
31
|
class Post < ActiveRecord::Base
|
35
|
-
elastic_searchable :index_options =>
|
32
|
+
elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
|
36
33
|
after_index :indexed
|
37
34
|
after_index :indexed_on_create, :on => :create
|
35
|
+
after_index :indexed_on_update, :on => :update
|
38
36
|
def indexed
|
39
37
|
@indexed = true
|
40
38
|
end
|
@@ -47,8 +45,14 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
47
45
|
def indexed_on_create?
|
48
46
|
@indexed_on_create
|
49
47
|
end
|
48
|
+
def indexed_on_update
|
49
|
+
@indexed_on_update = true
|
50
|
+
end
|
51
|
+
def indexed_on_update?
|
52
|
+
@indexed_on_update
|
53
|
+
end
|
50
54
|
end
|
51
|
-
context '
|
55
|
+
context 'activerecord class with default elastic_searchable config' do
|
52
56
|
setup do
|
53
57
|
@clazz = Post
|
54
58
|
end
|
@@ -58,9 +62,13 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
58
62
|
should 'define elastic_options' do
|
59
63
|
assert @clazz.elastic_options
|
60
64
|
end
|
65
|
+
should 'respond to :percolations' do
|
66
|
+
assert @clazz.new.respond_to?(:percolations)
|
67
|
+
assert_equal [], @clazz.new.percolations
|
68
|
+
end
|
61
69
|
end
|
62
70
|
|
63
|
-
context '
|
71
|
+
context 'Model.request with invalid url' do
|
64
72
|
should 'raise error' do
|
65
73
|
assert_raises ElasticSearchable::ElasticError do
|
66
74
|
ElasticSearchable.request :get, '/elastic_searchable/foobar/notfound'
|
@@ -68,36 +76,18 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
68
76
|
end
|
69
77
|
end
|
70
78
|
|
71
|
-
context '
|
79
|
+
context 'Model.create_index' do
|
72
80
|
setup do
|
73
81
|
Post.create_index
|
82
|
+
Post.refresh_index
|
74
83
|
@status = ElasticSearchable.request :get, '/elastic_searchable/_status'
|
75
84
|
end
|
76
85
|
should 'have created index' do
|
77
86
|
assert @status['ok']
|
78
87
|
end
|
79
|
-
should 'have used custom index_options' do
|
80
|
-
expected = {
|
81
|
-
"index.number_of_replicas" => "1",
|
82
|
-
"index.number_of_shards" => "5",
|
83
|
-
"index.analysis.analyzer.default.tokenizer" => "standard",
|
84
|
-
"index.analysis.analyzer.default.filter.0" => "standard",
|
85
|
-
"index.analysis.analyzer.default.filter.1" => "lowercase",
|
86
|
-
"index.analysis.analyzer.default.filter.2" => "porterStem"
|
87
|
-
}
|
88
|
-
assert_equal expected, @status['indices']['elastic_searchable']['settings'], @status.inspect
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
context 'deleting object that does not exist in search index' do
|
93
|
-
should 'raise error' do
|
94
|
-
assert_raises ElasticSearchable::ElasticError do
|
95
|
-
Post.delete_id_from_index 123
|
96
|
-
end
|
97
|
-
end
|
98
88
|
end
|
99
89
|
|
100
|
-
context '
|
90
|
+
context 'Model.create' do
|
101
91
|
setup do
|
102
92
|
@post = Post.create :title => 'foo', :body => "bar"
|
103
93
|
end
|
@@ -107,27 +97,78 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
107
97
|
should 'have fired after_index_on_create callback' do
|
108
98
|
assert @post.indexed_on_create?
|
109
99
|
end
|
100
|
+
should 'not have fired after_index_on_update callback' do
|
101
|
+
assert !@post.indexed_on_update?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'Model.update' do
|
106
|
+
setup do
|
107
|
+
Post.create! :title => 'foo'
|
108
|
+
@post = Post.last
|
109
|
+
@post.update_attribute :title, 'bar'
|
110
|
+
end
|
111
|
+
should 'have fired after_index callback' do
|
112
|
+
assert @post.indexed?
|
113
|
+
end
|
114
|
+
should 'not have fired after_index_on_create callback' do
|
115
|
+
assert !@post.indexed_on_create?
|
116
|
+
end
|
117
|
+
should 'have fired after_index_on_update callback' do
|
118
|
+
assert @post.indexed_on_update?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'Model.create within ElasticSearchable.offline block' do
|
123
|
+
setup do
|
124
|
+
Post.any_instance.expects(:update_index_on_create).never
|
125
|
+
ElasticSearchable.offline do
|
126
|
+
@post = Post.create :title => 'foo', :body => "bar"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
should 'not have triggered indexing behavior' do end #see expectations
|
130
|
+
should 'not have fired after_index callback' do
|
131
|
+
assert !@post.indexed?
|
132
|
+
end
|
133
|
+
should 'not have fired after_index_on_create callback' do
|
134
|
+
assert !@post.indexed_on_create?
|
135
|
+
end
|
136
|
+
should 'not have fired after_index_on_update callback' do
|
137
|
+
assert !@post.indexed_on_update?
|
138
|
+
end
|
110
139
|
end
|
111
140
|
|
112
141
|
context 'with empty index when multiple database records' do
|
113
142
|
setup do
|
143
|
+
Post.delete_all
|
114
144
|
Post.create_index
|
115
145
|
@first_post = Post.create :title => 'foo', :body => "first bar"
|
116
146
|
@second_post = Post.create :title => 'foo', :body => "second bar"
|
117
|
-
Post.
|
147
|
+
Post.delete_index
|
148
|
+
Post.create_index
|
118
149
|
end
|
119
150
|
should 'not raise error if error occurs reindexing model' do
|
120
151
|
ElasticSearchable.expects(:request).raises(ElasticSearchable::ElasticError.new('faux error'))
|
121
|
-
|
152
|
+
assert_nothing_raised do
|
153
|
+
Post.reindex
|
154
|
+
end
|
122
155
|
end
|
123
|
-
|
156
|
+
should 'not raise error if destroying one instance' do
|
157
|
+
Logger.any_instance.expects(:warn)
|
158
|
+
assert_nothing_raised do
|
159
|
+
@first_post.destroy
|
160
|
+
end
|
161
|
+
end
|
162
|
+
context 'Model.reindex' do
|
124
163
|
setup do
|
125
|
-
Post.reindex
|
164
|
+
Post.reindex :per_page => 1, :scope => Post.scoped(:order => 'body desc')
|
126
165
|
Post.refresh_index
|
127
166
|
end
|
128
167
|
should 'have reindexed both records' do
|
129
|
-
|
130
|
-
|
168
|
+
assert_nothing_raised do
|
169
|
+
ElasticSearchable.request :get, "/elastic_searchable/posts/#{@first_post.id}"
|
170
|
+
ElasticSearchable.request :get, "/elastic_searchable/posts/#{@second_post.id}"
|
171
|
+
end
|
131
172
|
end
|
132
173
|
end
|
133
174
|
end
|
@@ -149,7 +190,7 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
149
190
|
end
|
150
191
|
should 'be paginated' do
|
151
192
|
assert_equal 1, @results.current_page
|
152
|
-
assert_equal
|
193
|
+
assert_equal Post.per_page, @results.per_page
|
153
194
|
assert_nil @results.previous_page
|
154
195
|
assert_nil @results.next_page
|
155
196
|
end
|
@@ -197,7 +238,7 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
197
238
|
|
198
239
|
|
199
240
|
class Blog < ActiveRecord::Base
|
200
|
-
elastic_searchable :if => proc {|b| b.should_index? }
|
241
|
+
elastic_searchable :if => proc {|b| b.should_index? }, :index_options => SINGLE_NODE_CLUSTER_CONFIG
|
201
242
|
def should_index?
|
202
243
|
false
|
203
244
|
end
|
@@ -217,15 +258,32 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
217
258
|
end
|
218
259
|
|
219
260
|
class User < ActiveRecord::Base
|
220
|
-
elastic_searchable :
|
261
|
+
elastic_searchable :index_options => {
|
262
|
+
'number_of_replicas' => 0,
|
263
|
+
'number_of_shards' => 1,
|
264
|
+
"analysis.analyzer.default.tokenizer" => 'standard',
|
265
|
+
"analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem']},
|
266
|
+
:mapping => {:properties => {:name => {:type => :string, :index => :not_analyzed}}}
|
221
267
|
end
|
222
|
-
context 'activerecord class with :mapping
|
268
|
+
context 'activerecord class with :index_options and :mapping' do
|
223
269
|
context 'creating index' do
|
224
270
|
setup do
|
225
271
|
User.create_index
|
226
|
-
|
272
|
+
end
|
273
|
+
should 'have used custom index_options' do
|
274
|
+
@status = ElasticSearchable.request :get, '/elastic_searchable/_status'
|
275
|
+
expected = {
|
276
|
+
"index.number_of_replicas" => "0",
|
277
|
+
"index.number_of_shards" => "1",
|
278
|
+
"index.analysis.analyzer.default.tokenizer" => "standard",
|
279
|
+
"index.analysis.analyzer.default.filter.0" => "standard",
|
280
|
+
"index.analysis.analyzer.default.filter.1" => "lowercase",
|
281
|
+
"index.analysis.analyzer.default.filter.2" => "porterStem"
|
282
|
+
}
|
283
|
+
assert_equal expected, @status['indices']['elastic_searchable']['settings'], @status.inspect
|
227
284
|
end
|
228
285
|
should 'have set mapping' do
|
286
|
+
@status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
|
229
287
|
expected = {
|
230
288
|
"users"=> {
|
231
289
|
"properties"=> {
|
@@ -239,19 +297,27 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
239
297
|
end
|
240
298
|
|
241
299
|
class Friend < ActiveRecord::Base
|
242
|
-
|
300
|
+
belongs_to :book
|
301
|
+
elastic_searchable :json => {:include => {:book => {:only => :title}}, :only => :name}, :index_options => SINGLE_NODE_CLUSTER_CONFIG
|
243
302
|
end
|
244
|
-
context 'activerecord class with
|
303
|
+
context 'activerecord class with optional :json config' do
|
245
304
|
context 'creating index' do
|
246
305
|
setup do
|
247
306
|
Friend.create_index
|
248
|
-
@
|
307
|
+
@book = Book.create! :isbn => '123abc', :title => 'another world'
|
308
|
+
@friend = Friend.new :name => 'bob', :favorite_color => 'red'
|
309
|
+
@friend.book = @book
|
310
|
+
@friend.save!
|
249
311
|
Friend.refresh_index
|
250
312
|
end
|
251
313
|
should 'index json with configuration' do
|
252
314
|
@response = ElasticSearchable.request :get, "/elastic_searchable/friends/#{@friend.id}"
|
315
|
+
# should not index:
|
316
|
+
# friend.favorite_color
|
317
|
+
# book.isbn
|
253
318
|
expected = {
|
254
|
-
"name" => 'bob'
|
319
|
+
"name" => 'bob',
|
320
|
+
'book' => {'title' => 'another world'}
|
255
321
|
}
|
256
322
|
assert_equal expected, @response['_source'], @response.inspect
|
257
323
|
end
|
@@ -263,7 +329,7 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
263
329
|
ElasticSearchable.default_index = 'my_new_index'
|
264
330
|
end
|
265
331
|
teardown do
|
266
|
-
ElasticSearchable.default_index =
|
332
|
+
ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
|
267
333
|
end
|
268
334
|
should 'change default index' do
|
269
335
|
assert_equal 'my_new_index', ElasticSearchable.default_index
|
@@ -318,7 +384,7 @@ class TestElasticSearchable < Test::Unit::TestCase
|
|
318
384
|
end
|
319
385
|
|
320
386
|
class MaxPageSizeClass < ActiveRecord::Base
|
321
|
-
elastic_searchable
|
387
|
+
elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
|
322
388
|
def self.max_per_page
|
323
389
|
1
|
324
390
|
end
|
metadata
CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 1.0.2
|
10
|
+
version: 1.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ryan Sonnek
|
@@ -15,8 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
19
|
-
default_executable:
|
18
|
+
date: 2011-04-15 00:00:00 Z
|
20
19
|
dependencies:
|
21
20
|
- !ruby/object:Gem::Dependency
|
22
21
|
name: activerecord
|
@@ -24,62 +23,62 @@ dependencies:
|
|
24
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
24
|
none: false
|
26
25
|
requirements:
|
27
|
-
- -
|
26
|
+
- - ">="
|
28
27
|
- !ruby/object:Gem::Version
|
29
|
-
hash:
|
28
|
+
hash: 21
|
30
29
|
segments:
|
30
|
+
- 2
|
31
31
|
- 3
|
32
|
-
-
|
33
|
-
|
34
|
-
version: 3.0.0
|
32
|
+
- 11
|
33
|
+
version: 2.3.11
|
35
34
|
type: :runtime
|
36
35
|
version_requirements: *id001
|
37
36
|
- !ruby/object:Gem::Dependency
|
38
|
-
name:
|
37
|
+
name: will_paginate
|
39
38
|
prerelease: false
|
40
39
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
40
|
none: false
|
42
41
|
requirements:
|
43
|
-
- -
|
42
|
+
- - ">="
|
44
43
|
- !ruby/object:Gem::Version
|
45
|
-
hash:
|
44
|
+
hash: 3
|
46
45
|
segments:
|
46
|
+
- 2
|
47
|
+
- 3
|
47
48
|
- 0
|
48
|
-
|
49
|
-
- 0
|
50
|
-
version: 0.6.0
|
49
|
+
version: 2.3.0
|
51
50
|
type: :runtime
|
52
51
|
version_requirements: *id002
|
53
52
|
- !ruby/object:Gem::Dependency
|
54
|
-
name:
|
53
|
+
name: httparty
|
55
54
|
prerelease: false
|
56
55
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
56
|
none: false
|
58
57
|
requirements:
|
59
58
|
- - ~>
|
60
59
|
- !ruby/object:Gem::Version
|
61
|
-
hash:
|
60
|
+
hash: 7
|
62
61
|
segments:
|
63
62
|
- 0
|
64
|
-
-
|
63
|
+
- 6
|
65
64
|
- 0
|
66
|
-
version: 0.
|
65
|
+
version: 0.6.0
|
67
66
|
type: :runtime
|
68
67
|
version_requirements: *id003
|
69
68
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
69
|
+
name: backgrounded
|
71
70
|
prerelease: false
|
72
71
|
requirement: &id004 !ruby/object:Gem::Requirement
|
73
72
|
none: false
|
74
73
|
requirements:
|
75
|
-
- -
|
74
|
+
- - ~>
|
76
75
|
- !ruby/object:Gem::Version
|
77
|
-
hash:
|
76
|
+
hash: 3
|
78
77
|
segments:
|
79
|
-
-
|
80
|
-
-
|
81
|
-
-
|
82
|
-
version:
|
78
|
+
- 0
|
79
|
+
- 7
|
80
|
+
- 0
|
81
|
+
version: 0.7.0
|
83
82
|
type: :runtime
|
84
83
|
version_requirements: *id004
|
85
84
|
- !ruby/object:Gem::Dependency
|
@@ -180,22 +179,22 @@ extra_rdoc_files: []
|
|
180
179
|
files:
|
181
180
|
- .document
|
182
181
|
- .gitignore
|
182
|
+
- .rvmrc
|
183
183
|
- Gemfile
|
184
184
|
- LICENSE.txt
|
185
185
|
- README.rdoc
|
186
186
|
- Rakefile
|
187
|
-
- VERSION
|
188
187
|
- elastic_searchable.gemspec
|
189
188
|
- lib/elastic_searchable.rb
|
190
|
-
- lib/elastic_searchable/
|
189
|
+
- lib/elastic_searchable/active_record_extensions.rb
|
191
190
|
- lib/elastic_searchable/callbacks.rb
|
192
191
|
- lib/elastic_searchable/index.rb
|
193
192
|
- lib/elastic_searchable/queries.rb
|
194
193
|
- lib/elastic_searchable/version.rb
|
195
194
|
- test/database.yml
|
196
195
|
- test/helper.rb
|
196
|
+
- test/setup_database.rb
|
197
197
|
- test/test_elastic_searchable.rb
|
198
|
-
has_rdoc: true
|
199
198
|
homepage: http://github.com/wireframe/elastic_searchable
|
200
199
|
licenses: []
|
201
200
|
|
@@ -225,11 +224,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
225
224
|
requirements: []
|
226
225
|
|
227
226
|
rubyforge_project: elastic_searchable
|
228
|
-
rubygems_version: 1.
|
227
|
+
rubygems_version: 1.7.2
|
229
228
|
signing_key:
|
230
229
|
specification_version: 3
|
231
230
|
summary: elastic search for activerecord
|
232
231
|
test_files:
|
233
232
|
- test/database.yml
|
234
233
|
- test/helper.rb
|
234
|
+
- test/setup_database.rb
|
235
235
|
- test/test_elastic_searchable.rb
|
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.6.1
|
@@ -1,67 +0,0 @@
|
|
1
|
-
require 'active_record'
|
2
|
-
require 'backgrounded'
|
3
|
-
require 'elastic_searchable/queries'
|
4
|
-
require 'elastic_searchable/callbacks'
|
5
|
-
require 'elastic_searchable/index'
|
6
|
-
|
7
|
-
module ElasticSearchable
|
8
|
-
module ActiveRecord
|
9
|
-
def self.included(base)
|
10
|
-
base.send :extend, ClassMethods
|
11
|
-
end
|
12
|
-
|
13
|
-
module ClassMethods
|
14
|
-
attr_accessor :elastic_options
|
15
|
-
|
16
|
-
# Valid options:
|
17
|
-
# :index (optional) configure index to store data in. default to ElasticSearchable.default_index
|
18
|
-
# :type (optional) configue type to store data in. default to model table name
|
19
|
-
# :index_options (optional) configure index properties (ex: tokenizer)
|
20
|
-
# :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
|
21
|
-
# :if (optional) reference symbol/proc condition to only index when condition is true
|
22
|
-
# :unless (optional) reference symbol/proc condition to skip indexing when condition is true
|
23
|
-
# :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)
|
24
|
-
#
|
25
|
-
# Available callbacks:
|
26
|
-
# after_index
|
27
|
-
# called after the object is indexed in elasticsearch
|
28
|
-
# (optional) :on => :create/:update can be used to only fire callback when object is created or updated
|
29
|
-
#
|
30
|
-
# after_percolate
|
31
|
-
# called after object is indexed in elasticsearch
|
32
|
-
# only fires if the update index call returns a non-empty set of registered percolations
|
33
|
-
# use the "percolations" instance method from within callback to inspect what percolations were returned
|
34
|
-
def elastic_searchable(options = {})
|
35
|
-
options.symbolize_keys!
|
36
|
-
self.elastic_options = options
|
37
|
-
|
38
|
-
extend ElasticSearchable::Indexing::ClassMethods
|
39
|
-
extend ElasticSearchable::Queries
|
40
|
-
|
41
|
-
include ElasticSearchable::Indexing::InstanceMethods
|
42
|
-
include ElasticSearchable::Callbacks::InstanceMethods
|
43
|
-
|
44
|
-
backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
|
45
|
-
class << self
|
46
|
-
backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
|
47
|
-
end
|
48
|
-
|
49
|
-
attr_accessor :percolations
|
50
|
-
define_model_callbacks :index, :percolate, :only => :after
|
51
|
-
after_commit :update_index_on_create_backgrounded, :if => :should_index?, :on => :create
|
52
|
-
after_commit :update_index_on_update_backgrounded, :if => :should_index?, :on => :update
|
53
|
-
after_commit :delete_from_index, :on => :destroy
|
54
|
-
end
|
55
|
-
# override default after_index callback definition to support :on option
|
56
|
-
# see ActiveRecord::Transactions::ClassMethods#after_commit for example
|
57
|
-
def after_index(*args, &block)
|
58
|
-
options = args.last
|
59
|
-
if options.is_a?(Hash) && options[:on]
|
60
|
-
options[:if] = Array.wrap(options[:if])
|
61
|
-
options[:if] << "@index_lifecycle == :#{options[:on]}"
|
62
|
-
end
|
63
|
-
set_callback(:index, :after, *args, &block)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|