elastic_searchable 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,6 +1,31 @@
1
1
  = elastic_searchable
2
2
 
3
- Description goes here.
3
+ Integrate the elasticsearch library into Rails.
4
+
5
+ == Usage
6
+ class Blog < ActiveRecord::Base
7
+ elastic_searchable
8
+ end
9
+
10
+ results = Blog.search 'foo'
11
+
12
+ == Features
13
+
14
+ * fast. fast! FAST! 30% faster than rubberband on average.
15
+ * active record callbacks automatically keep search index up to date as your data changes
16
+ * out of the box background indexing of data using backgrounded. Don't lock up a foreground process waiting on a background job!
17
+ * integrates with will_paginate library for easy pagination of search results
18
+
19
+ == Installation
20
+ #Gemfile
21
+ gem 'elastic_searchable'
22
+
23
+ == Configuration
24
+
25
+ #config/initializers/elastic_searchable.rb
26
+ #customize elasticsearch host
27
+ #defaults to localhost:9200
28
+ ElasticSearchable.base_uri = 'server:9200'
4
29
 
5
30
  == Contributing to elastic_searchable
6
31
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.4.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{elastic_searchable}
8
- s.version = "0.3.1"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Ryan Sonnek"]
12
- s.date = %q{2011-02-03}
12
+ s.date = %q{2011-02-22}
13
13
  s.description = %q{integrate the elastic search engine with rails}
14
14
  s.email = %q{ryan@codecrate.com}
