Skip to content
This repository has been archived by the owner on Oct 5, 2023. It is now read-only.

Commit

Permalink
promote index level operations off of model and into ElasticSearchabl…
Browse files Browse the repository at this point in the history
…e namespace
  • Loading branch information
Ryan Sonnek committed Jan 23, 2012
1 parent c81387b commit af9467a
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .rvmrc
@@ -1 +1 @@
rvm use ruby-1.9.2@elastic_searchable --create
rvm use ruby-1.9.3@elastic_searchable --create
34 changes: 30 additions & 4 deletions lib/elastic_searchable.rb
Expand Up @@ -4,14 +4,13 @@
require 'elastic_searchable/active_record_extensions'

module ElasticSearchable
DEFAULT_INDEX = 'elastic_searchable'
include HTTParty
format :json
base_uri 'localhost:9200'

class ElasticError < StandardError; end
class << self
attr_accessor :logger, :default_index, :offline
attr_accessor :logger, :index_name, :index_settings, :offline

# execute a block of work without reindexing objects
def offline(&block)
Expand All @@ -34,7 +33,7 @@ def encode_json(options = {})
# ElasticSearchable.debug_output outputs all http traffic to console
def request(method, url, options = {})
options.merge! :headers => {'Content-Type' => 'application/json'}
options.merge! :body => ElasticSearchable.encode_json(options.delete(:json_body)) if options[:json_body]
options.merge! :body => self.encode_json(options.delete(:json_body)) if options[:json_body]

