forked from OpenNeo/impress
Matchu
6e09b8bc10
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.
151 lines
4.9 KiB
Ruby
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
|