Skip to content

Commit 50ddcb3

Browse files
committed
[MODEL] Refactored the {Searching} module and the {Response} class to allow lazy loading
Previously, the Searching::ClassMethods#search method executed the search request immediately, and passed the Elasticsearch response as a Hash to the Response class initializer. This prevented for instance the common pattern of paginating search results in a Rails controller action: @articles = Article.search(params[:q]).page(params[:page]).records Here, the `search` method is re-initialized on each page load, immediately executing the search, and the `#page` call then performs another, separate request. With this patch, the Response class is initialized lazily, and the search request is performed only when `Response#response` is called (eg. via `Response#results`, `Response#records`, etc).
1 parent 2aa08cf commit 50ddcb3

17 files changed

+180
-77
lines changed

elasticsearch-model/lib/elasticsearch/model/adapters/active_record.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ module Records
1313
# Returns an `ActiveRecord::Relation` instance
1414
#
1515
def records
16-
sql_records = klass.where(id: @ids)
16+
sql_records = klass.where(id: ids)
1717

1818
# Re-order records based on the order from Elasticsearch hits
1919
# by redefining `to_a`, unless the user has called `order()`
2020
#
21-
sql_records.instance_exec(response['hits']['hits']) do |hits|
21+
sql_records.instance_exec(response.response['hits']['hits']) do |hits|
2222
define_singleton_method :to_a do
2323
if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
2424
self.load

elasticsearch-model/lib/elasticsearch/model/adapters/mongoid.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ module Records
1616
# Return a `Mongoid::Criteria` instance
1717
#
1818
def records
19-
criteria = klass.where(:id.in => @ids)
19+
criteria = klass.where(:id.in => ids)
2020

21-
criteria.instance_exec(response['hits']['hits']) do |hits|
21+
criteria.instance_exec(response.response['hits']['hits']) do |hits|
2222
define_singleton_method :to_a do
2323
self.entries.sort_by { |e| hits.index { |hit| hit['_id'].to_s == e.id.to_s } }
2424
end

elasticsearch-model/lib/elasticsearch/model/response.rb

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,52 @@ class Response
1818

1919
forward :results, :each, :empty?, :size, :slice, :[], :to_ary
2020

21-
def initialize(klass, search, response)
21+
def initialize(klass, search, options={})
2222
@klass = klass
2323
@search = search
24-
@response = response
25-
@took = response['took']
26-
@timed_out = response['timed_out']
27-
@shards = Hashie::Mash.new(response['_shards'])
2824
end
2925

30-
# Return the collection of "hits" from Elasticsearch
26+
# Returns the Elasticsearch response
3127
#
32-
def results
28+
# @return [Hash]
29+
#
30+
def response
3331
@response ||= search.execute!
34-
@results ||= Results.new(klass, response, nil, self)
3532
end
3633

37-
# Return the collection of records from the database
34+
# Returns the collection of "hits" from Elasticsearch
35+
#
36+
# @return [Results]
37+
#
38+
def results
39+
@results ||= Results.new(klass, self)
40+
end
41+
42+
# Returns the collection of records from the database
43+
#
44+
# @return [Records]
3845
#
3946
def records
40-
@response ||= search.execute!
41-
@records ||= Records.new(klass, response, results, self)
47+
@records ||= Records.new(klass, self)
4248
end
4349

50+
# Returns the "took" time
51+
#
52+
def took
53+
response['took']
54+
end
55+
56+
# Returns whether the response timed out
57+
#
58+
def timed_out
59+
response['timed_out']
60+
end
61+
62+
# Returns the statistics on shards
63+
#
64+
def shards
65+
Hashie::Mash.new(response['_shards'])
66+
end
4467
end
4568
end
4669
end

elasticsearch-model/lib/elasticsearch/model/response/base.rb

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@ module Response
44
# Common funtionality for classes in the {Elasticsearch::Model::Response} module
55
#
66
module Base
7-
attr_reader :klass, :response, :response_object,
8-
:total, :max_score
7+
attr_reader :klass, :response
98

109
# @param klass [Class] The name of the model class
1110
# @param response [Hash] The full response returned from Elasticsearch client
1211
# @param results [Results] The collection of results
1312
#
14-
def initialize(klass, response, results=nil, response_object=nil)
13+
def initialize(klass, response, options={})
1514
@klass = klass
16-
@response_object = response_object
1715
@response = response
18-
@total = response['hits']['total']
19-
@max_score = response['hits']['max_score']
2016
end
2117

2218
# @abstract Implement this method in specific class
@@ -31,6 +27,17 @@ def records
3127
raise NotImplemented, "Implement this method in #{klass}"
3228
end
3329

30+
# Returns the total number of hits
31+
#
32+
def total
33+
response.response['hits']['total']
34+
end
35+
36+
# Returns the max_score
37+
#
38+
def max_score
39+
response.response['hits']['max_score']
40+
end
3441
end
3542
end
3643
end

