Skip to content
This repository has been archived by the owner on Jun 30, 2018. It is now read-only.

Commit

Permalink
[ACTIVEMODEL] Added the :load option for search to eagerly load mod…
Browse files Browse the repository at this point in the history
…el instances from the database

In ActiveModel/ActiveRecord implementation, there's an option to load the "real",
underlying model with the `load` method. This allows
for loading the records only one-by-one, though.

An option as `:load => true` for the search methods will eagerly load all the records
from the database (in Results::Tire::Collection#results),
based on IDs returned from ElasticSearch:

    Article.search 'love', :load => true
    => Article.find [1, 2]

Any option other then TrueClass given as `:load` will be passed
to the model's `find` method:

    Article.search 'love', :include => 'comments'
    => Article.find [1, 2], :include => 'comments'

This allows to use Tire (and ElasticSearch) to retrieve batches of records at once,
when needed (eg. in export jobs).

[Closes #61]
  • Loading branch information
karmi committed Aug 18, 2011
1 parent 0cc761e commit 1e34cde
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 16 deletions.
52 changes: 36 additions & 16 deletions lib/tire/results/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,42 @@ def initialize(response, options={})

def results
@results ||= begin
@response['hits']['hits'].map do |h|
if @wrapper == Hash then h
else
document = {}

# Update the document with content and ID
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
document.update( {'id' => h['_id']} )

# Update the document with meta information
['_score', '_type', '_index', '_version', 'sort', 'highlight'].each { |key| document.update( {key => h[key]} || {} ) }

# Return an instance of the "wrapper" class
@wrapper.new(document)
end
end
unless @options[:load]
@response['hits']['hits'].map do |h|
if @wrapper == Hash then h
else
document = {}

# Update the document with content and ID
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
document.update( {'id' => h['_id']} )

# Update the document with meta information
['_score', '_type', '_index', '_version', 'sort', 'highlight'].each { |key| document.update( {key => h[key]} || {} ) }

# Return an instance of the "wrapper" class
@wrapper.new(document)
end
end
else
begin
type = @response['hits']['hits'].first['_type']
raise NoMethodError, "You have tried to eager load the model instances, " +
"but Tire cannot find the model class because " +
"document has no _type property." unless type

klass = type.camelize.constantize
ids = @response['hits']['hits'].map { |h| h['_id'] }
records = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])

# Reorder records to preserve order from search results
ids.map { |id| records.detect { |record| record.id.to_s == id.to_s } }
rescue NameError => e
raise NameError, "You have tried to eager load the model instances, but" +
"Tire cannot find the model class '#{type.camelize}' " +
"based on _type '#{type}'.", e.backtrace
end
end
end
end

Expand Down
27 changes: 27 additions & 0 deletions test/integration/active_record_searchable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,33 @@ def setup
assert_equal 'Test', results.first.title
end

context "with eager loading" do
setup do
ActiveRecordArticle.destroy_all
5.times { |n| ActiveRecordArticle.create! :title => "Test #{n+1}" }
ActiveRecordArticle.elasticsearch_index.refresh
end

should "load records on query search" do
results = ActiveRecordArticle.search '"Test 1"', :load => true

assert_equal ActiveRecordArticle.find(1), results.first
end

should "load records on block search" do
results = ActiveRecordArticle.search nil, :load => true do
query { string '"Test 1"' }
end

assert_equal ActiveRecordArticle.find(1), results.first
end

should "load records with options on query search" do
assert_equal ActiveRecordArticle.find(['1', '2'], :include => 'comments'),
ActiveRecordArticle.search('"Test 1" OR "Test 2"', :load => { :include => 'comments' }).results
end
end

should "remove document from index on destroy" do
a = ActiveRecordArticle.new :title => 'Test'
a.save!
Expand Down
48 changes: 48 additions & 0 deletions test/unit/results_collection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,54 @@ class ResultsCollectionTest < Test::Unit::TestCase

end

context "with eager loading" do
setup do
@response = { 'hits' => { 'hits' => [ {'_id' => 1, '_type' => 'active_record_article'},
{'_id' => 2, '_type' => 'active_record_article'},
{'_id' => 3, '_type' => 'active_record_article'}] } }
ActiveRecordArticle.stubs(:inspect).returns("<ActiveRecordArticle>")
end

should "load the records via model find method from database" do
ActiveRecordArticle.expects(:find).with([1,2,3]).
returns([ Results::Item.new(:id => 3),
Results::Item.new(:id => 1),
Results::Item.new(:id => 2) ])
Results::Collection.new(@response, :load => true).results
end

should "pass the :load option Hash to model find metod" do
ActiveRecordArticle.expects(:find).with([1,2,3], :include => 'comments').
returns([ Results::Item.new(:id => 3),
Results::Item.new(:id => 1),
Results::Item.new(:id => 2) ])
Results::Collection.new(@response, :load => { :include => 'comments' }).results
end

should "preserve the order of records returned from search" do
ActiveRecordArticle.expects(:find).with([1,2,3]).
returns([ Results::Item.new(:id => 3),
Results::Item.new(:id => 1),
Results::Item.new(:id => 2) ])
assert_equal [1,2,3], Results::Collection.new(@response, :load => true).results.map(&:id)
end

should "raise error when model class cannot be inferred from _type" do
assert_raise(NameError) do
response = { 'hits' => { 'hits' => [ {'_id' => 1, '_type' => 'hic_sunt_leones'}] } }
Results::Collection.new(response, :load => true).results
end
end

should "raise error when _type is missing" do
assert_raise(NoMethodError) do
response = { 'hits' => { 'hits' => [ {'_id' => 1}] } }
Results::Collection.new(response, :load => true).results
end
end

end

end

end
Expand Down

0 comments on commit 1e34cde

Please sign in to comment.