Use item proxies for JSON caching
That is, once we get our list of IDs from the search engine, only fetch records whose JSON we don't already have cached. It's simpler here to use as_json, but it'd probably be even faster if I figure out how to serve a plain JSON string from a Rails controller. In the meantime, requests of entirely cached items are coming in at about 85ms on average on my box (dev, cache classes, many items), about 10ms better than the last iteration.
This commit is contained in:
parent
298eb46871
commit
e42de795dd
6 changed files with 107 additions and 17 deletions
|
@ -13,20 +13,29 @@ class ItemsController < ApplicationController
|
||||||
end
|
end
|
||||||
# Note that we sort by name by hand, since we might have to use
|
# Note that we sort by name by hand, since we might have to use
|
||||||
# fallbacks after the fact
|
# fallbacks after the fact
|
||||||
|
# TODO: use proxies for everything!
|
||||||
|
output_format = params[:format] == :html ? :records : :proxies
|
||||||
@items = Item::Search::Query.from_text(@query, current_user).
|
@items = Item::Search::Query.from_text(@query, current_user).
|
||||||
paginate(:page => params[:page], :per_page => per_page)
|
paginate(page: params[:page], per_page: per_page, as: output_format)
|
||||||
assign_closeted!
|
assign_closeted!
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render }
|
format.html { render }
|
||||||
format.json { render :json => {:items => @items, :total_pages => @items.total_pages} }
|
format.json {
|
||||||
format.js { render :json => {:items => @items, :total_pages => @items.total_pages}, :callback => params[:callback] }
|
@items.prepare_method(:as_json)
|
||||||
|
render json: {items: @items, total_pages: @items.total_pages}
|
||||||
|
}
|
||||||
|
format.js {
|
||||||
|
@items.prepare_method(:as_json)
|
||||||
|
render json: {items: @items, total_pages: @items.total_pages},
|
||||||
|
callback: params[:callback]
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elsif params.has_key?(:ids) && params[:ids].is_a?(Array)
|
elsif params.has_key?(:ids) && params[:ids].is_a?(Array)
|
||||||
@items = Item.includes(:translations).find(params[:ids])
|
@items = Item.includes(:translations).find(params[:ids])
|
||||||
assign_closeted!
|
assign_closeted!
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json { render :json => @items }
|
format.json { render json: @items }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
@ -35,7 +44,7 @@ class ItemsController < ApplicationController
|
||||||
@newest_items = Item.newest.includes(:translations).limit(18)
|
@newest_items = Item.newest.includes(:translations).limit(18)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
format.js { render :json => {:error => '$q required'}}
|
format.js { render json: {error: '$q required'}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -47,10 +56,10 @@ class ItemsController < ApplicationController
|
||||||
format.html do
|
format.html do
|
||||||
unless localized_fragment_exist?("items/#{@item.id} info")
|
unless localized_fragment_exist?("items/#{@item.id} info")
|
||||||
@occupied_zones = @item.occupied_zones(
|
@occupied_zones = @item.occupied_zones(
|
||||||
:scope => Zone.includes_translations.alphabetical
|
scope: Zone.includes_translations.alphabetical
|
||||||
)
|
)
|
||||||
@restricted_zones = @item.restricted_zones(
|
@restricted_zones = @item.restricted_zones(
|
||||||
:scope => Zone.includes_translations.alphabetical
|
scope: Zone.includes_translations.alphabetical
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -72,7 +81,7 @@ class ItemsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
@current_user_quantities = Hash.new(0) # default is zero
|
@current_user_quantities = Hash.new(0) # default is zero
|
||||||
hangers = current_user.closet_hangers.where(:item_id => @item.id).
|
hangers = current_user.closet_hangers.where(item_id: @item.id).
|
||||||
select([:owned, :list_id, :quantity])
|
select([:owned, :list_id, :quantity])
|
||||||
|
|
||||||
hangers.each do |hanger|
|
hangers.each do |hanger|
|
||||||
|
@ -122,8 +131,8 @@ class ItemsController < ApplicationController
|
||||||
@items = []
|
@items = []
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { flash.now[:alert] = e.message; render }
|
format.html { flash.now[:alert] = e.message; render }
|
||||||
format.json { render :json => {:error => e.message} }
|
format.json { render :json => {error: e.message} }
|
||||||
format.js { render :json => {:error => e.message},
|
format.js { render :json => {error: e.message},
|
||||||
:callback => params[:callback] }
|
:callback => params[:callback] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,13 +8,21 @@ module FlexSearchExtender
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxied_collection
|
||||||
|
Item.build_proxies(collection.map(&:_id)).tap do |proxies|
|
||||||
|
proxies.extend Flex::Result::Collection
|
||||||
|
proxies.setup(self['hits']['total'], variables)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def scoped_loaded_collection(options)
|
def scoped_loaded_collection(options)
|
||||||
options[:scopes] ||= {}
|
options[:scopes] ||= {}
|
||||||
@loaded_collection ||= begin
|
@loaded_collection ||= begin
|
||||||
records_by_class_and_id_str = {}
|
records_by_class_and_id_str = {}
|
||||||
# returns a structure like {Comment=>[{"_id"=>"123", ...}, {...}], BlogPost=>[...]}
|
grouped_collection = collection.group_by { |d|
|
||||||
h = collection.group_by { |d| d.mapped_class(should_raise=true) }
|
d.mapped_class(should_raise=true)
|
||||||
h.each do |klass, docs|
|
}
|
||||||
|
grouped_collection.each do |klass, docs|
|
||||||
record_ids = docs.map(&:_id)
|
record_ids = docs.map(&:_id)
|
||||||
scope = options[:scopes][klass.name] || klass.scoped
|
scope = options[:scopes][klass.name] || klass.scoped
|
||||||
records = scope.find(record_ids)
|
records = scope.find(record_ids)
|
||||||
|
|
|
@ -404,6 +404,10 @@ class Item < ActiveRecord::Base
|
||||||
items.values
|
items.values
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.build_proxies(ids)
|
||||||
|
Item::ProxyArray.new(ids)
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
MALL_HOST = 'ncmall.neopets.com'
|
MALL_HOST = 'ncmall.neopets.com'
|
||||||
MALL_MAIN_PATH = '/mall/shop.phtml'
|
MALL_MAIN_PATH = '/mall/shop.phtml'
|
||||||
|
|
45
app/models/item/proxy.rb
Normal file
45
app/models/item/proxy.rb
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
class Item
|
||||||
|
class Proxy
|
||||||
|
include FragmentLocalization
|
||||||
|
|
||||||
|
attr_reader :id
|
||||||
|
attr_writer :item
|
||||||
|
|
||||||
|
def initialize(id)
|
||||||
|
@id = id
|
||||||
|
@known_method_outputs = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_cached?(method_name)
|
||||||
|
# TODO: is there a way to cache nil? Right now we treat is as a miss.
|
||||||
|
# We eagerly read the cache rather than just check if the value exists,
|
||||||
|
# which will usually cut down on cache requests.
|
||||||
|
@known_method_outputs[method_name] ||= Rails.cache.read(
|
||||||
|
method_fragment_key(method_name))
|
||||||
|
!@known_method_outputs[method_name].nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def as_json(options={})
|
||||||
|
cache_method(:as_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def cache_method(method_name, &block)
|
||||||
|
# Two layers of cache: a local copy, in case the method is called again,
|
||||||
|
# and then the Rails cache, before we hit the actual method call.
|
||||||
|
@known_method_outputs[method_name] ||= begin
|
||||||
|
key = method_fragment_key(method_name)
|
||||||
|
Rails.cache.fetch(key) { item.send(method_name) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def item
|
||||||
|
@item ||= Item.find(@id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_fragment_key(method_name)
|
||||||
|
localize_fragment_key("item/#{@id}##{method_name}", I18n.locale)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
app/models/item/proxy_array.rb
Normal file
19
app/models/item/proxy_array.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
class Item
|
||||||
|
class ProxyArray < Array
|
||||||
|
METHOD_SCOPES = {as_json: Item.includes(:translations)}
|
||||||
|
|
||||||
|
def initialize(ids)
|
||||||
|
self.replace(ids.map { |id| Proxy.new(id.to_i) })
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_method(method_name)
|
||||||
|
missed_proxies_by_id = self.
|
||||||
|
reject { |p| p.method_cached?(method_name) }.
|
||||||
|
index_by(&:id)
|
||||||
|
item_scope = METHOD_SCOPES[method_name.to_sym] || Item.scoped
|
||||||
|
item_scope.find(missed_proxies_by_id.keys).each do |item|
|
||||||
|
missed_proxies_by_id[item.id].item = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -93,10 +93,15 @@ class Item
|
||||||
end
|
end
|
||||||
|
|
||||||
result = FlexSearch.item_search(final_flex_params)
|
result = FlexSearch.item_search(final_flex_params)
|
||||||
|
|
||||||
|
if options[:as] == :proxies
|
||||||
|
result.proxied_collection
|
||||||
|
else
|
||||||
result.scoped_loaded_collection(
|
result.scoped_loaded_collection(
|
||||||
:scopes => {'Item' => Item.includes(:translations)}
|
:scopes => {'Item' => Item.includes(:translations)}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Load the text query labels from I18n, so that when we see, say,
|
# Load the text query labels from I18n, so that when we see, say,
|
||||||
# the filter "species:acara", we know it means species_support_id.
|
# the filter "species:acara", we know it means species_support_id.
|
||||||
|
|
Loading…
Reference in a new issue