require 'ultrasphinx' module ActiveRecord class Base =begin rdoc The is_indexed method configures a model for indexing. Its parameters help generate SQL queries for Sphinx. = Options == Including regular fields Use the <tt>:fields</tt> key. Accepts an array of field names or field hashes. :fields => [ 'created_at', 'title', {:field => 'body', :as => 'description'}, {:field => 'user_category', :facet => true, :as => 'category' } ] To alias a field, pass a hash instead of a string and set the <tt>:as</tt> key. To allow faceting support on a text field, also pass a hash and set the <tt>:facet</tt> key to <tt>true</tt>. Faceting is off by default for text fields because there is some indexing overhead associated with it. Faceting is always on for numeric or date fields. To allow sorting by a text field, also pass a hash and set the <tt>:sortable</tt> key to true. This is turned off by default for the same reason as above. Sorting is always on for numeric or date fields. To apply an SQL function to a field before it is indexed, use the key <tt>:function_sql</tt>. Pass a string such as <tt>"REPLACE(?, '_', ' ')"</tt>. The table and column name for your field will be interpolated into the first <tt>?</tt> in the string. Note that <tt>float</tt> fields are supported, but require Sphinx 0.98. == Requiring conditions Use the <tt>:conditions</tt> key. SQL conditions, to scope which records are selected for indexing. Accepts a string. :conditions => "created_at < NOW() AND deleted IS NOT NULL" The <tt>:conditions</tt> key is especially useful if you delete records by marking them deleted rather than removing them from the database. == Including a field from an association Use the <tt>:include</tt> key. Accepts an array of hashes. :include => [{:association_name => 'category', :field => 'name', :as => 'category_name'}] Each should contain an <tt>:association_name</tt> key (the association name for the included model), a <tt>:field</tt> key (the name of the field to include), and an optional <tt>:as</tt> key (what to name the field in the parent). <tt>:include</tt> hashes also accept their own <tt>:conditions</tt> key. You can use this if you need custom WHERE conditions for this particular association (e.g, this JOIN). The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:class_name</tt>, <tt>:association_sql</tt>, and <tt>:function_sql</tt> are also recognized. == Concatenating several fields within one record Use the <tt>:concatenate</tt> key (MySQL only). Accepts an array of option hashes. To concatenate several fields within one record as a combined field, use a regular (or lateral) concatenation. Regular concatenations contain a <tt>:fields</tt> key (again, an array of field names), and a mandatory <tt>:as</tt> key (the name of the result of the concatenation). For example, to concatenate the <tt>title</tt> and <tt>body</tt> into one field called <tt>text</tt>: :concatenate => [{:fields => ['title', 'body'], :as => 'text'}] The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:conditions</tt>, <tt>:function_sql</tt>, <tt>:class_name</tt>, and <tt>:association_sql</tt>, are also recognized. Lateral concatenations are implemented with CONCAT_WS on MySQL and with a stored procedure on PostgreSQL. == Concatenating the same field from a set of associated records Also use the <tt>:concatenate</tt> key. To concatenate one field from a set of associated records as a combined field in the parent record, use a group (or vertical) concatenation. A group concatenation should contain an <tt>:association_name</tt> key (the association name for the included model), a <tt>:field</tt> key (the field on the included model to concatenate), and an optional <tt>:as</tt> key (also the name of the result of the concatenation). For example, to concatenate all <tt>Post#body</tt> contents into the parent's <tt>responses</tt> field: :concatenate => [{:association_name => 'posts', :field => 'body', :as => 'responses'}] The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:conditions</tt>, <tt>:function_sql</tt>, <tt>:class_name</tt>, and <tt>:association_sql</tt>, are also recognized. Vertical concatenations are implemented with GROUP_CONCAT on MySQL and with an aggregate and a stored procedure on PostgreSQL. == Custom joins <tt>:include</tt> and <tt>:concatenate</tt> accept an <tt>:association_sql</tt> key. You can use this if you need to pass a custom JOIN string, for example, a double JOIN for a <tt>has_many :through</tt>). If <tt>:association_sql</tt> is present, the default JOIN for <tt>belongs_to</tt> will not be generated. Also, If you want to include a model that you don't have an actual ActiveRecord association for, you can use <tt>:association_sql</tt> combined with <tt>:class_name</tt> instead of <tt>:association_name</tt>. <tt>:class_name</tt> should be camelcase. Ultrasphinx is not an object-relational mapper, and the association generation is intended to stay minimal--don't be afraid of <tt>:association_sql</tt>. = Examples == Complex configuration Here's an example configuration using most of the options, taken from production code: class Story < ActiveRecord::Base is_indexed :fields => [ 'title', 'published_at', {:field => 'author', :facet => true} ], :include => [ {:association_name => 'category', :field => 'name', :as => 'category_name'} ], :concatenate => [ {:fields => ['title', 'long_description', 'short_description'], :as => 'editorial'}, {:association_name => 'pages', :field => 'body', :as => 'body'}, {:association_name => 'comments', :field => 'body', :as => 'comments', :conditions => "comments.item_type = '#{base_class}'"} ], :conditions => self.live_condition_string end Note how setting the <tt>:conditions</tt> on Comment is enough to configure a polymorphic <tt>has_many</tt>. == Association scoping A common use case is to only search records that belong to a particular parent model. Ultrasphinx configures Sphinx to support a <tt>:filters</tt> element on any date or numeric field, so any <tt>*_id</tt> fields you have will be filterable. For example, say a Company <tt>has_many :users</tt> and each User <tt>has_many :articles</tt>. If you want to to filter Articles by Company, add <tt>company_id</tt> to the Article's <tt>is_indexed</tt> method. The best way is to grab it from the User association: class Article < ActiveRecord::Base is_indexed :include => [{:association_name => 'users', :field => 'company_id'}] end Now you can run: @search = Ultrasphinx::Search.new('something', :filters => {'company_id' => 493}) If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you would need to use the <tt>:association_sql</tt> key to set up a custom JOIN. =end def self.is_indexed opts = {} opts = HashWithIndifferentAccess.new(opts) opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include'] Array(opts['fields']).each do |entry| if entry.is_a? Hash entry.stringify_keys! entry.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable'] end end Array(opts['concatenate']).each do |entry| entry.stringify_keys! entry.assert_valid_keys ['class_name', 'association_name', 'conditions', 'field', 'as', 'fields', 'association_sql', 'facet', 'function_sql', 'sortable'] raise Ultrasphinx::ConfigurationError, "You can't mix regular concat and group concats" if entry['fields'] and (entry['field'] or entry['class_name'] or entry['association_name']) raise Ultrasphinx::ConfigurationError, "Concatenations must specify an :as key" unless entry['as'] raise Ultrasphinx::ConfigurationError, "Group concatenations must not have multiple fields" if entry['field'].is_a? Array raise Ultrasphinx::ConfigurationError, "Regular concatenations should have multiple fields" if entry['fields'] and !entry['fields'].is_a?(Array) end Array(opts['include']).each do |entry| entry.stringify_keys! entry.assert_valid_keys ['class_name', 'association_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable'] end Ultrasphinx::MODEL_CONFIGURATION[self.name] = opts end end end