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