Skip to content

Commit

Permalink
[ACTIVEMODEL] Implemented all Tire features for models via a `MyModel…
Browse files Browse the repository at this point in the history
….tire` and `MyModel#tire` proxies for better isolation

Brings the Tire methods into the model class only when not trampling on someone other's foot (`settings`, `update_index`, etc.)

These three calls are thus equivalent:

    class Article
      # ...
      mapping do
        indexes :id, :type => 'string', :index => :not_analyzed
      end
    end

    class Article
      # ...
      tire.mapping do
        indexes :id, :type => 'string', :index => :not_analyzed
      end
    end

    class Article
      # ...
      tire do
        mapping do
          indexes :id, :type => 'string', :index => :not_analyzed
        end
      end
    end

Closes karmi#8, closes karmi#9.
  • Loading branch information
karmi committed Sep 1, 2011
1 parent afbd4ce commit 1255bfc
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 140 deletions.
48 changes: 46 additions & 2 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,53 @@ In this case, just wrap the `mapping` method in a `settings` one, passing it the
```

It may well be reasonable to wrap the index creation logic declared with `Tire.index('urls').create`
in a class method of your model, in a module method, etc, so have better control on index creation when bootstrapping your application with Rake tasks or when setting up the test suite.
in a class method of your model, in a module method, etc, so have better control on index creation when
bootstrapping your application with Rake tasks or when setting up the test suite.
_Tire_ will not hold that against you.

You may have just stopped wondering: what if I have my own `settings` class method defined?
Or what if some other gem defines `settings`, or some other _Tire_ method, such as `update_index`?
Things will break, right? No, they won't.

In fact, all this time you've been using only _proxies_ to the real _Tire_ methods, which live in the `tire`
class and instance methods of your model. Only when not trampling on someone's foot — which is the majority
of cases —, will _Tire_ bring its methods to the namespace of your class.

So, instead of writing `Article.search`, you could write `Article.tire.search`, and instead of
`@article.update_index` you could write `@article.tire.update_index`, to be on the safe side.
Let's have a look on an example with the `mapping` method:

```ruby
class Article < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks

tire.mapping do
indexes :id, :type => 'string', :index => :not_analyzed
# ...
end
end
```

Of course, you could also use the block form:

```ruby
class Article < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks

