lib/thinking_sphinx/search.rb in dpickett-thinking-sphinx-1.1.4 vs lib/thinking_sphinx/search.rb in dpickett-thinking-sphinx-1.1.12
- old
+ new
@@ -1,14 +1,19 @@
-module ThinkingSphinx
+module ThinkingSphinx
# Once you've got those indexes in and built, this is the stuff that
# matters - how to search! This class provides a generic search
# interface - which you can use to search all your indexed models at once.
# Most times, you will just want a specific model's results - to search and
# search_for_ids methods will do the job in exactly the same manner when
# called from a model.
#
class Search
+ GlobalFacetOptions = {
+ :all_attributes => false,
+ :class_facet => true
+ }
+
class << self
# Searches for results that match the parameters provided. Will only
# return the ids for the matching objects. See #search for syntax
# examples.
#
@@ -92,20 +97,28 @@
# note that you don't need to put in a search string.
#
# == Searching by Attributes
#
# Also known as filters, you can limit your searches to documents that
- # have specific values for their attributes. There are two ways to do
- # this. The first is one that works in all scenarios - using the :with
- # option.
+ # have specific values for their attributes. There are three ways to do
+ # this. The first two techniques work in all scenarios - using the :with
+ # or :with_all options.
#
- # ThinkingSphinx::Search.search :with => {:parent_id => 10}
+ # ThinkingSphinx::Search.search :with => {:tag_ids => 10}
+ # ThinkingSphinx::Search.search :with => {:tag_ids => [10,12]}
+ # ThinkingSphinx::Search.search :with_all => {:tag_ids => [10,12]}
#
- # The second is only viable if you're searching with a specific model
- # (not multi-model searching). With a single model, Thinking Sphinx
- # can figure out what attributes and fields are available, so you can
- # put it all in the :conditions hash, and it will sort it out.
+ # The first :with search will match records with a tag_id attribute of 10.
+ # The second :with will match records with a tag_id attribute of 10 OR 12.
+ # If you need to find records that are tagged with ids 10 AND 12, you
+ # will need to use the :with_all search parameter. This is particuarly
+ # useful in conjunction with Multi Value Attributes (MVAs).
+ #
+ # The third filtering technique is only viable if you're searching with a
+ # specific model (not multi-model searching). With a single model,
+ # Thinking Sphinx can figure out what attributes and fields are available,
+ # so you can put it all in the :conditions hash, and it will sort it out.
#
# Node.search :conditions => {:parent_id => 10}
#
# Filters can be single values, arrays of values, or ranges.
#
@@ -184,20 +197,85 @@
#
# Of course, there are other sort modes - check out the Sphinx
# documentation[http://sphinxsearch.com/doc.html] for that level of
# detail though.
#
+ # If desired, you can sort by a column in your model instead of a sphinx
+ # field or attribute. This sort only applies to the current page, so is
+ # most useful when performing a search with a single page of results.
+ #
+ # User.search("pat", :sql_order => "name")
+ #
# == Grouping
#
# For this you can use the group_by, group_clause and group_function
# options - which are all directly linked to Sphinx's expectations. No
# magic from Thinking Sphinx. It can get a little tricky, so make sure
# you read all the relevant
# documentation[http://sphinxsearch.com/doc.html#clustering] first.
#
- # Yes this section will be expanded, but this is a start.
+ # Grouping is done via three parameters within the options hash
+ # * <tt>:group_function</tt> determines the way grouping is done
+ # * <tt>:group_by</tt> determines the field which is used for grouping
+ # * <tt>:group_clause</tt> determines the sorting order
#
+ # === group_function
+ #
+ # Valid values for :group_function are
+ # * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
+ # * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
+ #
+ # === group_by
+ #
+ # This parameter denotes the field by which grouping is done. Note that the
+ # specified field must be a sphinx attribute or index.
+ #
+ # === group_clause
+ #
+ # This determines the sorting order of the groups. In a grouping search,
+ # the matches within a group will sorted by the <tt>:sort_mode</tt> and <tt>:order</tt> parameters.
+ # The group matches themselves however, will be sorted by <tt>:group_clause</tt>.
+ #
+ # The syntax for this is the same as an order parameter in extended sort mode.
+ # Namely, you can specify an SQL-like sort expression with up to 5 attributes
+ # (including internal attributes), eg: "@relevance DESC, price ASC, @id DESC"
+ #
+ # === Grouping by timestamp
+ #
+ # Timestamp grouping groups off items by the day, week, month or year of the
+ # attribute given. In order to do this you need to define a timestamp attribute,
+ # which pretty much looks like the standard defintion for any attribute.
+ #
+ # define_index do
+ # #
+ # # All your other stuff
+ # #
+ # has :created_at
+ # end
+ #
+ # When you need to fire off your search, it'll go something to the tune of
+ #
+ # Fruit.search "apricot", :group_function => :day, :group_by => 'created_at'
+ #
+ # The <tt>@groupby</tt> special attribute will contain the date for that group.
+ # Depending on the <tt>:group_function</tt> parameter, the date format will be
+ #
+ # * <tt>:day</tt> - YYYYMMDD
+ # * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
+ # counting from the start of the year )
+ # * <tt>:month</tt> - YYYYMM
+ # * <tt>:year</tt> - YYYY
+ #
+ #
+ # === Grouping by attribute
+ #
+ # The syntax is the same as grouping by timestamp, except for the fact that the
+ # <tt>:group_function</tt> parameter is changed
+ #
+ # Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
+ #
+ #
# == Geo/Location Searching
#
# Sphinx - and therefore Thinking Sphinx - has the facility to search
# around a geographical point, using a given latitude and longitude. To
# take advantage of this, you will need to have both of those values in
@@ -289,12 +367,14 @@
end
def retry_search_on_stale_index(query, options, &block)
stale_ids = []
stale_retries_left = case options[:retry_stale]
- when true: 3 # default to three retries
- when nil, false: 0 # no retries
+ when true
+ 3 # default to three retries
+ when nil, false
+ 0 # no retries
else options[:retry_stale].to_i
end
begin
# Passing this in an option so Collection.create_from_results can see it.
# It should only raise on stale records if there are any retries left.
@@ -350,47 +430,27 @@
rescue Errno::ECONNREFUSED => err
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
end
end
+ # Model.facets *args
+ # ThinkingSphinx::Search.facets *args
+ # ThinkingSphinx::Search.facets *args, :all_attributes => true
+ # ThinkingSphinx::Search.facets *args, :class_facet => false
+ #
def facets(*args)
- hash = ThinkingSphinx::FacetCollection.new args
- options = args.extract_options!.clone.merge! :group_function => :attr
+ options = args.extract_options!
- klasses = options[:classes] || [options[:class]]
- klasses = [] if options[:class].nil?
-
- #no classes specified so get classes from resultset
- if klasses.empty?
- options[:group_by] = "class_crc"
- results = search(*(args + [options]))
-
- hash[:class] = {}
- results.each_with_groupby_and_count do |result, group, count|
- hash[:class][result.class.name] = count
- klasses << result.class
- end
+ if options[:class]
+ facets_for_model options[:class], args, options
+ else
+ facets_for_all_models args, options
end
-
- klasses.each do |klass|
- klass.sphinx_facets.inject(hash) do |hash, facet|
- if facet.name != :class || options[:include_class_facets]
- hash.add_from_results facet,
- search(*(args +
- [options.merge(:group_by => facet.attribute_name)]))
- end
-
- hash
- end
- end
-
- hash
end
private
-
# This method handles the common search functionality, and returns both
# the result hash and the client. Not super elegant, but it'll do for
# the moment.
#
def search_results(*args)
@@ -410,10 +470,11 @@
set_sort_options! client, options
client.limit = options[:per_page].to_i if options[:per_page]
page = options[:page] ? options[:page].to_i : 1
+ page = 1 if page <= 0
client.offset = (page - 1) * client.limit
begin
::ActiveRecord::Base.logger.debug "Sphinx: #{query}"
results = client.query query
@@ -490,10 +551,17 @@
# exclusive attribute filters
client.filters += options[:without].collect { |attr,val|
Riddle::Client::Filter.new attr.to_s, filter_value(val), true
} if options[:without]
+ # every-match attribute filters
+ client.filters += options[:with_all].collect { |attr,vals|
+ Array(vals).collect { |val|
+ Riddle::Client::Filter.new attr.to_s, filter_value(val)
+ }
+ }.flatten if options[:with_all]
+
# exclusive attribute filter on primary key
client.filters += Array(options[:without_ids]).collect { |id|
Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
} if options[:without_ids]
@@ -646,9 +714,78 @@
match.gsub field.to_s, field.to_s.concat("_sort")
}
}
string
+ end
+
+ def facets_for_model(klass, args, options)
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
+ options = options.clone.merge! facet_query_options
+
+ klass.sphinx_facets.inject(hash) do |hash, facet|
+ unless facet.name == :class && !options[:class_facet]
+ options[:group_by] = facet.attribute_name
+ hash.add_from_results facet, search(*(args + [options]))
+ end
+
+ hash
+ end
+ end
+
+ def facets_for_all_models(args, options)
+ options = GlobalFacetOptions.merge(options)
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
+ options = options.merge! facet_query_options
+
+ facet_names(options).inject(hash) do |hash, name|
+ options[:group_by] = name
+ hash.add_from_results name, search(*(args + [options]))
+ hash
+ end
+ end
+
+ def facet_query_options
+ config = ThinkingSphinx::Configuration.instance
+ max = config.configuration.searchd.max_matches || 1000
+
+ {
+ :group_function => :attr,
+ :limit => max,
+ :max_matches => max
+ }
+ end
+
+ def facet_classes(options)
+ options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
+ model.constantize
+ }
+ end
+
+ def facet_names(options)
+ classes = facet_classes(options)
+ names = options[:all_attributes] ?
+ facet_names_for_all_classes(classes) :
+ facet_names_common_to_all_classes(classes)
+
+ names.delete "class_crc" unless options[:class_facet]
+ names
+ end
+
+ def facet_names_for_all_classes(classes)
+ classes.collect { |klass|
+ klass.sphinx_facets.collect { |facet| facet.attribute_name }
+ }.flatten.uniq
+ end
+
+ def facet_names_common_to_all_classes(classes)
+ facet_names_for_all_classes(classes).select { |name|
+ classes.all? { |klass|
+ klass.sphinx_facets.detect { |facet|
+ facet.attribute_name == name
+ }
+ }
+ }
end
end
end
end