elastic_searchable 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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