tire do
mapping do
indexes :id, :type => 'string', :index => :not_analyzed
# ...
end
end
end
```

Internally, _Tire_ uses these proxy methods exclusively. When you run into issues,
use the proxied method, eg. `Article.tire.mapping`, directly.

When you want a tight grip on how the attributes are added to the index, just
implement the `to_indexed_json` method in your model.

Expand Down Expand Up @@ -529,7 +573,7 @@ so you can pass all the usual parameters to the `search` method in the controlle
OK. Chances are, you have lots of records stored in your database. How will you get them to _ElasticSearch_? Easy:

```ruby
Article.elasticsearch_index.import Article.all
Article.index.import Article.all
```

This way, however, all your records are loaded into memory, serialized into JSON,
Expand Down
2 changes: 1 addition & 1 deletion lib/tire.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
require 'tire/model/naming'
require 'tire/model/callbacks'
require 'tire/model/percolate'
require 'tire/model/search'
require 'tire/model/indexing'
require 'tire/model/import'
require 'tire/model/search'
require 'tire/model/persistence/finders'
require 'tire/model/persistence/attributes'
require 'tire/model/persistence/storage'
Expand Down
6 changes: 3 additions & 3 deletions lib/tire/model/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module Callbacks

def self.included(base)
if base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
base.send :after_save, :update_elastic_search_index
base.send :after_destroy, :update_elastic_search_index
base.send :after_save, lambda { tire.update_index }
base.send :after_destroy, lambda { tire.update_index }
end

if base.respond_to?(:before_destroy) && !base.instance_methods.map(&:to_sym).include?(:destroyed?)
Expand All @@ -17,7 +17,7 @@ def destroyed?; !!@destroyed; end
end

base.class_eval do
define_model_callbacks(:update_elastic_search_index, :only => [:after, :before])
define_model_callbacks(:update_elasticsearch_index, :only => [:after, :before])
end if base.respond_to?(:define_model_callbacks)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/tire/model/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module ClassMethods

def import options={}, &block
method = options.delete(:method) || 'paginate'
self.elasticsearch_index.import self, method, options, &block
index.import klass, method, options, &block
end

end
Expand Down
4 changes: 2 additions & 2 deletions lib/tire/model/indexing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def store_mapping?
end

def create_elasticsearch_index
unless elasticsearch_index.exists?
elasticsearch_index.create :mappings => mapping_to_hash, :settings => settings
unless index.exists?
index.create :mappings => mapping_to_hash, :settings => settings
end
end

Expand Down
8 changes: 4 additions & 4 deletions lib/tire/model/naming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ module Naming
module ClassMethods
def index_name name=nil
@index_name = name if name
@index_name || model_name.plural
@index_name || klass.model_name.plural
end

def document_type
model_name.singular
klass.model_name.singular
end
end

module InstanceMethods
def index_name
self.class.index_name
instance.class.tire.index_name
end

def document_type
self.class.document_type
instance.class.tire.document_type
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/tire/model/percolate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def percolate!(pattern=true)

def on_percolate(pattern=true,&block)
percolate!(pattern)
after_update_elastic_search_index(block)
klass.after_update_elasticsearch_index(block)
end

def percolator
Expand All @@ -22,15 +22,15 @@ def percolator
module InstanceMethods

def percolate(&block)
index.percolate self, block
index.percolate instance, block
end

def percolate=(pattern)
@_percolator = pattern
end

def percolator
@_percolator || self.class.percolator || nil
@_percolator || instance.class.tire.percolator || nil
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/tire/model/persistence/storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def update_attributes(attributes={})
def save
return false unless valid?
run_callbacks :save do
# Document#id is set in the +update_elastic_search_index+ method,
# Document#id is set in the +update_elasticsearch_index+ method,
# where we have access to the JSON response
end
self
Expand Down
152 changes: 97 additions & 55 deletions lib/tire/model/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,6 @@ module Model

module Search

def self.included(base)
base.class_eval do
extend Tire::Model::Naming::ClassMethods
include Tire::Model::Naming::InstanceMethods

extend Tire::Model::Indexing::ClassMethods
extend Tire::Model::Import::ClassMethods

extend Tire::Model::Percolate::ClassMethods
include Tire::Model::Percolate::InstanceMethods

extend ClassMethods
include InstanceMethods

['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches'].each do |attr|
# TODO: Find a sane way to add attributes like _score for ActiveRecord -
# `define_attribute_methods [attr]` does not work in AR.
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
define_method("#{attr}") { @attributes[attr] }
end

def to_hash
self.serializable_hash
end unless instance_methods.map(&:to_sym).include?(:to_hash)
end

Results::Item.send :include, Loader
end

module ClassMethods

# Returns search results for a given query.
Expand Down Expand Up @@ -60,7 +31,7 @@ module ClassMethods
#
#
def search(*args, &block)
default_options = {:type => document_type, :index => elasticsearch_index.name}
default_options = {:type => document_type, :index => index.name}

if block_given?
options = args.shift || {}
Expand Down Expand Up @@ -91,15 +62,9 @@ def search(*args, &block)
s.perform.results
end

# Wrapper for the ES index for this class
#
# TODO: Implement some "forwardable" object named +tire+ for Tire mixins,
# and proxy everything via this object. If we're not stepping on
# other libs toes, extend/include also to the top level.
#
# The original culprit is Mongoid here, see https://github.com/karmi/tire/issues/7
# Wraps an Index instance for this class
#
def elasticsearch_index
def index
@index = Index.new(index_name)
end

Expand All @@ -108,32 +73,33 @@ def elasticsearch_index
module InstanceMethods

def index
self.class.elasticsearch_index
instance.class.tire.index
end

def update_elastic_search_index
_run_update_elastic_search_index_callbacks do
if destroyed?
index.remove self
def update_index
instance.send :_run_update_elasticsearch_index_callbacks do
if instance.destroyed?
index.remove instance
else
response = index.store( self, {:percolate => self.percolator} )
self.id ||= response['_id'] if self.respond_to?(:id=)
self._index = response['_index']
self._type = response['_type']
self._version = response['_version']
self.matches = response['matches']
response = index.store( instance, {:percolate => percolator} )
instance.id ||= response['_id'] if instance.respond_to?(:id=)
instance._index = response['_index'] if instance.respond_to?(:_index=)
instance._type = response['_type'] if instance.respond_to?(:_type=)
instance._version = response['_version'] if instance.respond_to?(:_version=)
instance.matches = response['matches'] if instance.respond_to?(:matches=)
self
end
end
end
alias :update_elasticsearch_index :update_elastic_search_index
alias :update_elasticsearch_index :update_index
alias :update_elastic_search_index :update_index

def to_indexed_json
if self.class.mapping.empty?
to_hash.to_json
if instance.class.tire.mapping.empty?
instance.to_hash.to_json
else
to_hash.
reject { |key, value| ! self.class.mapping.keys.map(&:to_s).include?(key.to_s) }.
instance.to_hash.
reject { |key, value| ! instance.class.tire.mapping.keys.map(&:to_s).include?(key.to_s) }.
to_json
end
end
Expand All @@ -150,7 +116,83 @@ def load(options=nil)

end

extend ClassMethods
class ClassMethodsProxy
include Tire::Model::Naming::ClassMethods
include Tire::Model::Import::ClassMethods
include Tire::Model::Indexing::ClassMethods
include Tire::Model::Percolate::ClassMethods
include ClassMethods

INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)

attr_reader :klass
def initialize(klass)
@klass = klass
end

end

class InstanceMethodsProxy
include Tire::Model::Naming::InstanceMethods
include Tire::Model::Percolate::InstanceMethods
include InstanceMethods

['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches'].each do |attr|
# TODO: Find a sane way to add attributes like _score for ActiveRecord -
# `define_attribute_methods [attr]` does not work in AR.
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
define_method("#{attr}") { @attributes[attr] }
end

INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)

attr_reader :instance
def initialize(instance)
@instance = instance
end
end

def self.included(base)
base.class_eval do
def self.tire &block
@__tire__ ||= ClassMethodsProxy.new(self)

@__tire__.instance_eval(&block) if block_given?
@__tire__
end

def tire &block
@__tire__ ||= InstanceMethodsProxy.new(self)

@__tire__.instance_eval(&block) if block_given?
@__tire__
end

def to_hash
self.serializable_hash
end unless instance_methods.map(&:to_sym).include?(:to_hash)

end

ClassMethodsProxy::INTERFACE.each do |method|
base.class_eval <<-"end;", __FILE__, __LINE__ unless base.public_methods.map(&:to_sym).include?(method.to_sym)
def self.#{method}(*args, &block)
tire.__send__(#{method.inspect}, *args, &block)
end
end;
end

InstanceMethodsProxy::INTERFACE.each do |method|
base.class_eval <<-"end;", __FILE__, __LINE__ unless base.instance_methods.map(&:to_sym).include?(method.to_sym)
def #{method}(*args, &block)
tire.__send__(#{method.inspect}, *args, &block)
end
end;
end

Results::Item.send :include, Loader
end

end

end
Expand Down
8 changes: 4 additions & 4 deletions lib/tire/tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ def elapsed_to_human(elapsed)
klass = eval(ENV['CLASS'].to_s)
params = eval(ENV['PARAMS'].to_s) || {}

index = Tire::Index.new( ENV['INDEX'] || klass.elasticsearch_index.name )
index = Tire::Index.new( ENV['INDEX'] || klass.tire.index.name )

if ENV['FORCE']
puts "[IMPORT] Deleting index '#{index.name}'"
index.delete
end

unless index.exists?
mapping = defined?(Yajl) ? Yajl::Encoder.encode(klass.mapping_to_hash, :pretty => true) :
MultiJson.encode(klass.mapping_to_hash)
mapping = defined?(Yajl) ? Yajl::Encoder.encode(klass.tire.mapping_to_hash, :pretty => true) :
MultiJson.encode(klass.tire.mapping_to_hash)
puts "[IMPORT] Creating index '#{index.name}' with mapping:", mapping
index.create :mappings => klass.mapping_to_hash
index.create :mappings => klass.tire.mapping_to_hash
end

STDOUT.sync = true
Expand Down
Loading

0 comments on commit 1255bfc

Please sign in to comment.