15
15
  s.extra_rdoc_files = [
@@ -36,7 +36,7 @@ Gem::Specification.new do |s|
36
36
  s.homepage = %q{http://github.com/wireframe/elastic_searchable}
37
37
  s.licenses = ["MIT"]
38
38
  s.require_paths = ["lib"]
39
- s.rubygems_version = %q{1.5.0}
39
+ s.rubygems_version = %q{1.5.2}
40
40
  s.summary = %q{elastic search for activerecord}
41
41
  s.test_files = [
42
42
  "test/helper.rb",
@@ -26,51 +26,22 @@ module ElasticSearchable
26
26
  options.symbolize_keys!
27
27
  self.elastic_options = options
28
28
 
29
- extend ElasticSearchable::ActiveRecord::Index
29
+ extend ElasticSearchable::Indexing::ClassMethods
30
30
  extend ElasticSearchable::Queries
31
31
 
32
- include ElasticSearchable::ActiveRecord::InstanceMethods
33
- include ElasticSearchable::Callbacks
32
+ include ElasticSearchable::Indexing::InstanceMethods
33
+ include ElasticSearchable::Callbacks::InstanceMethods
34
34
 
35
- add_indexing_callbacks
36
- end
37
- end
38
-
39
- module InstanceMethods
40
- def indexed_json_document
41
- self.as_json self.class.elastic_options[:json]
42
- end
43
- def index_in_elastic_search(lifecycle = nil)
44
- ElasticSearchable.request :put, self.class.index_type_path(self.id), :body => self.indexed_json_document.to_json
45
-
46
- self.run_callbacks("after_index_on_#{lifecycle}".to_sym) if lifecycle
47
- self.run_callbacks(:after_index)
48
- end
49
- def should_index?
50
- [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
51
- ![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
52
- end
53
-
54
- private
55
- #ripped from activesupport
56
- def evaluate_elastic_condition(method)
57
- case method
58
- when Symbol
59
- self.send method
60
- when String
61
- eval(method, self.instance_eval { binding })
62
- when Proc, Method
63
- method.call
64
- else
65
- if method.respond_to?(kind)
66
- method.send kind
67
- else
68
- raise ArgumentError,
69
- "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
70
- "a block to be invoked, or an object responding to the callback method."
71
- end
35
+ backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
36
+ class << self
37
+ backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
72
38
  end
39
+
40
+ define_callbacks :after_index_on_create, :after_index_on_update, :after_index
41
+ after_commit_on_create :update_index_on_create_backgrounded, :if => :should_index?
42
+ after_commit_on_update :update_index_on_update_backgrounded, :if => :should_index?
43
+ after_commit_on_destroy :delete_from_index
73
44
  end
74
45
  end
75
46
  end
76
- end
47
+ end
@@ -1,31 +1,22 @@
1
1
  module ElasticSearchable
2
2
  module Callbacks
3
- def self.included(base)
4
- base.send :extend, ClassMethods
5
- end
6
- def self.backgrounded_options
7
- {:queue => 'elasticsearch'}
8
- end
9
-
10
- module ClassMethods
11
- def add_indexing_callbacks
12
- backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
13
- class << self
14
- backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
15
- end
16
-
17
- define_callbacks :after_index_on_create, :after_index_on_update, :after_index
18
- after_commit_on_create :update_index_on_create_backgrounded, :if => :should_index?
19
- after_commit_on_update :update_index_on_update_backgrounded, :if => :should_index?
20
- after_commit_on_destroy Proc.new {|o| o.class.delete_id_from_index_backgrounded(o.id) }
3
+ class << self
4
+ def backgrounded_options
5
+ {:queue => 'elasticsearch'}
21
6
  end
22
7
  end
23
8
 
24
- def update_index_on_create
25
- index_in_elastic_search :create
26
- end
27
- def update_index_on_update
28
- index_in_elastic_search :update
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
+ index_in_elastic_search :create
16
+ end
17
+ def update_index_on_update
18
+ index_in_elastic_search :update
19
+ end
29
20
  end
30
21
  end
31
22
  end
@@ -1,17 +1,6 @@
1
1
  module ElasticSearchable
2
- module ActiveRecord
3
- module Index
4
-
5
- # helper method to clean out existing index and reindex all objects
6
- def rebuild_index
7
- self.clean_index
8
- self.update_index_mapping
9
- self.find_each do |record|
10
- record.index_in_elastic_search if record.should_index?
11
- end
12
- self.refresh_index
13
- end
14
-
2
+ module Indexing
3
+ module ClassMethods
15
4
  # delete all documents of this type in the index
16
5
  # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_mapping/
17
6
  def clean_index
@@ -29,7 +18,9 @@ module ElasticSearchable
29
18
  # create the index
30
19
  # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/create_index/
31
20
  def create_index
32
- ElasticSearchable.request :put, index_path
21
+ options = self.elastic_options[:index_options] ? self.elastic_options[:index_options].to_json : ''
22
+ ElasticSearchable.request :put, index_path, :body => options
23
+ self.update_index_mapping
33
24
  end
34
25
 
35
26
  # explicitly refresh the index, making all operations performed since the last refresh
@@ -70,5 +61,61 @@ module ElasticSearchable
70
61
  self.elastic_options[:type] || self.table_name
71
62
  end
72
63
  end
64
+
65
+ module InstanceMethods
66
+ # index the object in elasticsearch
67
+ # fire after_index callbacks after operation is complete
68
+ # see http://www.elasticsearch.org/guide/reference/api/index_.html
69
+ def index_in_elastic_search(lifecycle = nil)
70
+ query = {}
71
+ query.merge! :percolate => "*" if self.class.elastic_options[:percolate]
72
+ response = ElasticSearchable.request :put, self.class.index_type_path(self.id), :query => query, :body => self.indexed_json_document.to_json
73
+
74
+ self.run_callbacks("after_index_on_#{lifecycle}".to_sym) if lifecycle
75
+ self.run_callbacks(:after_index)
76
+
77
+ if percolate_callback = self.class.elastic_options[:percolate]
78
+ matches = response['matches']
79
+ self.send percolate_callback, matches if matches.any?
80
+ end
81
+ end
82
+ # document to index in elasticsearch
83
+ def indexed_json_document
84
+ self.as_json self.class.elastic_options[:json]
85
+ end
86
+ def should_index?
87
+ [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
88
+ ![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
89
+ end
90
+ # percolate this object to see what registered searches match
91
+ # can be done on transient/non-persisted objects!
92
+ # can be done automatically when indexing using :percolate => true config option
93
+ # http://www.elasticsearch.org/blog/2011/02/08/percolator.html
94
+ def percolate
95
+ response = ElasticSearchable.request :get, self.class.index_type_path('_percolate'), :body => {:doc => self.indexed_json_document}.to_json
96
+ response['matches']
97
+ end
98
+
99
+ private
100
+ # ripped from activesupport
101
+ def evaluate_elastic_condition(method)
102
+ case method
103
+ when Symbol
104
+ self.send method
105
+ when String
106
+ eval(method, self.instance_eval { binding })
107
+ when Proc, Method
108
+ method.call
109
+ else
110
+ if method.respond_to?(kind)
111
+ method.send kind
112
+ else
113
+ raise ArgumentError,
114
+ "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
115
+ "a block to be invoked, or an object responding to the callback method."
116
+ end
117
+ end
118
+ end
119
+ end
73
120
  end
74
121
  end
@@ -23,14 +23,16 @@ module ElasticSearchable
23
23
  def request(method, url, options = {})
24
24
  response = self.send(method, url, options)
25
25
  #puts "elasticsearch request: #{method} #{url} #{" finished in #{response['took']}ms" if response['took']}"
26
- assert_ok_response response
26
+ validate_response response
27
27
  response
28
28
  end
29
29
 
30
30
  private
31
- def assert_ok_response(response)
31
+ # all elasticsearch rest calls return a json response when an error occurs. ex:
32
+ # {error: 'an error occurred' }
33
+ def validate_response(response)
32
34
  error = response['error'] || "Error executing request: #{response.inspect}"
33
- raise ElasticSearchable::ElasticError.new(error) if response['error'] || !response.response.is_a?(Net::HTTPOK)
35
+ raise ElasticSearchable::ElasticError.new(error) if response['error'] || ![Net::HTTPOK, Net::HTTPCreated].include?(response.response.class)
34
36
  end
35
37
  end
36
38
  end
data/test/helper.rb CHANGED
@@ -20,4 +20,7 @@ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
20
20
  ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
21
21
 
22
22
  class Test::Unit::TestCase
23
+ def delete_index
24
+ ElasticSearchable.delete '/elastic_searchable' rescue nil
25
+ end
23
26
  end
@@ -1,24 +1,5 @@
1
1
  require File.join(File.dirname(__FILE__), 'helper')
2
2
 
3
- module ElasticSearch
4
- class Client
5
- def index_mapping(*args)
6
- options = args.last.is_a?(Hash) ? args.pop : {}
7
- indices = args.empty? ? [(default_index || :all)] : args.flatten
8
- indices.collect! { |i| [:all].include?(i) ? "_#{i}" : i }
9
- execute(:index_mapping, indices, options)
10
- end
11
- end
12
- module Transport
13
- class HTTP
14
- def index_mapping(index_list, options={})
15
- standard_request(:get, {:index => index_list, :op => "_mapping"})
16
- end
17
- end
18
- end
19
- end
20
-
21
-
22
3
  class TestElasticSearchable < Test::Unit::TestCase
23
4
  ActiveRecord::Schema.define(:version => 1) do
24
5
  create_table :posts, :force => true do |t|
@@ -36,10 +17,19 @@ class TestElasticSearchable < Test::Unit::TestCase
36
17
  t.column :name, :string
37
18
  t.column :favorite_color, :string
38
19
  end
20
+ create_table :books, :force => true do |t|
21
+ t.column :title, :string
22
+ end
39
23
  end
40
24
 
25
+ def setup
26
+ delete_index
27
+ end
28
+ def teardown
29
+ delete_index
30
+ end
41
31
  class Post < ActiveRecord::Base
42
- elastic_searchable
32
+ elastic_searchable :index_options => { "analysis.analyzer.default.tokenizer" => 'standard', "analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem'] }
43
33
  after_index :indexed
44
34
  after_index_on_create :indexed_on_create
45
35
  def indexed
@@ -67,7 +57,7 @@ class TestElasticSearchable < Test::Unit::TestCase
67
57
  end
68
58
  end
69
59
 
70
- context 'requesting invalid url' do
60
+ context 'ElasticSearchable.request with invalid url' do
71
61
  should 'raise error' do
72
62
  assert_raises ElasticSearchable::ElasticError do
73
63
  ElasticSearchable.request :get, '/elastic_searchable/foobar/notfound'
@@ -75,22 +65,31 @@ class TestElasticSearchable < Test::Unit::TestCase
75
65
  end
76
66
  end
77
67
 
78
- context 'with empty index' do
68
+ context 'Post.create_index' do
79
69
  setup do
80
- begin
81
- ElasticSearchable.delete '/elastic_searchable'
82
- rescue ElasticSearchable::ElasticError
83
- #already deleted
84
- end
70
+ Post.create_index
71
+ @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
72
+ end
73
+ should 'have created index' do
74
+ assert @status['ok']
75
+ end
76
+ should 'have used custom index_options' do
77
+ expected = {
78
+ "index.number_of_replicas" => "1",
79
+ "index.number_of_shards" => "5",
80
+ "index.analysis.analyzer.default.tokenizer" => "standard",
81
+ "index.analysis.analyzer.default.filter.0" => "standard",
82
+ "index.analysis.analyzer.default.filter.1" => "lowercase",
83
+ "index.analysis.analyzer.default.filter.2" => "porterStem"
84
+ }
85
+ assert_equal expected, @status['indices']['elastic_searchable']['settings'], @status.inspect
85
86
  end
87
+ end
86
88
 
87
- context 'Post.create_index' do
88
- setup do
89
- Post.create_index
90
- @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
91
- end
92
- should 'have created index' do
93
- assert @status['ok']
89
+ context 'deleting object that does not exist in search index' do
90
+ should 'raise error' do
91
+ assert_raises ElasticSearchable::ElasticError do
92
+ Post.delete_id_from_index 123
94
93
  end
95
94
  end
96
95
  end
@@ -109,10 +108,9 @@ class TestElasticSearchable < Test::Unit::TestCase
109
108
 
110
109
  context 'with index containing multiple results' do
111
110
  setup do
112
- Post.delete_all
111
+ Post.create_index
113
112
  @first_post = Post.create :title => 'foo', :body => "first bar"
114
113
  @second_post = Post.create :title => 'foo', :body => "second bar"
115
- Post.rebuild_index
116
114
  Post.refresh_index
117
115
  end
118
116
 
@@ -121,7 +119,7 @@ class TestElasticSearchable < Test::Unit::TestCase
121
119
  @results = Post.search 'first'
122
120
  end
123
121
  should 'find created object' do
124
- assert_equal @first_post, @results.first
122
+ assert_contains @results, @first_post
125
123
  end
126
124
  should 'be paginated' do
127
125
  assert_equal 1, @results.current_page
@@ -135,8 +133,11 @@ class TestElasticSearchable < Test::Unit::TestCase
135
133
  setup do
136
134
  @results = Post.search 'foo', :page => 2, :per_page => 1, :sort => 'id'
137
135
  end
138
- should 'find object' do
139
- assert_equal @second_post, @results.first
136
+ should 'not find objects from first page' do
137
+ assert_does_not_contain @results, @first_post
138
+ end
139
+ should 'find second object' do
140
+ assert_contains @results, @second_post
140
141
  end
141
142
  should 'be paginated' do
142
143
  assert_equal 2, @results.current_page
@@ -152,6 +153,7 @@ class TestElasticSearchable < Test::Unit::TestCase
152
153
  end
153
154
  should 'sort results correctly' do
154
155
  assert_equal @second_post, @results.first
156
+ assert_equal @first_post, @results.last
155
157
  end
156
158
  end
157
159
 
@@ -174,21 +176,17 @@ class TestElasticSearchable < Test::Unit::TestCase
174
176
  false
175
177
  end
176
178
  end
177
- context 'activerecord class with :if=>proc' do
179
+ context 'activerecord class with optional :if=>proc configuration' do
178
180
  context 'when creating new instance' do
179
181
  setup do
180
182
  Blog.any_instance.expects(:index_in_elastic_search).never
181
- Blog.create! :title => 'foo'
183
+ @blog = Blog.create! :title => 'foo'
182
184
  end
183
185
  should 'not index record' do end #see expectations
184
- end
185
- context 'rebuilding new index' do
186
- setup do
187
- Blog.any_instance.expects(:index_in_elastic_search).never
188
- Blog.create! :title => 'foo'
189
- Blog.rebuild_index
186
+ should 'not be found in elasticsearch' do
187
+ @request = ElasticSearchable.get "/elastic_searchable/blogs/#{@blog.id}"
188
+ assert @request.response.is_a?(Net::HTTPNotFound), @request.inspect
190
189
  end
191
- should 'not index record' do end #see expectations
192
190
  end
193
191
  end
194
192
 
@@ -198,7 +196,7 @@ class TestElasticSearchable < Test::Unit::TestCase
198
196
  context 'activerecord class with :mapping=>{}' do
199
197
  context 'creating index' do
200
198
  setup do
201
- User.update_index_mapping
199
+ User.create_index
202
200
  @status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
203
201
  end
204
202
  should 'have set mapping' do
@@ -217,15 +215,15 @@ class TestElasticSearchable < Test::Unit::TestCase
217
215
  class Friend < ActiveRecord::Base
218
216
  elastic_searchable :json => {:only => [:name]}
219
217
  end
220
- context 'activerecord class with :json=>{}' do
218
+ context 'activerecord class with optiona :json config' do
221
219
  context 'creating index' do
222
220
  setup do
223
- Friend.delete_all
221
+ Friend.create_index
224
222
  @friend = Friend.create! :name => 'bob', :favorite_color => 'red'
225
- Friend.rebuild_index
223
+ Friend.refresh_index
226
224
  end
227
225
  should 'index json with configuration' do
228
- @response = ElasticSearchable.request :get, "/friends/friends/#{@friend.id}"
226
+ @response = ElasticSearchable.request :get, "/elastic_searchable/friends/#{@friend.id}"
229
227
  expected = {
230
228
  "name" => 'bob' #favorite_color should not be indexed
231
229
  }
@@ -245,4 +243,44 @@ class TestElasticSearchable < Test::Unit::TestCase
245
243
  assert_equal 'my_new_index', ElasticSearchable.default_index
246
244
  end
247
245
  end
246
+
247
+ class Book < ActiveRecord::Base
248
+ elastic_searchable :percolate => :on_percolated
249
+ def on_percolated(percolated)
250
+ @percolated = percolated
251
+ end
252
+ def percolated
253
+ @percolated
254
+ end
255
+ end
256
+ context 'Book class with percolate=true' do
257
+ context 'with created index' do
258
+ setup do
259
+ Book.create_index
260
+ end
261
+ context "when index has configured percolation" do
262
+ setup do
263
+ ElasticSearchable.request :put, '/_percolator/elastic_searchable/myfilter', :body => {:query => {:query_string => {:query => 'foo' }}}.to_json
264
+ ElasticSearchable.request :post, '/_percolator/_refresh'
265
+ end
266
+ context 'creating an object that matches the percolation' do
267
+ setup do
268
+ @book = Book.create :title => "foo"
269
+ end
270
+ should 'return percolated matches in the callback' do
271
+ assert_equal ['myfilter'], @book.percolated
272
+ end
273
+ end
274
+ context 'percolating a non-persisted object' do
275
+ setup do
276
+ @matches = Book.new(:title => 'foo').percolate
277
+ end
278
+ should 'return percolated matches' do
279
+ assert_equal ['myfilter'], @matches
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
248
285
  end
286
+
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_searchable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 1
10
- version: 0.3.1
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ryan Sonnek
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-02-03 00:00:00 -06:00
18
+ date: 2011-02-22 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -243,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
243
  requirements: []
244
244
 
245
245
  rubyforge_project:
246
- rubygems_version: 1.5.0
246
+ rubygems_version: 1.5.2
247
247
  signing_key:
248
248
  specification_version: 3
249
249
  summary: elastic search for activerecord