response = self.send(method, url, options)
logger.debug "elasticsearch request: #{method} #{url} #{"took #{response['took']}ms" if response['took']}"
Expand All @@ -47,6 +46,33 @@ def escape_query(string)
string.to_s.gsub(/([\(\)\[\]\{\}\?\\\"!\^\+\-\*:~])/,'\\\\\1')
end

# create the index
# http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html
def create_index
options = {}
options[:settings] = self.index_settings if self.index_settings
self.request :put, self.request_path, :json_body => options
end

# explicitly refresh the index, making all operations performed since the last refresh
# available for search
#
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/refresh/
def refresh_index
self.request :post, self.request_path('_refresh')
end

# deletes the entire index
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_index/
def delete_index
self.request :delete, self.request_path
end

# helper method to generate elasticsearch url for this index
def request_path(action = nil)
['', index_name, action].compact.join('/')
end

private
# all elasticsearch rest calls return a json response when an error occurs. ex:
# {error: 'an error occurred' }
Expand All @@ -63,5 +89,5 @@ def validate_response(response)

# configure default index to be elastic_searchable
# one index can hold many object 'types'
ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
ElasticSearchable.index_name = 'elastic_searchable'

9 changes: 7 additions & 2 deletions lib/elastic_searchable/active_record_extensions.rb
Expand Up @@ -8,9 +8,7 @@
module ElasticSearchable
module ActiveRecordExtensions
# Valid options:
# :index (optional) configure index to store data in. default to ElasticSearchable.default_index
# :type (optional) configue type to store data in. default to model table name
# :index_options (optional) configure index properties (ex: tokenizer)
# :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
# :if (optional) reference symbol/proc condition to only index when condition is true
# :unless (optional) reference symbol/proc condition to skip indexing when condition is true
Expand All @@ -29,6 +27,13 @@ def elastic_searchable(options = {})
cattr_accessor :elastic_options
self.elastic_options = options.symbolize_keys.merge(:unless => Array.wrap(options[:unless]).push(:elasticsearch_offline?))

if self.elastic_options[:index_options]
ActiveSupport::Deprecation.warn ":index_options has been deprecated. Use ElasticSearchable.index_settings instead.", caller
end
if self.elastic_options[:index]
ActiveSupport::Deprecation.warn ":index has been deprecated. Use ElasticSearchable.index_name instead.", caller
end

extend ElasticSearchable::Indexing::ClassMethods
extend ElasticSearchable::Queries

Expand Down
35 changes: 2 additions & 33 deletions lib/elastic_searchable/index.rb
Expand Up @@ -15,29 +15,6 @@ def update_index_mapping
end
end

# create the index
# http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html
def create_index
options = {}
options.merge! :settings => self.elastic_options[:index_options] if self.elastic_options[:index_options]
options.merge! :mappings => {index_type => self.elastic_options[:mapping]} if self.elastic_options[:mapping]
ElasticSearchable.request :put, index_path, :json_body => options
end

# explicitly refresh the index, making all operations performed since the last refresh
# available for search
#
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/refresh/
def refresh_index
ElasticSearchable.request :post, index_path('_refresh')
end

# deletes the entire index
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_index/
def delete_index
ElasticSearchable.request :delete, index_path
end

# delete one record from the index
# http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
def delete_id_from_index(id)
Expand All @@ -48,12 +25,7 @@ def delete_id_from_index(id)

# helper method to generate elasticsearch url for this object type
def index_type_path(action = nil)
index_path [index_type, action].compact.join('/')
end

# helper method to generate elasticsearch url for this index
def index_path(action = nil)
['', index_name, action].compact.join('/')
ElasticSearchable.request_path [index_type, action].compact.join('/')
end

# reindex all records using bulk api
Expand All @@ -78,7 +50,7 @@ def reindex(options = {})
next unless record.should_index?
begin
doc = ElasticSearchable.encode_json(record.as_json_for_index)
actions << ElasticSearchable.encode_json({:index => {'_index' => index_name, '_type' => index_type, '_id' => record.id}})
actions << ElasticSearchable.encode_json({:index => {'_index' => ElasticSearchable.index_name, '_type' => index_type, '_id' => record.id}})
actions << doc
rescue => e
ElasticSearchable.logger.warn "Unable to bulk index record: #{record.inspect} [#{e.message}]"
Expand All @@ -97,9 +69,6 @@ def reindex(options = {})
end

private
def index_name
self.elastic_options[:index] || ElasticSearchable.default_index
end
def index_type
self.elastic_options[:type] || self.table_name
end
Expand Down
2 changes: 1 addition & 1 deletion test/helper.rb
Expand Up @@ -19,6 +19,6 @@

class Test::Unit::TestCase
def delete_index
ElasticSearchable.delete '/elastic_searchable' rescue nil
ElasticSearchable.delete_index rescue nil
end
end
86 changes: 26 additions & 60 deletions test/test_elastic_searchable.rb
Expand Up @@ -3,9 +3,9 @@
class TestElasticSearchable < Test::Unit::TestCase
def setup
delete_index
ElasticSearchable.index_settings = {'number_of_replicas' => 0, 'number_of_shards' => 1}
# ElasticSearchable.debug_output
end
# ElasticSearchable.debug_output
SINGLE_NODE_CLUSTER_CONFIG = {'number_of_replicas' => 0, 'number_of_shards' => 1}

context 'non elastic activerecord class' do
class Parent < ActiveRecord::Base
Expand All @@ -29,7 +29,7 @@ class Parent < ActiveRecord::Base
end

class Post < ActiveRecord::Base
elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
elastic_searchable
after_index :indexed
after_index :indexed_on_create, :on => :create
after_index :indexed_on_update, :on => :update
Expand Down Expand Up @@ -76,10 +76,10 @@ def indexed_on_update?
end
end

context 'Model.create_index' do
context 'ElasticSearchable.create_index' do
setup do
Post.create_index
Post.refresh_index
ElasticSearchable.create_index
ElasticSearchable.refresh_index
@status = ElasticSearchable.request :get, '/elastic_searchable/_status'
end
should 'have created index' do
Expand Down Expand Up @@ -140,11 +140,11 @@ def indexed_on_update?
context 'with empty index when multiple database records' do
setup do
Post.delete_all
Post.create_index
ElasticSearchable.create_index
@first_post = Post.create :title => 'foo', :body => "first bar"
@second_post = Post.create :title => 'foo', :body => "second bar"
Post.delete_index
Post.create_index
ElasticSearchable.delete_index
ElasticSearchable.create_index
end
should 'not raise error if error occurs reindexing model' do
ElasticSearchable.expects(:request).raises(ElasticSearchable::ElasticError.new('faux error'))
Expand All @@ -161,7 +161,7 @@ def indexed_on_update?
context 'Model.reindex' do
setup do
Post.reindex :per_page => 1, :scope => Post.scoped(:order => 'body desc')
Post.refresh_index
ElasticSearchable.refresh_index
end
should 'have reindexed both records' do
assert_nothing_raised do
Expand All @@ -174,10 +174,10 @@ def indexed_on_update?

context 'with index containing multiple results' do
setup do
Post.create_index
ElasticSearchable.create_index
@first_post = Post.create :title => 'foo', :body => "first bar"
@second_post = Post.create :title => 'foo', :body => "second bar"
Post.refresh_index
ElasticSearchable.refresh_index
end

context 'searching for results' do
Expand Down Expand Up @@ -267,7 +267,7 @@ def indexed_on_update?
context 'destroying one object' do
setup do
@first_post.destroy
Post.refresh_index
ElasticSearchable.refresh_index
end
should 'be removed from the index' do
@request = ElasticSearchable.get "/elastic_searchable/posts/#{@first_post.id}"
Expand All @@ -278,7 +278,7 @@ def indexed_on_update?


class Blog < ActiveRecord::Base
elastic_searchable :if => proc {|b| b.should_index? }, :index_options => SINGLE_NODE_CLUSTER_CONFIG
elastic_searchable :if => proc {|b| b.should_index? }
def should_index?
false
end
Expand All @@ -297,54 +297,19 @@ def should_index?
end
end

class User < ActiveRecord::Base
elastic_searchable :index_options => {
'number_of_replicas' => 0,
'number_of_shards' => 1,
"analysis.analyzer.default.tokenizer" => 'standard',
"analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem']},
:mapping => {:properties => {:name => {:type => 'string', :index => 'not_analyzed'}}}
end
context 'activerecord class with :index_options and :mapping' do
context 'creating index' do
setup do
User.create_index
end
should 'have used custom index_options' do
@status = ElasticSearchable.request :get, '/elastic_searchable/_settings'
expected = {
"index.number_of_replicas" => "0",
"index.number_of_shards" => "1",
"index.analysis.analyzer.default.tokenizer" => "standard",
"index.analysis.analyzer.default.filter.0" => "standard",
"index.analysis.analyzer.default.filter.1" => "lowercase",
"index.analysis.analyzer.default.filter.2" => "porterStem"
}
assert_equal expected, @status['elastic_searchable']['settings'], @status.inspect
end
should 'have set mapping' do
@status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
expected = {
"name"=> {"type"=>"string", "index"=>"not_analyzed"}
}
assert_equal expected, @status['users']['properties'], @status.inspect
end
end
end

class Friend < ActiveRecord::Base
belongs_to :book
elastic_searchable :json => {:include => {:book => {:only => :title}}, :only => :name}, :index_options => SINGLE_NODE_CLUSTER_CONFIG
elastic_searchable :json => {:include => {:book => {:only => :title}}, :only => :name}
end
context 'activerecord class with optional :json config' do
context 'creating index' do
setup do
Friend.create_index
ElasticSearchable.create_index
@book = Book.create! :isbn => '123abc', :title => 'another world'
@friend = Friend.new :name => 'bob', :favorite_color => 'red'
@friend.book = @book
@friend.save!
Friend.refresh_index
ElasticSearchable.refresh_index
end
should 'index json with configuration' do
@response = ElasticSearchable.request :get, "/elastic_searchable/friends/#{@friend.id}"
Expand All @@ -360,15 +325,16 @@ class Friend < ActiveRecord::Base
end
end

context 'updating ElasticSearchable.default_index' do
context '.index_name' do
setup do
ElasticSearchable.default_index = 'my_new_index'
@orig_index_name = ElasticSearchable.index_name
ElasticSearchable.index_name = 'my_new_index'
end
teardown do
ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
ElasticSearchable.index_name = @orig_index_name
end
should 'change default index' do
assert_equal 'my_new_index', ElasticSearchable.default_index
assert_equal 'my_new_index', ElasticSearchable.index_name
end
end

Expand All @@ -385,7 +351,7 @@ def percolated
context 'Book class with after_percolate callback' do
context 'with created index' do
setup do
Book.create_index
ElasticSearchable.create_index
end
context "when index has configured percolation" do
setup do
Expand Down Expand Up @@ -443,17 +409,17 @@ def percolated
end

class MaxPageSizeClass < ActiveRecord::Base
elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
elastic_searchable
def self.max_per_page
1
end
end
context 'with 2 MaxPageSizeClass instances' do
setup do
MaxPageSizeClass.create_index
ElasticSearchable.create_index
@first = MaxPageSizeClass.create! :name => 'foo one'
@second = MaxPageSizeClass.create! :name => 'foo two'
MaxPageSizeClass.refresh_index
ElasticSearchable.refresh_index
end
context 'MaxPageSizeClass.search with default options and WillPaginate' do
setup do
Expand Down

0 comments on commit af9467a

Please sign in to comment.