elasticsearch-model/lib/elasticsearch/model/response/pagination.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def self.included(base)
2020
Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::PageScopeMethods
2121
Elasticsearch::Model::Response::Records.__send__ :include, ::Kaminari::PageScopeMethods
2222

23-
Elasticsearch::Model::Response::Results.__send__ :forward, :response_object, :limit_value, :offset_value, :total_count
24-
Elasticsearch::Model::Response::Records.__send__ :forward, :response_object, :limit_value, :offset_value, :total_count
23+
Elasticsearch::Model::Response::Results.__send__ :forward, :response, :limit_value, :offset_value, :total_count
24+
Elasticsearch::Model::Response::Records.__send__ :forward, :response, :limit_value, :offset_value, :total_count
2525

2626
base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
2727
# Define the `page` Kaminari method

elasticsearch-model/lib/elasticsearch/model/response/records.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ class Records
1717

1818
# @see Base#initialize
1919
#
20-
def initialize(klass, response, results=nil, response_object=nil)
20+
def initialize(klass, response, options={})
2121
super
22-
@response = response
23-
@results = results
24-
@ids = response['hits']['hits'].map { |hit| hit['_id'] }
2522

2623
# Include module provided by the adapter in the singleton class ("metaclass")
2724
#
@@ -32,16 +29,28 @@ def initialize(klass, response, results=nil, response_object=nil)
3229
self
3330
end
3431

32+
# Returns the hit IDs
33+
#
34+
def ids
35+
response.response['hits']['hits'].map { |hit| hit['_id'] }
36+
end
37+
38+
# Returns the {Results} collection
39+
#
40+
def results
41+
response.results
42+
end
43+
3544
# Yields [record, hit] pairs to the block
3645
#
3746
def each_with_hit(&block)
38-
records.zip(@results).each(&block)
47+
records.zip(results).each(&block)
3948
end
4049

4150
# Yields [record, hit] pairs and returns the result
4251
#
4352
def map_with_hit(&block)
44-
records.zip(@results).map(&block)
53+
records.zip(results).map(&block)
4554
end
4655

4756
# Delegate methods to `@records`

elasticsearch-model/lib/elasticsearch/model/response/results.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ module Response
88
#
99
class Results
1010
include Base
11-
12-
attr_reader :results
13-
1411
include Enumerable
1512

1613
extend Support::Forwardable
1714
forward :results, :each, :empty?, :size, :slice, :[], :to_a, :to_ary
1815

1916
# @see Base#initialize
2017
#
21-
def initialize(klass, response, results=nil, response_object=nil)
18+
def initialize(klass, response, options={})
2219
super
20+
end
21+
22+
# Returns the {Results} collection
23+
#
24+
def results
2325
# TODO: Configurable custom wrapper
24-
@results = response['hits']['hits'].map { |hit| Result.new(hit) }
26+
@results = response.response['hits']['hits'].map { |hit| Result.new(hit) }
2527
end
2628

2729
end

elasticsearch-model/lib/elasticsearch/model/searching.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,8 @@ module ClassMethods
9696
#
9797
def search(query_or_payload, options={})
9898
search = SearchRequest.new(self, query_or_payload, options={})
99-
response = search.execute!
10099

101-
Response::Response.new(self, search, response)
100+
Response::Response.new(self, search)
102101
end
103102

104103
end

elasticsearch-model/test/integration/mongoid_basic_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def as_indexed_json(options={})
4747

4848
context "Mongoid integration" do
4949
setup do
50+
Elasticsearch::Model::Adapter.register \
51+
Elasticsearch::Model::Adapter::Mongoid,
52+
lambda { |klass| !!defined?(::Mongoid::Document) && klass.ancestors.include?(::Mongoid::Document) }
53+
5054
MongoidArticle.__elasticsearch__.create_index! force: true
5155

5256
MongoidArticle.delete_all

elasticsearch-model/test/unit/adapter_active_record_test.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,23 @@
33
class Elasticsearch::Model::AdapterActiveRecordTest < Test::Unit::TestCase
44
context "Adapter ActiveRecord module: " do
55
class ::DummyClassForActiveRecord
6+
RESPONSE = Struct.new('DummyActiveRecordResponse') do
7+
def response
8+
{ 'hits' => {'hits' => [ {'_id' => 2}, {'_id' => 1} ]} }
9+
end
10+
end.new
11+
612
def response
7-
{ 'hits' => {'hits' => [ {'_id' => 2}, {'_id' => 1} ]} }
13+
RESPONSE
14+
end
15+
16+
def ids
17+
[2, 1]
818
end
919
end
1020

21+
RESPONSE = { 'hits' => { 'total' => 123, 'max_score' => 456, 'hits' => [] } }
22+
1123
setup do
1224
@records = [ stub(id: 1, inspect: '<Model-1>'), stub(id: 2, inspect: '<Model-2>') ]
1325
@records.stubs(:load).returns(true)

0 commit comments

Comments
 (0)