impress/app/models/item/search/query.rb
Matchu 6e09b8bc10 globalized search first draft
Confirmed features:
    * Output (retrieval, sorting, etc.)
    * Name (positive and negative, but new behavior)
    * Flags (positive and negative)

Planned features:
    * users:owns, user:wants

Known issues:
    * Sets are broken
        * Don't render properly
        * Shouldn't actually be done as joined sets, anyway, since
          we actually want (set1_zone1 OR set1_zone2) AND
          (set2_zone1 OR set2_zone2), which will require breaking
          it into multiple terms queries.
    * Name has regressed: ignores phrases, doesn't require *all*
      words. While we're breaking sets into multiple queries,
      maybe we'll do something similar for name. In fact, we
      really kinda have to if we're gonna keep sorting by name,
      since "straw hat" returns all hats. Eww.
2013-01-24 18:24:35 -06:00

151 lines
4.9 KiB
Ruby

class Item
module Search
class Query
FIELD_CLASSES = {
:is_nc => Fields::Flag,
:is_pb => Fields::Flag,
:species_support_id => Fields::SetField,
:occupied_zone_id => Fields::SetField,
:restricted_zone_id => Fields::SetField,
:name => Fields::TextField
}
def initialize(filters, user)
@filters = filters
end
def fields
initial_fields.tap do |fields|
@filters.each { |filter| fields[filter.key] << filter }
end
end
def to_flex_params
fields.values.map(&:to_flex_params).inject(&:merge)
end
def paginate(options={})
begin
flex_params = self.to_flex_params
rescue Item::Search::Contradiction
# If we have a contradictory query, no need to raise a stink about
# it, but no need to actually run a search, either.
return []
end
final_flex_params = {
:page => (options[:page] || 1),
:size => (options[:per_page] || 30)
}.merge(flex_params)
locales = I18n.fallbacks[I18n.locale] &
I18n.locales_with_neopets_language_code
final_flex_params[:locale] = locales.first
if final_flex_params[:name] || final_flex_params[:negative_name]
locale_entries = locales.map do |locale|
boost = (locale == I18n.locale) ? 4 : 1
{:locale => locale, :boost => boost}
end
if final_flex_params[:name]
final_flex_params[:_name_locales] = locale_entries
end
if final_flex_params[:negative_name]
final_flex_params[:_negative_name_locales] = locale_entries
end
end
result = FlexSearch.item_search(final_flex_params)
result.scoped_loaded_collection(:scopes => {'Item' => Item.with_translations})
end
# Load the text query labels from I18n, so that when we see, say,
# the filter "species:acara", we know it means species_support_id.
TEXT_KEYS_BY_LABEL = {}
I18n.available_locales.each do |locale|
TEXT_KEYS_BY_LABEL[locale] = {}
FIELD_CLASSES.keys.each do |key|
# A locale can specify multiple labels for a key by separating by
# commas: "occupies,zone,type"
labels = I18n.translate("items.search.labels.#{key}",
:locale => locale).split(',')
labels.each { |label| TEXT_KEYS_BY_LABEL[locale][label] = key }
end
end
TEXT_QUERY_RESOURCE_FINDERS = {
:species => lambda { |name|
species = Species.find_by_name(name)
unless species
Item::Search.error 'not_found.species', :species_name => name
end
species.id
},
:zone => lambda { |name|
zone_set = Zone.find_set(name)
unless zone_set
Item::Search.error 'not_found.zone', :zone_name => name
end
zone_set.map(&:id)
}
}
TEXT_QUERY_RESOURCE_TYPES_BY_KEY = {
:species_support_id => :species,
:occupied_zone_id => :zone,
:restricted_zone_id => :zone
}
TEXT_FILTER_EXPR = /([+-]?)(?:([a-z]+):)?(?:"([^"]+)"|(\S+))/
def self.from_text(text, user=nil)
filters = []
text.scan(TEXT_FILTER_EXPR) do |sign, label, quoted_value, unquoted_value|
label ||= 'name'
raw_value = quoted_value || unquoted_value
is_positive = (sign != '-')
if label == 'is'
# is-filters are weird. "-is:nc" is transposed to something more
# like "-nc:<nil>", then it's translated into a negative "is_nc"
# flag.
label = raw_value
raw_value = nil
end
key = TEXT_KEYS_BY_LABEL[I18n.locale][label]
if key.nil?
message = I18n.translate('items.search.errors.not_found.label',
:label => label)
raise Item::Search::Error, message
end
if TEXT_QUERY_RESOURCE_TYPES_BY_KEY.has_key?(key)
resource_type = TEXT_QUERY_RESOURCE_TYPES_BY_KEY[key]
finder = TEXT_QUERY_RESOURCE_FINDERS[resource_type]
value = finder.call(raw_value)
else
value = raw_value
end
filters << Filter.new(key, value, is_positive)
end
self.new(filters, user)
end
private
# The fields start out empty, then have the filters inserted into 'em,
# so that the fields can validate and aggregate their requirements.
def initial_fields
{}.tap do |fields|
FIELD_CLASSES.map do |key, klass|
fields[key] = klass.new(key)
end
end
end
end
end
end