From 4bd16f053847f2efe347ebda9136ef2233ee0d2c Mon Sep 17 00:00:00 2001 From: hukl Date: Tue, 28 Apr 2009 00:15:53 +0200 Subject: added thinking_sphinx plugin for fulltext search on nodes and heads --- .../lib/thinking_sphinx/active_record.rb | 260 +++++++ .../lib/thinking_sphinx/active_record/delta.rb | 78 +++ .../active_record/has_many_association.rb | 29 + .../lib/thinking_sphinx/active_record/search.rb | 57 ++ .../thinking_sphinx/adapters/abstract_adapter.rb | 42 ++ .../lib/thinking_sphinx/adapters/mysql_adapter.rb | 54 ++ .../thinking_sphinx/adapters/postgresql_adapter.rb | 130 ++++ .../lib/thinking_sphinx/association.rb | 161 +++++ .../lib/thinking_sphinx/attribute.rb | 358 ++++++++++ .../lib/thinking_sphinx/class_facet.rb | 15 + .../lib/thinking_sphinx/collection.rb | 147 ++++ .../lib/thinking_sphinx/configuration.rb | 237 +++++++ .../lib/thinking_sphinx/core/string.rb | 15 + .../thinking-sphinx/lib/thinking_sphinx/deltas.rb | 27 + .../lib/thinking_sphinx/deltas/datetime_delta.rb | 50 ++ .../lib/thinking_sphinx/deltas/default_delta.rb | 67 ++ .../lib/thinking_sphinx/deltas/delayed_delta.rb | 25 + .../deltas/delayed_delta/delta_job.rb | 24 + .../deltas/delayed_delta/flag_as_deleted_job.rb | 27 + .../thinking_sphinx/deltas/delayed_delta/job.rb | 26 + .../thinking-sphinx/lib/thinking_sphinx/facet.rb | 58 ++ .../lib/thinking_sphinx/facet_collection.rb | 60 ++ .../thinking-sphinx/lib/thinking_sphinx/field.rb | 172 +++++ .../thinking-sphinx/lib/thinking_sphinx/index.rb | 423 +++++++++++ .../lib/thinking_sphinx/index/builder.rb | 264 +++++++ .../lib/thinking_sphinx/index/faux_column.rb | 110 +++ .../lib/thinking_sphinx/rails_additions.rb | 136 ++++ .../thinking-sphinx/lib/thinking_sphinx/search.rb | 780 +++++++++++++++++++++ .../thinking-sphinx/lib/thinking_sphinx/tasks.rb | 128 ++++ 29 files changed, 3960 insertions(+) create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb create mode 100644 vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb (limited to 'vendor/plugins/thinking-sphinx/lib/thinking_sphinx') diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb new file mode 100644 index 0000000..3e85b50 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb @@ -0,0 +1,260 @@ +require 'thinking_sphinx/active_record/delta' +require 'thinking_sphinx/active_record/search' +require 'thinking_sphinx/active_record/has_many_association' + +module ThinkingSphinx + # Core additions to ActiveRecord models - define_index for creating indexes + # for models. If you want to interrogate the index objects created for the + # model, you can use the class-level accessor :sphinx_indexes. + # + module ActiveRecord + def self.included(base) + base.class_eval do + class_inheritable_array :sphinx_indexes, :sphinx_facets + class << self + # Allows creation of indexes for Sphinx. If you don't do this, there + # isn't much point trying to search (or using this plugin at all, + # really). + # + # An example or two: + # + # define_index + # indexes :id, :as => :model_id + # indexes name + # end + # + # You can also grab fields from associations - multiple levels deep + # if necessary. + # + # define_index do + # indexes tags.name, :as => :tag + # indexes articles.content + # indexes orders.line_items.product.name, :as => :product + # end + # + # And it will automatically concatenate multiple fields: + # + # define_index do + # indexes [author.first_name, author.last_name], :as => :author + # end + # + # The #indexes method is for fields - if you want attributes, use + # #has instead. All the same rules apply - but keep in mind that + # attributes are for sorting, grouping and filtering, not searching. + # + # define_index do + # # fields ... + # + # has created_at, updated_at + # end + # + # One last feature is the delta index. This requires the model to + # have a boolean field named 'delta', and is enabled as follows: + # + # define_index do + # # fields ... + # # attributes ... + # + # set_property :delta => true + # end + # + # Check out the more detailed documentation for each of these methods + # at ThinkingSphinx::Index::Builder. + # + def define_index(&block) + return unless ThinkingSphinx.define_indexes? + + self.sphinx_indexes ||= [] + index = Index.new(self, &block) + + self.sphinx_indexes << index + unless ThinkingSphinx.indexed_models.include?(self.name) + ThinkingSphinx.indexed_models << self.name + end + + if index.delta? + before_save :toggle_delta + after_commit :index_delta + end + + after_destroy :toggle_deleted + + index + end + alias_method :sphinx_index, :define_index + + def sphinx_index_options + sphinx_indexes.last.options + end + + # Generate a unique CRC value for the model's name, to use to + # determine which Sphinx documents belong to which AR records. + # + # Really only written for internal use - but hey, if it's useful to + # you in some other way, awesome. + # + def to_crc32 + self.name.to_crc32 + end + + def to_crc32s + (subclasses << self).collect { |klass| klass.to_crc32 } + end + + def source_of_sphinx_index + possible_models = self.sphinx_indexes.collect { |index| index.model } + return self if possible_models.include?(self) + + parent = self.superclass + while !possible_models.include?(parent) && parent != ::ActiveRecord::Base + parent = parent.superclass + end + + return parent + end + + def to_riddle(offset) + sphinx_database_adapter.setup + + indexes = [to_riddle_for_core(offset)] + indexes << to_riddle_for_delta(offset) if sphinx_delta? + indexes << to_riddle_for_distributed + end + + def sphinx_database_adapter + @sphinx_database_adapter ||= + ThinkingSphinx::AbstractAdapter.detect(self) + end + + private + + def sphinx_name + self.name.underscore.tr(':/\\', '_') + end + + def sphinx_delta? + self.sphinx_indexes.any? { |index| index.delta? } + end + + def to_riddle_for_core(offset) + index = Riddle::Configuration::Index.new("#{sphinx_name}_core") + index.path = File.join( + ThinkingSphinx::Configuration.instance.searchd_file_path, index.name + ) + + set_configuration_options_for_indexes index + set_field_settings_for_indexes index + + self.sphinx_indexes.select { |ts_index| + ts_index.model == self + }.each_with_index do |ts_index, i| + index.sources << ts_index.to_riddle_for_core(offset, i) + end + + index + end + + def to_riddle_for_delta(offset) + index = Riddle::Configuration::Index.new("#{sphinx_name}_delta") + index.parent = "#{sphinx_name}_core" + index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name) + + self.sphinx_indexes.each_with_index do |ts_index, i| + index.sources << ts_index.to_riddle_for_delta(offset, i) if ts_index.delta? + end + + index + end + + def to_riddle_for_distributed + index = Riddle::Configuration::DistributedIndex.new(sphinx_name) + index.local_indexes << "#{sphinx_name}_core" + index.local_indexes.unshift "#{sphinx_name}_delta" if sphinx_delta? + index + end + + def set_configuration_options_for_indexes(index) + ThinkingSphinx::Configuration.instance.index_options.each do |key, value| + index.send("#{key}=".to_sym, value) + end + + self.sphinx_indexes.each do |ts_index| + ts_index.options.each do |key, value| + index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil? + end + end + end + + def set_field_settings_for_indexes(index) + field_names = lambda { |field| field.unique_name.to_s } + + self.sphinx_indexes.each do |ts_index| + index.prefix_field_names += ts_index.prefix_fields.collect(&field_names) + index.infix_field_names += ts_index.infix_fields.collect(&field_names) + end + end + end + end + + base.send(:include, ThinkingSphinx::ActiveRecord::Delta) + base.send(:include, ThinkingSphinx::ActiveRecord::Search) + + ::ActiveRecord::Associations::HasManyAssociation.send( + :include, ThinkingSphinx::ActiveRecord::HasManyAssociation + ) + ::ActiveRecord::Associations::HasManyThroughAssociation.send( + :include, ThinkingSphinx::ActiveRecord::HasManyAssociation + ) + end + + def in_index?(suffix) + self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix) + end + + def in_core_index? + in_index? "core" + end + + def in_delta_index? + in_index? "delta" + end + + def in_both_indexes? + in_core_index? && in_delta_index? + end + + def toggle_deleted + return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running? + + config = ThinkingSphinx::Configuration.instance + client = Riddle::Client.new config.address, config.port + + client.update( + "#{self.class.sphinx_indexes.first.name}_core", + ['sphinx_deleted'], + {self.sphinx_document_id => 1} + ) if self.in_core_index? + + client.update( + "#{self.class.sphinx_indexes.first.name}_delta", + ['sphinx_deleted'], + {self.sphinx_document_id => 1} + ) if ThinkingSphinx.deltas_enabled? && + self.class.sphinx_indexes.any? { |index| index.delta? } && + self.toggled_delta? + rescue ::ThinkingSphinx::ConnectionError + # nothing + end + + def sphinx_document_id + (self.id * ThinkingSphinx.indexed_models.size) + + ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name) + end + + private + + def sphinx_index_name(suffix) + "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}" + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb new file mode 100644 index 0000000..f21aba2 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb @@ -0,0 +1,78 @@ +module ThinkingSphinx + module ActiveRecord + # This module contains all the delta-related code for models. There isn't + # really anything you need to call manually in here - except perhaps + # index_delta, but not sure what reason why. + # + module Delta + # Code for after_commit callback is written by Eli Miller: + # http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html + # with slight modification from Joost Hietbrink. + # + def self.included(base) + base.class_eval do + class << self + # Temporarily disable delta indexing inside a block, then perform a single + # rebuild of index at the end. + # + # Useful when performing updates to batches of models to prevent + # the delta index being rebuilt after each individual update. + # + # In the following example, the delta index will only be rebuilt once, + # not 10 times. + # + # SomeModel.suspended_delta do + # 10.times do + # SomeModel.create( ... ) + # end + # end + # + def suspended_delta(reindex_after = true, &block) + original_setting = ThinkingSphinx.deltas_enabled? + ThinkingSphinx.deltas_enabled = false + begin + yield + ensure + ThinkingSphinx.deltas_enabled = original_setting + self.index_delta if reindex_after + end + end + + # Build the delta index for the related model. This won't be called + # if running in the test environment. + # + def index_delta(instance = nil) + delta_object.index(self, instance) + end + + def delta_object + self.sphinx_indexes.first.delta_object + end + end + + def toggled_delta? + self.class.delta_object.toggled(self) + end + + private + + # Set the delta value for the model to be true. + def toggle_delta + self.class.delta_object.toggle(self) if should_toggle_delta? + end + + # Build the delta index for the related model. This won't be called + # if running in the test environment. + # + def index_delta + self.class.index_delta(self) if self.class.delta_object.toggled(self) + end + + def should_toggle_delta? + !self.respond_to?(:changed?) || self.changed? || self.new_record? + end + end + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb new file mode 100644 index 0000000..44b25c0 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb @@ -0,0 +1,29 @@ +module ThinkingSphinx + module ActiveRecord + module HasManyAssociation + def search(*args) + foreign_key = @reflection.primary_key_name + stack = [@reflection.options[:through]].compact + + attribute = nil + (@reflection.klass.sphinx_indexes || []).each do |index| + attribute = index.attributes.detect { |attrib| + attrib.columns.length == 1 && + attrib.columns.first.__name == foreign_key.to_sym && + attrib.columns.first.__stack == stack + } + break if attribute + end + + raise "Missing Attribute for Foreign Key #{foreign_key}" unless attribute + + options = args.extract_options! + options[:with] ||= {} + options[:with][attribute.unique_name] = @owner.id + + args << options + @reflection.klass.search(*args) + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb new file mode 100644 index 0000000..fc3f2b4 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb @@ -0,0 +1,57 @@ +module ThinkingSphinx + module ActiveRecord + # This module covers the specific model searches - but the syntax is + # exactly the same as the core Search class - so use that as your refence + # point. + # + module Search + def self.included(base) + base.class_eval do + class << self + # Searches for results that match the parameters provided. Will only + # return the ids for the matching objects. See + # ThinkingSphinx::Search#search for syntax examples. + # + def search_for_ids(*args) + options = args.extract_options! + options[:class] = self + args << options + ThinkingSphinx::Search.search_for_ids(*args) + end + + # Searches for results limited to a single model. See + # ThinkingSphinx::Search#search for syntax examples. + # + def search(*args) + options = args.extract_options! + options[:class] = self + args << options + ThinkingSphinx::Search.search(*args) + end + + def search_count(*args) + options = args.extract_options! + options[:class] = self + args << options + ThinkingSphinx::Search.count(*args) + end + + def search_for_id(*args) + options = args.extract_options! + options[:class] = self + args << options + ThinkingSphinx::Search.search_for_id(*args) + end + + def facets(*args) + options = args.extract_options! + options[:class] = self + args << options + ThinkingSphinx::Search.facets(*args) + end + end + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb new file mode 100644 index 0000000..b68b75e --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb @@ -0,0 +1,42 @@ +module ThinkingSphinx + class AbstractAdapter + def initialize(model) + @model = model + end + + def setup + # Deliberately blank - subclasses should do something though. Well, if + # they need to. + end + + def self.detect(model) + case model.connection.class.name + when "ActiveRecord::ConnectionAdapters::MysqlAdapter", + "ActiveRecord::ConnectionAdapters::MysqlplusAdapter" + ThinkingSphinx::MysqlAdapter.new model + when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter" + ThinkingSphinx::PostgreSQLAdapter.new model + when "ActiveRecord::ConnectionAdapters::JdbcAdapter" + if model.connection.config[:adapter] == "jdbcmysql" + ThinkingSphinx::MysqlAdapter.new model + elsif model.connection.config[:adapter] == "jdbcpostgresql" + ThinkingSphinx::PostgreSQLAdapter.new model + else + raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL" + end + else + raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}" + end + end + + def quote_with_table(column) + "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}" + end + + protected + + def connection + @connection ||= @model.connection + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb new file mode 100644 index 0000000..597d4b6 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb @@ -0,0 +1,54 @@ +module ThinkingSphinx + class MysqlAdapter < AbstractAdapter + def setup + # Does MySQL actually need to do anything? + end + + def sphinx_identifier + "mysql" + end + + def concatenate(clause, separator = ' ') + "CONCAT_WS('#{separator}', #{clause})" + end + + def group_concatenate(clause, separator = ' ') + "GROUP_CONCAT(DISTINCT #{clause} SEPARATOR '#{separator}')" + end + + def cast_to_string(clause) + "CAST(#{clause} AS CHAR)" + end + + def cast_to_datetime(clause) + "UNIX_TIMESTAMP(#{clause})" + end + + def cast_to_unsigned(clause) + "CAST(#{clause} AS UNSIGNED)" + end + + def convert_nulls(clause, default = '') + default = "'#{default}'" if default.is_a?(String) + + "IFNULL(#{clause}, #{default})" + end + + def boolean(value) + value ? 1 : 0 + end + + def crc(clause, blank_to_null = false) + clause = "NULLIF(#{clause},'')" if blank_to_null + "CRC32(#{clause})" + end + + def utf8_query_pre + "SET NAMES utf8" + end + + def time_difference(diff) + "DATE_SUB(NOW(), INTERVAL #{diff} SECOND)" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb new file mode 100644 index 0000000..625971d --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb @@ -0,0 +1,130 @@ +module ThinkingSphinx + class PostgreSQLAdapter < AbstractAdapter + def setup + create_array_accum_function + create_crc32_function + end + + def sphinx_identifier + "pgsql" + end + + def concatenate(clause, separator = ' ') + clause.split(', ').collect { |field| + "COALESCE(CAST(#{field} as varchar), '')" + }.join(" || '#{separator}' || ") + end + + def group_concatenate(clause, separator = ' ') + "array_to_string(array_accum(#{clause}), '#{separator}')" + end + + def cast_to_string(clause) + clause + end + + def cast_to_datetime(clause) + "cast(extract(epoch from #{clause}) as int)" + end + + def cast_to_unsigned(clause) + clause + end + + def convert_nulls(clause, default = '') + default = "'#{default}'" if default.is_a?(String) + + "COALESCE(#{clause}, #{default})" + end + + def boolean(value) + value ? 'TRUE' : 'FALSE' + end + + def crc(clause, blank_to_null = false) + clause = "NULLIF(#{clause},'')" if blank_to_null + "crc32(#{clause})" + end + + def utf8_query_pre + nil + end + + def time_difference(diff) + "current_timestamp - interval '#{diff} seconds'" + end + + private + + def execute(command, output_error = false) + connection.execute "begin" + connection.execute "savepoint ts" + begin + connection.execute command + rescue StandardError => err + puts err if output_error + connection.execute "rollback to savepoint ts" + end + connection.execute "release savepoint ts" + connection.execute "commit" + end + + def create_array_accum_function + if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200 + execute <<-SQL + CREATE AGGREGATE array_accum (anyelement) + ( + sfunc = array_append, + stype = anyarray, + initcond = '{}' + ); + SQL + else + execute <<-SQL + CREATE AGGREGATE array_accum + ( + basetype = anyelement, + sfunc = array_append, + stype = anyarray, + initcond = '{}' + ); + SQL + end + end + + def create_crc32_function + execute "CREATE LANGUAGE 'plpgsql';" + function = <<-SQL + CREATE OR REPLACE FUNCTION crc32(word text) + RETURNS bigint AS $$ + DECLARE tmp bigint; + DECLARE i int; + DECLARE j int; + DECLARE word_array bytea; + BEGIN + i = 0; + tmp = 4294967295; + word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape'); + LOOP + tmp = (tmp # get_byte(word_array, i))::bigint; + i = i + 1; + j = 0; + LOOP + tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint; + j = j + 1; + IF j >= 8 THEN + EXIT; + END IF; + END LOOP; + IF i >= char_length(word) THEN + EXIT; + END IF; + END LOOP; + return (tmp # 4294967295); + END + $$ IMMUTABLE STRICT LANGUAGE plpgsql; + SQL + execute function, true + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb new file mode 100644 index 0000000..b057035 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb @@ -0,0 +1,161 @@ +module ThinkingSphinx + # Association tracks a specific reflection and join to reference data that + # isn't in the base model. Very much an internal class for Thinking Sphinx - + # perhaps because I feel it's not as strong (or simple) as most of the rest. + # + class Association + attr_accessor :parent, :reflection, :join + + # Create a new association by passing in the parent association, and the + # corresponding reflection instance. If there is no parent, pass in nil. + # + # top = Association.new nil, top_reflection + # child = Association.new top, child_reflection + # + def initialize(parent, reflection) + @parent, @reflection = parent, reflection + @children = {} + end + + # Get the children associations for a given association name. The only time + # that there'll actually be more than one association is when the + # relationship is polymorphic. To keep things simple though, it will always + # be an Array that gets returned (an empty one if no matches). + # + # # where pages is an association on the class tied to the reflection. + # association.children(:pages) + # + def children(assoc) + @children[assoc] ||= Association.children(@reflection.klass, assoc, self) + end + + # Get the children associations for a given class, association name and + # parent association. Much like the instance method of the same name, it + # will return an empty array if no associations have the name, and only + # have multiple association instances if the underlying relationship is + # polymorphic. + # + # Association.children(User, :pages, user_association) + # + def self.children(klass, assoc, parent=nil) + ref = klass.reflect_on_association(assoc) + + return [] if ref.nil? + return [Association.new(parent, ref)] unless ref.options[:polymorphic] + + # association is polymorphic - create associations for each + # non-polymorphic reflection. + polymorphic_classes(ref).collect { |klass| + Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new( + ref.macro, + "#{ref.name}_#{klass.name}".to_sym, + casted_options(klass, ref), + ref.active_record + ) + } + end + + # Link up the join for this model from a base join - and set parent + # associations' joins recursively. + # + def join_to(base_join) + parent.join_to(base_join) if parent && parent.join.nil? + + @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new( + @reflection, base_join, parent ? parent.join : base_join.joins.first + ) + end + + # Returns the association's join SQL statements - and it replaces + # ::ts_join_alias:: with the aliased table name so the generated reflection + # join conditions avoid column name collisions. + # + def to_sql + @join.association_join.gsub(/::ts_join_alias::/, + "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}" + ) + end + + # Returns true if the association - or a parent - is a has_many or + # has_and_belongs_to_many. + # + def is_many? + case @reflection.macro + when :has_many, :has_and_belongs_to_many + true + else + @parent ? @parent.is_many? : false + end + end + + # Returns an array of all the associations that lead to this one - starting + # with the top level all the way to the current association object. + # + def ancestors + (parent ? parent.ancestors : []) << self + end + + def has_column?(column) + @reflection.klass.column_names.include?(column.to_s) + end + + def primary_key_from_reflection + if @reflection.options[:through] + @reflection.source_reflection.options[:foreign_key] || + @reflection.source_reflection.primary_key_name + else + nil + end + end + + def table + if @reflection.options[:through] + @join.aliased_join_table_name + else + @join.aliased_table_name + end + end + + private + + # Returns all the objects that could be currently instantiated from a + # polymorphic association. This is pretty damn fast if there's an index on + # the foreign type column - but if there isn't, it can take a while if you + # have a lot of data. + # + def self.polymorphic_classes(ref) + ref.active_record.connection.select_all( + "SELECT DISTINCT #{ref.options[:foreign_type]} " + + "FROM #{ref.active_record.table_name} " + + "WHERE #{ref.options[:foreign_type]} IS NOT NULL" + ).collect { |row| + row[ref.options[:foreign_type]].constantize + } + end + + # Returns a new set of options for an association that mimics an existing + # polymorphic relationship for a specific class. It adds a condition to + # filter by the appropriate object. + # + def self.casted_options(klass, ref) + options = ref.options.clone + options[:polymorphic] = nil + options[:class_name] = klass.name + options[:foreign_key] ||= "#{ref.name}_id" + + quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type] + case options[:conditions] + when nil + options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'" + when Array + options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'" + when Hash + options[:conditions].merge!(ref.options[:foreign_type] => klass.name) + else + options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'" + end + + options + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb new file mode 100644 index 0000000..1d45b2e --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb @@ -0,0 +1,358 @@ +module ThinkingSphinx + # Attributes - eternally useful when it comes to filtering, sorting or + # grouping. This class isn't really useful to you unless you're hacking + # around with the internals of Thinking Sphinx - but hey, don't let that + # stop you. + # + # One key thing to remember - if you're using the attribute manually to + # generate SQL statements, you'll need to set the base model, and all the + # associations. Which can get messy. Use Index.link!, it really helps. + # + class Attribute + attr_accessor :alias, :columns, :associations, :model, :faceted, :source + + # To create a new attribute, you'll need to pass in either a single Column + # or an array of them, and some (optional) options. + # + # Valid options are: + # - :as => :alias_name + # - :type => :attribute_type + # - :source => :field, :query, :ranged_query + # + # Alias is only required in three circumstances: when there's + # another attribute or field with the same name, when the column name is + # 'id', or when there's more than one column. + # + # Type is not required, unless you want to force a column to be a certain + # type (but keep in mind the value will not be CASTed in the SQL + # statements). The only time you really need to use this is when the type + # can't be figured out by the column - ie: when not actually using a + # database column as your source. + # + # Source is only used for multi-value attributes (MVA). By default this will + # use a left-join and a group_concat to obtain the values. For better performance + # during indexing it can be beneficial to let Sphinx use a separate query to retrieve + # all document,value-pairs. + # Either :query or :ranged_query will enable this feature, where :ranged_query will cause + # the query to be executed incremental. + # + # Example usage: + # + # Attribute.new( + # Column.new(:created_at) + # ) + # + # Attribute.new( + # Column.new(:posts, :id), + # :as => :post_ids + # ) + # + # Attribute.new( + # Column.new(:posts, :id), + # :as => :post_ids, + # :source => :ranged_query + # ) + # + # Attribute.new( + # [Column.new(:pages, :id), Column.new(:articles, :id)], + # :as => :content_ids + # ) + # + # Attribute.new( + # Column.new("NOW()"), + # :as => :indexed_at, + # :type => :datetime + # ) + # + # If you're creating attributes for latitude and longitude, don't forget + # that Sphinx expects these values to be in radians. + # + def initialize(columns, options = {}) + @columns = Array(columns) + @associations = {} + + raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) } + + @alias = options[:as] + @type = options[:type] + @faceted = options[:facet] + @source = options[:source] + @crc = options[:crc] + + @type ||= :multi unless @source.nil? + @type = :integer if @type == :string && @crc + end + + # Get the part of the SELECT clause related to this attribute. Don't forget + # to set your model and associations first though. + # + # This will concatenate strings and arrays of integers, and convert + # datetimes to timestamps, as needed. + # + def to_select_sql + return nil unless include_as_association? + + clause = @columns.collect { |column| + column_with_prefix(column) + }.join(', ') + + separator = all_ints? ? ',' : ' ' + + clause = adapter.concatenate(clause, separator) if concat_ws? + clause = adapter.group_concatenate(clause, separator) if is_many? + clause = adapter.cast_to_datetime(clause) if type == :datetime + clause = adapter.convert_nulls(clause) if type == :string + clause = adapter.crc(clause) if @crc + + "#{clause} AS #{quote_column(unique_name)}" + end + + # Get the part of the GROUP BY clause related to this attribute - if one is + # needed. If not, all you'll get back is nil. The latter will happen if + # there isn't actually a real column to get data from, or if there's + # multiple data values (read: a has_many or has_and_belongs_to_many + # association). + # + def to_group_sql + case + when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut? + nil + else + @columns.collect { |column| + column_with_prefix(column) + } + end + end + + def type_to_config + { + :multi => :sql_attr_multi, + :datetime => :sql_attr_timestamp, + :string => :sql_attr_str2ordinal, + :float => :sql_attr_float, + :boolean => :sql_attr_bool, + :integer => :sql_attr_uint + }[type] + end + + def include_as_association? + ! (type == :multi && (source == :query || source == :ranged_query)) + end + + # Returns the configuration value that should be used for + # the attribute. + # Special case is the multi-valued attribute that needs some + # extra configuration. + # + def config_value(offset = nil) + if type == :multi + multi_config = include_as_association? ? "field" : + source_value(offset).gsub(/\n\s*/, " ") + "uint #{unique_name} from #{multi_config}" + else + unique_name + end + end + + # Returns the unique name of the attribute - which is either the alias of + # the attribute, or the name of the only column - if there is only one. If + # there isn't, there should be an alias. Else things probably won't work. + # Consider yourself warned. + # + def unique_name + if @columns.length == 1 + @alias || @columns.first.__name + else + @alias + end + end + + # Returns the type of the column. If that's not already set, it returns + # :multi if there's the possibility of more than one value, :string if + # there's more than one association, otherwise it figures out what the + # actual column's datatype is and returns that. + # + def type + @type ||= begin + base_type = case + when is_many?, is_many_ints? + :multi + when @associations.values.flatten.length > 1 + :string + else + translated_type_from_database + end + + if base_type == :string && @crc + :integer + else + @crc = false + base_type + end + end + end + + def to_facet + return nil unless @faceted + + ThinkingSphinx::Facet.new(self) + end + + private + + def source_value(offset) + if is_string? + "#{source.to_s.dasherize}; #{columns.first.__name}" + elsif source == :ranged_query + "ranged-query; #{query offset} #{query_clause}; #{range_query}" + else + "query; #{query offset}" + end + end + + def query(offset) + assoc = association_for_mva + raise "Could not determine SQL for MVA" if assoc.nil? + + <<-SQL +SELECT #{foreign_key_for_mva assoc} + #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')}, + #{primary_key_for_mva(assoc)} AS #{quote_column(unique_name)} +FROM #{quote_table_name assoc.table} + SQL + end + + def query_clause + foreign_key = foreign_key_for_mva association_for_mva + "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end" + end + + def range_query + assoc = association_for_mva + foreign_key = foreign_key_for_mva assoc + "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}" + end + + def primary_key_for_mva(assoc) + quote_with_table( + assoc.table, assoc.primary_key_from_reflection || columns.first.__name + ) + end + + def foreign_key_for_mva(assoc) + quote_with_table assoc.table, assoc.reflection.primary_key_name + end + + def association_for_mva + @association_for_mva ||= associations[columns.first].detect { |assoc| + assoc.has_column?(columns.first.__name) + } + end + + def adapter + @adapter ||= @model.sphinx_database_adapter + end + + def quote_with_table(table, column) + "#{quote_table_name(table)}.#{quote_column(column)}" + end + + def quote_column(column) + @model.connection.quote_column_name(column) + end + + def quote_table_name(table_name) + @model.connection.quote_table_name(table_name) + end + + # Indication of whether the columns should be concatenated with a space + # between each value. True if there's either multiple sources or multiple + # associations. + # + def concat_ws? + multiple_associations? || @columns.length > 1 + end + + # Checks whether any column requires multiple associations (which only + # happens for polymorphic situations). + # + def multiple_associations? + associations.any? { |col,assocs| assocs.length > 1 } + end + + # Builds a column reference tied to the appropriate associations. This + # dives into the associations hash and their corresponding joins to + # figure out how to correctly reference a column in SQL. + # + def column_with_prefix(column) + if column.is_string? + column.__name + elsif associations[column].empty? + "#{@model.quoted_table_name}.#{quote_column(column.__name)}" + else + associations[column].collect { |assoc| + assoc.has_column?(column.__name) ? + "#{quote_table_name(assoc.join.aliased_table_name)}" + + ".#{quote_column(column.__name)}" : + nil + }.compact.join(', ') + end + end + + # Could there be more than one value related to the parent record? If so, + # then this will return true. If not, false. It's that simple. + # + def is_many? + associations.values.flatten.any? { |assoc| assoc.is_many? } + end + + def is_many_ints? + concat_ws? && all_ints? + end + + # Returns true if any of the columns are string values, instead of database + # column references. + def is_string? + columns.all? { |col| col.is_string? } + end + + def all_ints? + @columns.all? { |col| + klasses = @associations[col].empty? ? [@model] : + @associations[col].collect { |assoc| assoc.reflection.klass } + klasses.all? { |klass| + column = klass.columns.detect { |column| column.name == col.__name.to_s } + !column.nil? && column.type == :integer + } + } + end + + def type_from_database + klass = @associations.values.flatten.first ? + @associations.values.flatten.first.reflection.klass : @model + + klass.columns.detect { |col| + @columns.collect { |c| c.__name.to_s }.include? col.name + }.type + end + + def translated_type_from_database + case type_from_db = type_from_database + when :datetime, :string, :float, :boolean, :integer + type_from_db + when :decimal + :float + when :timestamp, :date + :datetime + else + raise <<-MESSAGE + +Cannot automatically map column type #{type_from_db} to an equivalent Sphinx +type (integer, float, boolean, datetime, string as ordinal). You could try to +explicitly convert the column's value in your define_index block: + has "CAST(column AS INT)", :type => :integer, :as => :column + MESSAGE + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb new file mode 100644 index 0000000..cb301b8 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb @@ -0,0 +1,15 @@ +module ThinkingSphinx + class ClassFacet < ThinkingSphinx::Facet + def name + :class + end + + def attribute_name + "class_crc" + end + + def value(object, attribute_value) + object.class.name + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb new file mode 100644 index 0000000..406c2ae --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb @@ -0,0 +1,147 @@ +module ThinkingSphinx + class Collection < ::Array + attr_reader :total_entries, :total_pages, :current_page, :per_page + attr_accessor :results + + # Compatibility with older versions of will_paginate + alias_method :page_count, :total_pages + + def initialize(page, per_page, entries, total_entries) + @current_page, @per_page, @total_entries = page, per_page, total_entries + + @total_pages = (entries / @per_page.to_f).ceil + end + + def self.ids_from_results(results, page, limit, options) + collection = self.new(page, limit, + results[:total] || 0, results[:total_found] || 0 + ) + collection.results = results + collection.replace results[:matches].collect { |match| + match[:attributes]["sphinx_internal_id"] + } + return collection + end + + def self.create_from_results(results, page, limit, options) + collection = self.new(page, limit, + results[:total] || 0, results[:total_found] || 0 + ) + collection.results = results + collection.replace instances_from_matches(results[:matches], options) + return collection + end + + def self.instances_from_matches(matches, options = {}) + if klass = options[:class] + instances_from_class klass, matches, options + else + instances_from_classes matches, options + end + end + + def self.instances_from_class(klass, matches, options = {}) + index_options = klass.sphinx_index_options + + ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] } + instances = ids.length > 0 ? klass.find( + :all, + :conditions => {klass.primary_key.to_sym => ids}, + :include => (options[:include] || index_options[:include]), + :select => (options[:select] || index_options[:select]), + :order => (options[:sql_order] || index_options[:sql_order]) + ) : [] + + # Raise an exception if we find records in Sphinx but not in the DB, so + # the search method can retry without them. See + # ThinkingSphinx::Search.retry_search_on_stale_index. + if options[:raise_on_stale] && instances.length < ids.length + stale_ids = ids - instances.map {|i| i.id } + raise StaleIdsException, stale_ids + end + + # if the user has specified an SQL order, return the collection + # without rearranging it into the Sphinx order + return instances if options[:sql_order] + + ids.collect { |obj_id| + instances.detect { |obj| obj.id == obj_id } + } + end + + # Group results by class and call #find(:all) once for each group to reduce + # the number of #find's in multi-model searches. + # + def self.instances_from_classes(matches, options = {}) + groups = matches.group_by { |match| match[:attributes]["class_crc"] } + groups.each do |crc, group| + group.replace( + instances_from_class(class_from_crc(crc), group, options) + ) + end + + matches.collect do |match| + groups.detect { |crc, group| + crc == match[:attributes]["class_crc"] + }[1].detect { |obj| + obj.id == match[:attributes]["sphinx_internal_id"] + } + end + end + + def self.class_from_crc(crc) + @@models_by_crc ||= ThinkingSphinx.indexed_models.inject({}) do |hash, model| + hash[model.constantize.to_crc32] = model + model.constantize.subclasses.each { |subclass| + hash[subclass.to_crc32] = subclass.name + } + hash + end + @@models_by_crc[crc].constantize + end + + def previous_page + current_page > 1 ? (current_page - 1) : nil + end + + def next_page + current_page < total_pages ? (current_page + 1): nil + end + + def offset + (current_page - 1) * @per_page + end + + def method_missing(method, *args, &block) + super unless method.to_s[/^each_with_.*/] + + each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block + end + + def each_with_groupby_and_count(&block) + results[:matches].each_with_index do |match, index| + yield self[index], match[:attributes]["@groupby"], match[:attributes]["@count"] + end + end + + def each_with_attribute(attribute, &block) + results[:matches].each_with_index do |match, index| + yield self[index], (match[:attributes][attribute] || match[:attributes]["@#{attribute}"]) + end + end + + def each_with_weighting(&block) + results[:matches].each_with_index do |match, index| + yield self[index], match[:weight] + end + end + + def inject_with_groupby_and_count(initial = nil, &block) + index = -1 + results[:matches].inject(initial) do |memo, match| + index += 1 + yield memo, self[index], match[:attributes]["@groupby"], match[:attributes]["@count"] + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb new file mode 100644 index 0000000..31543f5 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb @@ -0,0 +1,237 @@ +require 'erb' +require 'singleton' + +module ThinkingSphinx + # This class both keeps track of the configuration settings for Sphinx and + # also generates the resulting file for Sphinx to use. + # + # Here are the default settings, relative to RAILS_ROOT where relevant: + # + # config file:: config/#{environment}.sphinx.conf + # searchd log file:: log/searchd.log + # query log file:: log/searchd.query.log + # pid file:: log/searchd.#{environment}.pid + # searchd files:: db/sphinx/#{environment}/ + # address:: 127.0.0.1 + # port:: 3312 + # allow star:: false + # min prefix length:: 1 + # min infix length:: 1 + # mem limit:: 64M + # max matches:: 1000 + # morphology:: stem_en + # charset type:: utf-8 + # charset table:: nil + # ignore chars:: nil + # html strip:: false + # html remove elements:: '' + # + # If you want to change these settings, create a YAML file at + # config/sphinx.yml with settings for each environment, in a similar + # fashion to database.yml - using the following keys: config_file, + # searchd_log_file, query_log_file, pid_file, searchd_file_path, port, + # allow_star, enable_star, min_prefix_len, min_infix_len, mem_limit, + # max_matches, # morphology, charset_type, charset_table, ignore_chars, + # html_strip, # html_remove_elements. I think you've got the idea. + # + # Each setting in the YAML file is optional - so only put in the ones you + # want to change. + # + # Keep in mind, if for some particular reason you're using a version of + # Sphinx older than 0.9.8 r871 (that's prior to the proper 0.9.8 release), + # don't set allow_star to true. + # + class Configuration + include Singleton + + SourceOptions = %w( mysql_connect_flags sql_range_step sql_query_pre + sql_query_post sql_ranged_throttle sql_query_post_index ) + + IndexOptions = %w( charset_table charset_type docinfo enable_star + exceptions html_index_attrs html_remove_elements html_strip ignore_chars + min_infix_len min_prefix_len min_word_len mlock morphology ngram_chars + ngram_len phrase_boundary phrase_boundary_step preopen stopwords + wordforms ) + + attr_accessor :config_file, :searchd_log_file, :query_log_file, + :pid_file, :searchd_file_path, :address, :port, :allow_star, + :database_yml_file, :app_root, :bin_path, :model_directories + + attr_accessor :source_options, :index_options + + attr_reader :environment, :configuration + + # Load in the configuration settings - this will look for config/sphinx.yml + # and parse it according to the current environment. + # + def initialize(app_root = Dir.pwd) + self.reset + end + + def reset + self.app_root = RAILS_ROOT if defined?(RAILS_ROOT) + self.app_root = Merb.root if defined?(Merb) + self.app_root ||= app_root + + @configuration = Riddle::Configuration.new + @configuration.searchd.address = "127.0.0.1" + @configuration.searchd.port = 3312 + @configuration.searchd.pid_file = "#{self.app_root}/log/searchd.#{environment}.pid" + @configuration.searchd.log = "#{self.app_root}/log/searchd.log" + @configuration.searchd.query_log = "#{self.app_root}/log/searchd.query.log" + + self.database_yml_file = "#{self.app_root}/config/database.yml" + self.config_file = "#{self.app_root}/config/#{environment}.sphinx.conf" + self.searchd_file_path = "#{self.app_root}/db/sphinx/#{environment}" + self.allow_star = false + self.bin_path = "" + self.model_directories = ["#{app_root}/app/models/"] + + Dir.glob("#{app_root}/vendor/plugins/*/app/models/") + + self.source_options = {} + self.index_options = { + :charset_type => "utf-8", + :morphology => "stem_en" + } + + parse_config + + self + end + + def self.environment + @@environment ||= ( + defined?(Merb) ? Merb.environment : ENV['RAILS_ENV'] + ) || "development" + end + + def environment + self.class.environment + end + + def controller + @controller ||= Riddle::Controller.new(@configuration, self.config_file) + end + + # Generate the config file for Sphinx by using all the settings defined and + # looping through all the models with indexes to build the relevant + # indexer and searchd configuration, and sources and indexes details. + # + def build(file_path=nil) + load_models + file_path ||= "#{self.config_file}" + + @configuration.indexes.clear + + ThinkingSphinx.indexed_models.each_with_index do |model, model_index| + @configuration.indexes.concat model.constantize.to_riddle(model_index) + end + + open(file_path, "w") do |file| + file.write @configuration.render + end + end + + # Make sure all models are loaded - without reloading any that + # ActiveRecord::Base is already aware of (otherwise we start to hit some + # messy dependencies issues). + # + def load_models + self.model_directories.each do |base| + Dir["#{base}**/*.rb"].each do |file| + model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1') + + next if model_name.nil? + next if ::ActiveRecord::Base.send(:subclasses).detect { |model| + model.name == model_name + } + + begin + model_name.camelize.constantize + rescue LoadError + model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry + rescue NameError + next + end + end + end + end + + def address + @configuration.searchd.address + end + + def address=(address) + @configuration.searchd.address = address + end + + def port + @configuration.searchd.port + end + + def port=(port) + @configuration.searchd.port = port + end + + def pid_file + @configuration.searchd.pid_file + end + + def pid_file=(pid_file) + @configuration.searchd.pid_file = pid_file + end + + def searchd_log_file + @configuration.searchd.log + end + + def searchd_log_file=(file) + @configuration.searchd.log = file + end + + def query_log_file + @configuration.searchd.query_log + end + + def query_log_file=(file) + @configuration.searchd.query_log = file + end + + private + + # Parse the config/sphinx.yml file - if it exists - then use the attribute + # accessors to set the appropriate values. Nothing too clever. + # + def parse_config + path = "#{app_root}/config/sphinx.yml" + return unless File.exists?(path) + + conf = YAML::load(ERB.new(IO.read(path)).result)[environment] + + conf.each do |key,value| + self.send("#{key}=", value) if self.methods.include?("#{key}=") + + set_sphinx_setting self.source_options, key, value, SourceOptions + set_sphinx_setting self.index_options, key, value, IndexOptions + set_sphinx_setting @configuration.searchd, key, value + set_sphinx_setting @configuration.indexer, key, value + end unless conf.nil? + + self.bin_path += '/' unless self.bin_path.blank? + + if self.allow_star + self.index_options[:enable_star] = true + self.index_options[:min_prefix_len] = 1 + end + end + + def set_sphinx_setting(object, key, value, allowed = {}) + if object.is_a?(Hash) + object[key.to_sym] = value if allowed.include?(key.to_s) + else + object.send("#{key}=", value) if object.methods.include?("#{key}") + send("#{key}=", value) if self.methods.include?("#{key}") + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb new file mode 100644 index 0000000..7ab3e62 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb @@ -0,0 +1,15 @@ +require 'zlib' + +module ThinkingSphinx + module Core + module String + def to_crc32 + Zlib.crc32 self + end + end + end +end + +class String + include ThinkingSphinx::Core::String +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb new file mode 100644 index 0000000..57c396e --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb @@ -0,0 +1,27 @@ +require 'thinking_sphinx/deltas/default_delta' +require 'thinking_sphinx/deltas/delayed_delta' +require 'thinking_sphinx/deltas/datetime_delta' + +module ThinkingSphinx + module Deltas + def self.parse(index, options) + delta_option = options.delete(:delta) + case delta_option + when TrueClass, :default + DefaultDelta.new index, options + when :delayed + DelayedDelta.new index, options + when :datetime + DatetimeDelta.new index, options + when FalseClass, nil + nil + else + if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta) + delta_option.new index, options + else + raise "Unknown delta type" + end + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb new file mode 100644 index 0000000..2ee46d4 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb @@ -0,0 +1,50 @@ +module ThinkingSphinx + module Deltas + class DatetimeDelta < ThinkingSphinx::Deltas::DefaultDelta + attr_accessor :column, :threshold + + def initialize(index, options) + @index = index + @column = options.delete(:delta_column) || :updated_at + @threshold = options.delete(:threshold) || 1.day + end + + def index(model, instance = nil) + # do nothing + true + end + + def delayed_index(model) + config = ThinkingSphinx::Configuration.instance + rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : "" + + output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}` + output += `#{config.bin_path}indexer --config #{config.config_file} #{rotate} --merge #{core_index_name model} #{delta_index_name model} --merge-dst-range sphinx_deleted 0 0` + puts output unless ThinkingSphinx.suppress_delta_output? + + true + end + + def toggle(instance) + # do nothing + end + + def toggled(instance) + instance.send(@column) > @threshold.ago + end + + def reset_query(model) + nil + end + + def clause(model, toggled) + if toggled + "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" + + " > #{adapter.time_difference(@threshold)}" + else + nil + end + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb new file mode 100644 index 0000000..c973612 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb @@ -0,0 +1,67 @@ +module ThinkingSphinx + module Deltas + class DefaultDelta + attr_accessor :column + + def initialize(index, options) + @index = index + @column = options.delete(:delta_column) || :delta + end + + def index(model, instance = nil) + return true unless ThinkingSphinx.updates_enabled? && + ThinkingSphinx.deltas_enabled? + return true if instance && !toggled(instance) + + config = ThinkingSphinx::Configuration.instance + client = Riddle::Client.new config.address, config.port + rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : "" + + output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}` + puts(output) unless ThinkingSphinx.suppress_delta_output? + + client.update( + core_index_name(model), + ['sphinx_deleted'], + {instance.sphinx_document_id => [1]} + ) if instance && ThinkingSphinx.sphinx_running? && instance.in_both_indexes? + + true + end + + def toggle(instance) + instance.delta = true + end + + def toggled(instance) + instance.delta + end + + def reset_query(model) + "UPDATE #{model.quoted_table_name} SET " + + "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}" + end + + def clause(model, toggled) + "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" + + " = #{adapter.boolean(toggled)}" + end + + protected + + def core_index_name(model) + "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core" + end + + def delta_index_name(model) + "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta" + end + + private + + def adapter + @adapter = @index.model.sphinx_database_adapter + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb new file mode 100644 index 0000000..e95298b --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb @@ -0,0 +1,25 @@ +require 'delayed/job' + +require 'thinking_sphinx/deltas/delayed_delta/delta_job' +require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job' +require 'thinking_sphinx/deltas/delayed_delta/job' + +module ThinkingSphinx + module Deltas + class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta + def index(model, instance = nil) + ThinkingSphinx::Deltas::Job.enqueue( + ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model)) + ) + + Delayed::Job.enqueue( + ThinkingSphinx::Deltas::FlagAsDeletedJob.new( + core_index_name(model), instance.sphinx_document_id + ) + ) if instance + + true + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb new file mode 100644 index 0000000..f9511ec --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb @@ -0,0 +1,24 @@ +module ThinkingSphinx + module Deltas + class DeltaJob + attr_accessor :index + + def initialize(index) + @index = index + end + + def perform + return true unless ThinkingSphinx.updates_enabled? && + ThinkingSphinx.deltas_enabled? + + config = ThinkingSphinx::Configuration.instance + client = Riddle::Client.new config.address, config.port + + output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{index}` + puts output unless ThinkingSphinx.suppress_delta_output? + + true + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb new file mode 100644 index 0000000..d6afd27 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb @@ -0,0 +1,27 @@ +module ThinkingSphinx + module Deltas + class FlagAsDeletedJob + attr_accessor :index, :document_id + + def initialize(index, document_id) + @index, @document_id = index, document_id + end + + def perform + return true unless ThinkingSphinx.updates_enabled? + + config = ThinkingSphinx::Configuration.instance + client = Riddle::Client.new config.address, config.port + + client.update( + @index, + ['sphinx_deleted'], + {@document_id => [1]} + ) if ThinkingSphinx.sphinx_running? && + ThinkingSphinx::Search.search_for_id(@document_id, @index) + + true + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb new file mode 100644 index 0000000..de0a7cb --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb @@ -0,0 +1,26 @@ +module ThinkingSphinx + module Deltas + class Job < Delayed::Job + def self.enqueue(object, priority = 0) + super unless duplicates_exist(object) + end + + def self.cancel_thinking_sphinx_jobs + if connection.tables.include?("delayed_jobs") + delete_all("handler LIKE '--- !ruby/object:ThinkingSphinx::Deltas::%'") + end + end + + private + + def self.duplicates_exist(object) + count( + :conditions => { + :handler => object.to_yaml, + :locked_at => nil + } + ) > 0 + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb new file mode 100644 index 0000000..e2db449 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb @@ -0,0 +1,58 @@ +module ThinkingSphinx + class Facet + attr_reader :reference + + def initialize(reference) + @reference = reference + + if reference.columns.length != 1 + raise "Can't translate Facets on multiple-column field or attribute" + end + end + + def name + reference.unique_name + end + + def attribute_name + # @attribute_name ||= case @reference + # when Attribute + # @reference.unique_name.to_s + # when Field + @attribute_name ||= @reference.unique_name.to_s + "_facet" + # end + end + + def value(object, attribute_value) + return translate(object, attribute_value) if @reference.is_a?(Field) + + case @reference.type + when :string + translate(object, attribute_value) + when :datetime + Time.at(attribute_value) + when :boolean + attribute_value > 0 + else + attribute_value + end + end + + def to_s + name + end + + private + + def translate(object, attribute_value) + column.__stack.each { |method| + object = object.send(method) + } + object.send(column.__name) + end + + def column + @reference.columns.first + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb new file mode 100644 index 0000000..1ad9d1a --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb @@ -0,0 +1,60 @@ +module ThinkingSphinx + class FacetCollection < Hash + attr_accessor :arguments + + def initialize(arguments) + @arguments = arguments.clone + @attribute_values = {} + @facets = [] + end + + def add_from_results(facet, results) + facet = facet_from_object(results.first, facet) if facet.is_a?(String) + + self[facet.name] ||= {} + @attribute_values[facet.name] ||= {} + @facets << facet + + results.each_with_groupby_and_count { |result, group, count| + facet_value = facet.value(result, group) + + self[facet.name][facet_value] ||= 0 + self[facet.name][facet_value] += count + @attribute_values[facet.name][facet_value] = group + } + end + + def for(hash = {}) + arguments = @arguments.clone + options = arguments.extract_options! + options[:with] ||= {} + + hash.each do |key, value| + attrib = facet_for_key(key).attribute_name + options[:with][attrib] = underlying_value key, value + end + + arguments << options + ThinkingSphinx::Search.search *arguments + end + + private + + def underlying_value(key, value) + case value + when Array + value.collect { |item| underlying_value(key, item) } + else + @attribute_values[key][value] + end + end + + def facet_for_key(key) + @facets.detect { |facet| facet.name == key } + end + + def facet_from_object(object, name) + object.sphinx_facets.detect { |facet| facet.attribute_name == name } + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb new file mode 100644 index 0000000..9edaede --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb @@ -0,0 +1,172 @@ +module ThinkingSphinx + # Fields - holding the string data which Sphinx indexes for your searches. + # This class isn't really useful to you unless you're hacking around with the + # internals of Thinking Sphinx - but hey, don't let that stop you. + # + # One key thing to remember - if you're using the field manually to + # generate SQL statements, you'll need to set the base model, and all the + # associations. Which can get messy. Use Index.link!, it really helps. + # + class Field + attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes, + :prefixes, :faceted + + # To create a new field, you'll need to pass in either a single Column + # or an array of them, and some (optional) options. The columns are + # references to the data that will make up the field. + # + # Valid options are: + # - :as => :alias_name + # - :sortable => true + # - :infixes => true + # - :prefixes => true + # + # Alias is only required in three circumstances: when there's + # another attribute or field with the same name, when the column name is + # 'id', or when there's more than one column. + # + # Sortable defaults to false - but is quite useful when set to true, as + # it creates an attribute with the same string value (which Sphinx converts + # to an integer value), which can be sorted by. Thinking Sphinx is smart + # enough to realise that when you specify fields in sort statements, you + # mean their respective attributes. + # + # If you have partial matching enabled (ie: enable_star), then you can + # specify certain fields to have their prefixes and infixes indexed. Keep + # in mind, though, that Sphinx's default is _all_ fields - so once you + # highlight a particular field, no other fields in the index will have + # these partial indexes. + # + # Here's some examples: + # + # Field.new( + # Column.new(:name) + # ) + # + # Field.new( + # [Column.new(:first_name), Column.new(:last_name)], + # :as => :name, :sortable => true + # ) + # + # Field.new( + # [Column.new(:posts, :subject), Column.new(:posts, :content)], + # :as => :posts, :prefixes => true + # ) + # + def initialize(columns, options = {}) + @columns = Array(columns) + @associations = {} + + raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) } + + @alias = options[:as] + @sortable = options[:sortable] || false + @infixes = options[:infixes] || false + @prefixes = options[:prefixes] || false + @faceted = options[:facet] || false + end + + # Get the part of the SELECT clause related to this field. Don't forget + # to set your model and associations first though. + # + # This will concatenate strings if there's more than one data source or + # multiple data values (has_many or has_and_belongs_to_many associations). + # + def to_select_sql + clause = @columns.collect { |column| + column_with_prefix(column) + }.join(', ') + + clause = adapter.concatenate(clause) if concat_ws? + clause = adapter.group_concatenate(clause) if is_many? + + "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}" + end + + # Get the part of the GROUP BY clause related to this field - if one is + # needed. If not, all you'll get back is nil. The latter will happen if + # there's multiple data values (read: a has_many or has_and_belongs_to_many + # association). + # + def to_group_sql + case + when is_many?, ThinkingSphinx.use_group_by_shortcut? + nil + else + @columns.collect { |column| + column_with_prefix(column) + } + end + end + + # Returns the unique name of the field - which is either the alias of + # the field, or the name of the only column - if there is only one. If + # there isn't, there should be an alias. Else things probably won't work. + # Consider yourself warned. + # + def unique_name + if @columns.length == 1 + @alias || @columns.first.__name + else + @alias + end + end + + def to_facet + return nil unless @faceted + + ThinkingSphinx::Facet.new(self) + end + + private + + def adapter + @adapter ||= @model.sphinx_database_adapter + end + + def quote_column(column) + @model.connection.quote_column_name(column) + end + + # Indication of whether the columns should be concatenated with a space + # between each value. True if there's either multiple sources or multiple + # associations. + # + def concat_ws? + @columns.length > 1 || multiple_associations? + end + + # Checks whether any column requires multiple associations (which only + # happens for polymorphic situations). + # + def multiple_associations? + associations.any? { |col,assocs| assocs.length > 1 } + end + + # Builds a column reference tied to the appropriate associations. This + # dives into the associations hash and their corresponding joins to + # figure out how to correctly reference a column in SQL. + # + def column_with_prefix(column) + if column.is_string? + column.__name + elsif associations[column].empty? + "#{@model.quoted_table_name}.#{quote_column(column.__name)}" + else + associations[column].collect { |assoc| + assoc.has_column?(column.__name) ? + "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" + + ".#{quote_column(column.__name)}" : + nil + }.compact.join(', ') + end + end + + # Could there be more than one value related to the parent record? If so, + # then this will return true. If not, false. It's that simple. + # + def is_many? + associations.values.flatten.any? { |assoc| assoc.is_many? } + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb new file mode 100644 index 0000000..30cfe38 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb @@ -0,0 +1,423 @@ +require 'thinking_sphinx/index/builder' +require 'thinking_sphinx/index/faux_column' + +module ThinkingSphinx + # The Index class is a ruby representation of a Sphinx source (not a Sphinx + # index - yes, I know it's a little confusing. You'll manage). This is + # another 'internal' Thinking Sphinx class - if you're using it directly, + # you either know what you're doing, or messing with things beyond your ken. + # Enjoy. + # + class Index + attr_accessor :model, :fields, :attributes, :conditions, :groupings, + :delta_object, :options + + # Create a new index instance by passing in the model it is tied to, and + # a block to build it with (optional but recommended). For documentation + # on the syntax for inside the block, the Builder class is what you want. + # + # Quick Example: + # + # Index.new(User) do + # indexes login, email + # + # has created_at + # + # set_property :delta => true + # end + # + def initialize(model, &block) + @model = model + @associations = {} + @fields = [] + @attributes = [] + @conditions = [] + @groupings = [] + @options = {} + @delta_object = nil + + initialize_from_builder(&block) if block_given? + end + + def name + self.class.name(@model) + end + + def self.name(model) + model.name.underscore.tr(':/\\', '_') + end + + def to_riddle_for_core(offset, index) + add_internal_attributes_and_facets + link! + + source = Riddle::Configuration::SQLSource.new( + "#{name}_core_#{index}", adapter.sphinx_identifier + ) + + set_source_database_settings source + set_source_attributes source, offset + set_source_sql source, offset + set_source_settings source + + source + end + + def to_riddle_for_delta(offset, index) + add_internal_attributes_and_facets + link! + + source = Riddle::Configuration::SQLSource.new( + "#{name}_delta_#{index}", adapter.sphinx_identifier + ) + source.parent = "#{name}_core_#{index}" + + set_source_database_settings source + set_source_attributes source, offset + set_source_sql source, offset, true + + source + end + + # Link all the fields and associations to their corresponding + # associations and joins. This _must_ be called before interrogating + # the index's fields and associations for anything that may reference + # their SQL structure. + # + def link! + base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new( + @model, [], nil + ) + + @fields.each { |field| + field.model ||= @model + field.columns.each { |col| + field.associations[col] = associations(col.__stack.clone) + field.associations[col].each { |assoc| assoc.join_to(base) } + } + } + + @attributes.each { |attribute| + attribute.model ||= @model + attribute.columns.each { |col| + attribute.associations[col] = associations(col.__stack.clone) + attribute.associations[col].each { |assoc| assoc.join_to(base) } + } + } + end + + # Flag to indicate whether this index has a corresponding delta index. + # + def delta? + !@delta_object.nil? + end + + def adapter + @adapter ||= @model.sphinx_database_adapter + end + + def prefix_fields + @fields.select { |field| field.prefixes } + end + + def infix_fields + @fields.select { |field| field.infixes } + end + + def index_options + all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone + @options.keys.select { |key| + ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) + }.each { |key| all_index_options[key.to_sym] = @options[key] } + all_index_options + end + + def quote_column(column) + @model.connection.quote_column_name(column) + end + + private + + def utf8? + self.index_options[:charset_type] == "utf-8" + end + + # Does all the magic with the block provided to the base #initialize. + # Creates a new class subclassed from Builder, and evaluates the block + # on it, then pulls all relevant settings - fields, attributes, conditions, + # properties - into the new index. + # + # Also creates a CRC attribute for the model. + # + def initialize_from_builder(&block) + builder = Class.new(Builder) + builder.setup + + builder.instance_eval &block + + unless @model.descends_from_active_record? + stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize + builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'") + end + + set_model = Proc.new { |item| item.model = @model } + + @fields = builder.fields &set_model + @attributes = builder.attributes.each &set_model + @conditions = builder.conditions + @groupings = builder.groupings + @delta_object = ThinkingSphinx::Deltas.parse self, builder.properties + @options = builder.properties + + is_faceted = Proc.new { |item| item.faceted } + add_facet = Proc.new { |item| @model.sphinx_facets << item.to_facet } + + @model.sphinx_facets ||= [] + @fields.select( &is_faceted).each &add_facet + @attributes.select(&is_faceted).each &add_facet + + # We want to make sure that if the database doesn't exist, then Thinking + # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop + # and db:migrate). It's a bit hacky, but I can't think of a better way. + rescue StandardError => err + case err.class.name + when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid" + return + else + raise err + end + end + + # Returns all associations used amongst all the fields and attributes. + # This includes all associations between the model and what the actual + # columns are from. + # + def all_associations + @all_associations ||= ( + # field associations + @fields.collect { |field| + field.associations.values + }.flatten + + # attribute associations + @attributes.collect { |attrib| + attrib.associations.values if attrib.include_as_association? + }.compact.flatten + ).uniq.collect { |assoc| + # get ancestors as well as column-level associations + assoc.ancestors + }.flatten.uniq + end + + # Gets a stack of associations for a specific path. + # + def associations(path, parent = nil) + assocs = [] + + if parent.nil? + assocs = association(path.shift) + else + assocs = parent.children(path.shift) + end + + until path.empty? + point = path.shift + assocs = assocs.collect { |assoc| + assoc.children(point) + }.flatten + end + + assocs + end + + # Gets the association stack for a specific key. + # + def association(key) + @associations[key] ||= Association.children(@model, key) + end + + def crc_column + if @model.column_names.include?(@model.inheritance_column) + adapter.cast_to_unsigned(adapter.convert_nulls( + adapter.crc(adapter.quote_with_table(@model.inheritance_column), true), + @model.to_crc32 + )) + else + @model.to_crc32.to_s + end + end + + def add_internal_attributes_and_facets + add_internal_attribute :sphinx_internal_id, :integer, @model.primary_key.to_sym + add_internal_attribute :class_crc, :integer, crc_column, true + add_internal_attribute :subclass_crcs, :multi, subclasses_to_s + add_internal_attribute :sphinx_deleted, :integer, "0" + + add_internal_facet :class_crc + end + + def add_internal_attribute(name, type, contents, facet = false) + return unless attribute_by_alias(name).nil? + + @attributes << Attribute.new( + FauxColumn.new(contents), + :type => type, + :as => name, + :facet => facet + ) + end + + def add_internal_facet(name) + return unless facet_by_alias(name).nil? + + @model.sphinx_facets << ClassFacet.new(attribute_by_alias(name)) + end + + def attribute_by_alias(attr_alias) + @attributes.detect { |attrib| attrib.alias == attr_alias } + end + + def facet_by_alias(name) + @model.sphinx_facets.detect { |facet| facet.name == name } + end + + def subclasses_to_s + "'" + (@model.send(:subclasses).collect { |klass| + klass.to_crc32.to_s + } << @model.to_crc32.to_s).join(",") + "'" + end + + def set_source_database_settings(source) + config = @model.connection.instance_variable_get(:@config) + + source.sql_host = config[:host] || "localhost" + source.sql_user = config[:username] || config[:user] || "" + source.sql_pass = (config[:password].to_s || "").gsub('#', '\#') + source.sql_db = config[:database] + source.sql_port = config[:port] + source.sql_sock = config[:socket] + end + + def set_source_attributes(source, offset = nil) + attributes.each do |attrib| + source.send(attrib.type_to_config) << attrib.config_value(offset) + end + end + + def set_source_sql(source, offset, delta = false) + source.sql_query = to_sql(:offset => offset, :delta => delta).gsub(/\n/, ' ') + source.sql_query_range = to_sql_query_range(:delta => delta) + source.sql_query_info = to_sql_query_info(offset) + + source.sql_query_pre += send(!delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta) + + if @options[:group_concat_max_len] + source.sql_query_pre << "SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" + end + + source.sql_query_pre += [adapter.utf8_query_pre].compact if utf8? + end + + def set_source_settings(source) + ThinkingSphinx::Configuration.instance.source_options.each do |key, value| + source.send("#{key}=".to_sym, value) + end + + @options.each do |key, value| + source.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::SourceOptions.include?(key.to_s) && !value.nil? + end + end + + def sql_query_pre_for_core + if self.delta? && !@delta_object.reset_query(@model).blank? + [@delta_object.reset_query(@model)] + else + [] + end + end + + def sql_query_pre_for_delta + [""] + end + + # Generates the big SQL statement to get the data back for all the fields + # and attributes, using all the relevant association joins. If you want + # the version filtered for delta values, send through :delta => true in the + # options. Won't do much though if the index isn't set up to support a + # delta sibling. + # + # Examples: + # + # index.to_sql + # index.to_sql(:delta => true) + # + def to_sql(options={}) + assocs = all_associations + + where_clause = "" + if self.delta? && !@delta_object.clause(@model, options[:delta]).blank? + where_clause << " AND #{@delta_object.clause(@model, options[:delta])}" + end + unless @conditions.empty? + where_clause << " AND " << @conditions.join(" AND ") + end + + internal_groupings = [] + if @model.column_names.include?(@model.inheritance_column) + internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}" + end + + unique_id_expr = ThinkingSphinx.unique_id_expression(options[:offset]) + + sql = <<-SQL +SELECT #{ ( + ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] + + @fields.collect { |field| field.to_select_sql } + + @attributes.collect { |attribute| attribute.to_select_sql } +).compact.join(", ") } +FROM #{ @model.table_name } + #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') } +WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start + AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end + #{ where_clause } +GROUP BY #{ ( + ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] + + @fields.collect { |field| field.to_group_sql }.compact + + @attributes.collect { |attribute| attribute.to_group_sql }.compact + + @groupings + internal_groupings +).join(", ") } + SQL + + sql += " ORDER BY NULL" if adapter.sphinx_identifier == "mysql" + sql + end + + # Simple helper method for the query info SQL - which is a statement that + # returns the single row for a corresponding id. + # + def to_sql_query_info(offset) + "SELECT * FROM #{@model.quoted_table_name} WHERE " + + " #{quote_column(@model.primary_key)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})" + end + + # Simple helper method for the query range SQL - which is a statement that + # returns minimum and maximum id values. These can be filtered by delta - + # so pass in :delta => true to get the delta version of the SQL. + # + def to_sql_query_range(options={}) + min_statement = adapter.convert_nulls( + "MIN(#{quote_column(@model.primary_key)})", 1 + ) + max_statement = adapter.convert_nulls( + "MAX(#{quote_column(@model.primary_key)})", 1 + ) + + sql = "SELECT #{min_statement}, #{max_statement} " + + "FROM #{@model.quoted_table_name} " + if self.delta? && !@delta_object.clause(@model, options[:delta]).blank? + sql << "WHERE #{@delta_object.clause(@model, options[:delta])}" + end + + sql + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb new file mode 100644 index 0000000..dbd2ba0 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb @@ -0,0 +1,264 @@ +module ThinkingSphinx + class Index + # The Builder class is the core for the index definition block processing. + # There are four methods you really need to pay attention to: + # - indexes (aliased to includes and attribute) + # - has (aliased to attribute) + # - where + # - set_property (aliased to set_properties) + # + # The first two of these methods allow you to define what data makes up + # your indexes. #where provides a method to add manual SQL conditions, and + # set_property allows you to set some settings on a per-index basis. Check + # out each method's documentation for better ideas of usage. + # + class Builder + class << self + # No idea where this is coming from - haven't found it in any ruby or + # rails documentation. It's not needed though, so it gets undef'd. + # Hopefully the list of methods that get in the way doesn't get too + # long. + HiddenMethods = [:parent, :name, :id, :type].each { |method| + define_method(method) { + caller.grep(/irb.completion/).empty? ? method_missing(method) : super + } + } + + attr_accessor :fields, :attributes, :properties, :conditions, + :groupings + + # Set up all the collections. Consider this the equivalent of an + # instance's initialize method. + # + def setup + @fields = [] + @attributes = [] + @properties = {} + @conditions = [] + @groupings = [] + end + + # This is how you add fields - the strings Sphinx looks at - to your + # index. Technically, to use this method, you need to pass in some + # columns and options - but there's some neat method_missing stuff + # happening, so lets stick to the expected syntax within a define_index + # block. + # + # Expected options are :as, which points to a column alias in symbol + # form, and :sortable, which indicates whether you want to sort by this + # field. + # + # Adding Single-Column Fields: + # + # You can use symbols or methods - and can chain methods together to + # get access down the associations tree. + # + # indexes :id, :as => :my_id + # indexes :name, :sortable => true + # indexes first_name, last_name, :sortable => true + # indexes users.posts.content, :as => :post_content + # indexes users(:id), :as => :user_ids + # + # Keep in mind that if any keywords for Ruby methods - such as id or + # name - clash with your column names, you need to use the symbol + # version (see the first, second and last examples above). + # + # If you specify multiple columns (example #2), a field will be created + # for each. Don't use the :as option in this case. If you want to merge + # those columns together, continue reading. + # + # Adding Multi-Column Fields: + # + # indexes [first_name, last_name], :as => :name + # indexes [location, parent.location], :as => :location + # + # To combine multiple columns into a single field, you need to wrap + # them in an Array, as shown by the above examples. There's no + # limitations on whether they're symbols or methods or what level of + # associations they come from. + # + # Adding SQL Fragment Fields + # + # You can also define a field using an SQL fragment, useful for when + # you would like to index a calculated value. + # + # indexes "age < 18", :as => :minor + # + def indexes(*args) + options = args.extract_options! + args.each do |columns| + field = Field.new(FauxColumn.coerce(columns), options) + fields << field + + add_sort_attribute field, options if field.sortable + add_facet_attribute field, options if field.faceted + end + end + alias_method :field, :indexes + alias_method :includes, :indexes + + # This is the method to add attributes to your index (hence why it is + # aliased as 'attribute'). The syntax is the same as #indexes, so use + # that as starting point, but keep in mind the following points. + # + # An attribute can have an alias (the :as option), but it is always + # sortable - so you don't need to explicitly request that. You _can_ + # specify the data type of the attribute (the :type option), but the + # code's pretty good at figuring that out itself from peering into the + # database. + # + # Attributes are limited to the following types: integers, floats, + # datetimes (converted to timestamps), booleans and strings. Don't + # forget that Sphinx converts string attributes to integers, which are + # useful for sorting, but that's about it. + # + # You can also have a collection of integers for multi-value attributes + # (MVAs). Generally these would be through a has_many relationship, + # like in this example: + # + # has posts(:id), :as => :post_ids + # + # This allows you to filter on any of the values tied to a specific + # record. Might be best to read through the Sphinx documentation to get + # a better idea of that though. + # + # Adding SQL Fragment Attributes + # + # You can also define an attribute using an SQL fragment, useful for + # when you would like to index a calculated value. Don't forget to set + # the type of the attribute though: + # + # has "age < 18", :as => :minor, :type => :boolean + # + # If you're creating attributes for latitude and longitude, don't + # forget that Sphinx expects these values to be in radians. + # + def has(*args) + options = args.extract_options! + args.each do |columns| + attribute = Attribute.new(FauxColumn.coerce(columns), options) + attributes << attribute + + add_facet_attribute attribute, options if attribute.faceted + end + end + alias_method :attribute, :has + + def facet(*args) + options = args.extract_options! + options[:facet] = true + + args.each do |columns| + attribute = Attribute.new(FauxColumn.coerce(columns), options) + attributes << attribute + + add_facet_attribute attribute, options + end + end + + # Use this method to add some manual SQL conditions for your index + # request. You can pass in as many strings as you like, they'll get + # joined together with ANDs later on. + # + # where "user_id = 10" + # where "parent_type = 'Article'", "created_at < NOW()" + # + def where(*args) + @conditions += args + end + + # Use this method to add some manual SQL strings to the GROUP BY + # clause. You can pass in as many strings as you'd like, they'll get + # joined together with commas later on. + # + # group_by "lat", "lng" + # + def group_by(*args) + @groupings += args + end + + # This is what to use to set properties on the index. Chief amongst + # those is the delta property - to allow automatic updates to your + # indexes as new models are added and edited - but also you can + # define search-related properties which will be the defaults for all + # searches on the model. + # + # set_property :delta => true + # set_property :field_weights => {"name" => 100} + # set_property :order => "name ASC" + # set_property :include => :picture + # set_property :select => 'name' + # + # Also, the following two properties are particularly relevant for + # geo-location searching - latitude_attr and longitude_attr. If your + # attributes for these two values are named something other than + # lat/latitude or lon/long/longitude, you can dictate what they are + # when defining the index, so you don't need to specify them for every + # geo-related search. + # + # set_property :latitude_attr => "lt", :longitude_attr => "lg" + # + # Please don't forget to add a boolean field named 'delta' to your + # model's database table if enabling the delta index for it. + # Valid options for the delta property are: + # + # true + # false + # :default + # :delayed + # :datetime + # + # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement + # your own handling for delta indexing. + + def set_property(*args) + options = args.extract_options! + if options.empty? + @properties[args[0]] = args[1] + else + @properties.merge!(options) + end + end + alias_method :set_properties, :set_property + + # Handles the generation of new columns for the field and attribute + # definitions. + # + def method_missing(method, *args) + FauxColumn.new(method, *args) + end + + # A method to allow adding fields from associations which have names + # that clash with method names in the Builder class (ie: properties, + # fields, attributes). + # + # Example: indexes assoc(:properties).column + # + def assoc(assoc, *args) + FauxColumn.new(assoc, *args) + end + + private + + def add_sort_attribute(field, options) + add_internal_attribute field, options, "_sort" + end + + def add_facet_attribute(resource, options) + add_internal_attribute resource, options, "_facet", true + end + + def add_internal_attribute(resource, options, suffix, crc = false) + @attributes << Attribute.new( + resource.columns.collect { |col| col.clone }, + options.merge( + :type => resource.is_a?(Field) ? :string : nil, + :as => resource.unique_name.to_s.concat(suffix).to_sym, + :crc => crc + ).except(:facet) + ) + end + end + end + end +end diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb new file mode 100644 index 0000000..84068de --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb @@ -0,0 +1,110 @@ +module ThinkingSphinx + class Index + # Instances of this class represent database columns and the stack of + # associations that lead from the base model to them. + # + # The name and stack are accessible through methods starting with __ to + # avoid conflicting with the method_missing calls that build the stack. + # + class FauxColumn + # Create a new column with a pre-defined stack. The top element in the + # stack will get shifted to be the name value. + # + def initialize(*stack) + @name = stack.pop + @stack = stack + end + + def self.coerce(columns) + case columns + when Symbol, String + FauxColumn.new(columns) + when Array + columns.collect { |col| FauxColumn.coerce(col) } + when FauxColumn + columns + else + nil + end + end + + # Can't use normal method name, as that could be an association or + # column name. + # + def __name + @name + end + + # Can't use normal method name, as that could be an association or + # column name. + # + def __stack + @stack + end + + # Returns true if the stack is empty *and* if the name is a string - + # which is an indication that of raw SQL, as opposed to a value from a + # table's column. + # + def is_string? + @name.is_a?(String) && @stack.empty? + end + + # This handles any 'invalid' method calls and sets them as the name, + # and pushing the previous name into the stack. The object returns + # itself. + # + # If there's a single argument, it becomes the name, and the method + # symbol goes into the stack as well. Multiple arguments means new + # columns with the original stack and new names (from each argument) gets + # returned. + # + # Easier to explain with examples: + # + # col = FauxColumn.new :a, :b, :c + # col.__name #=> :c + # col.__stack #=> [:a, :b] + # + # col.whatever #=> col + # col.__name #=> :whatever + # col.__stack #=> [:a, :b, :c] + # + # col.something(:id) #=> col + # col.__name #=> :id + # col.__stack #=> [:a, :b, :c, :whatever, :something] + # + # cols = col.short(:x, :y, :z) + # cols[0].__name #=> :x + # cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short] + # cols[1].__name #=> :y + # cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short] + # cols[2].__name #=> :z + # cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short] + # + # Also, this allows method chaining to build up a relevant stack: + # + # col = FauxColumn.new :a, :b + # col.__name #=> :b + # col.__stack #=> [:a] + # + # col.one.two.three #=> col + # col.__name #=> :three + # col.__stack #=> [:a, :b, :one, :two] + # + def method_missing(method, *args) + @stack << @name + @name = method + + if (args.empty?) + self + elsif (args.length == 1) + method_missing(args.first) + else + args.collect { |arg| + FauxColumn.new(@stack + [@name, arg]) + } + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb new file mode 100644 index 0000000..c7db0a1 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb @@ -0,0 +1,136 @@ +module ThinkingSphinx + module HashExcept + # Returns a new hash without the given keys. + def except(*keys) + rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys) + reject { |key,| rejected.include?(key) } + end + + # Replaces the hash without only the given keys. + def except!(*keys) + replace(except(*keys)) + end + end +end + +Hash.send( + :include, ThinkingSphinx::HashExcept +) unless Hash.instance_methods.include?("except") + +module ThinkingSphinx + module ArrayExtractOptions + def extract_options! + last.is_a?(::Hash) ? pop : {} + end + end +end + +Array.send( + :include, ThinkingSphinx::ArrayExtractOptions +) unless Array.instance_methods.include?("extract_options!") + +module ThinkingSphinx + module AbstractQuotedTableName + def quote_table_name(name) + quote_column_name(name) + end + end +end + +ActiveRecord::ConnectionAdapters::AbstractAdapter.send( + :include, ThinkingSphinx::AbstractQuotedTableName +) unless ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_methods.include?("quote_table_name") + +module ThinkingSphinx + module MysqlQuotedTableName + def quote_table_name(name) #:nodoc: + quote_column_name(name).gsub('.', '`.`') + end + end +end + +if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") or ActiveRecord::Base.respond_to?(:jdbcmysql_connection) + adapter = ActiveRecord::ConnectionAdapters.const_get( + defined?(JRUBY_VERSION) ? :JdbcAdapter : :MysqlAdapter + ) + unless adapter.instance_methods.include?("quote_table_name") + adapter.send(:include, ThinkingSphinx::MysqlQuotedTableName) + end +end + +module ThinkingSphinx + module ActiveRecordQuotedName + def quoted_table_name + self.connection.quote_table_name(self.table_name) + end + end +end + +ActiveRecord::Base.extend( + ThinkingSphinx::ActiveRecordQuotedName +) unless ActiveRecord::Base.respond_to?("quoted_table_name") + +module ThinkingSphinx + module ActiveRecordStoreFullSTIClass + def store_full_sti_class + false + end + end +end + +ActiveRecord::Base.extend( + ThinkingSphinx::ActiveRecordStoreFullSTIClass +) unless ActiveRecord::Base.respond_to?(:store_full_sti_class) + +module ThinkingSphinx + module ClassAttributeMethods + def cattr_reader(*syms) + syms.flatten.each do |sym| + next if sym.is_a?(Hash) + class_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym} + @@#{sym} + end + + def #{sym} + @@#{sym} + end + EOS + end + end + + def cattr_writer(*syms) + options = syms.extract_options! + syms.flatten.each do |sym| + class_eval(<<-EOS, __FILE__, __LINE__) + unless defined? @@#{sym} + @@#{sym} = nil + end + + def self.#{sym}=(obj) + @@#{sym} = obj + end + + #{" + def #{sym}=(obj) + @@#{sym} = obj + end + " unless options[:instance_writer] == false } + EOS + end + end + + def cattr_accessor(*syms) + cattr_reader(*syms) + cattr_writer(*syms) + end + end +end + +Class.extend( + ThinkingSphinx::ClassAttributeMethods +) unless Class.respond_to?(:cattr_reader) diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb new file mode 100644 index 0000000..d476787 --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb @@ -0,0 +1,780 @@ +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. + # + # Note that this only searches the Sphinx index, with no ActiveRecord + # queries. Thus, if your index is not in sync with the database, this + # method may return ids that no longer exist there. + # + def search_for_ids(*args) + results, client = search_results(*args.clone) + + options = args.extract_options! + page = options[:page] ? options[:page].to_i : 1 + + ThinkingSphinx::Collection.ids_from_results(results, page, client.limit, options) + end + + # Searches through the Sphinx indexes for relevant matches. There's + # various ways to search, sort, group and filter - which are covered + # below. + # + # Also, if you have WillPaginate installed, the search method can be used + # just like paginate. The same parameters - :page and :per_page - work as + # expected, and the returned result set can be used by the will_paginate + # helper. + # + # == Basic Searching + # + # The simplest way of searching is straight text. + # + # ThinkingSphinx::Search.search "pat" + # ThinkingSphinx::Search.search "google" + # User.search "pat", :page => (params[:page] || 1) + # Article.search "relevant news issue of the day" + # + # If you specify :include, like in an #find call, this will be respected + # when loading the relevant models from the search results. + # + # User.search "pat", :include => :posts + # + # == Match Modes + # + # Sphinx supports 5 different matching modes. By default Thinking Sphinx + # uses :all, which unsurprisingly requires all the supplied search terms + # to match a result. + # + # Alternative modes include: + # + # User.search "pat allan", :match_mode => :any + # User.search "pat allan", :match_mode => :phrase + # User.search "pat | allan", :match_mode => :boolean + # User.search "@name pat | @username pat", :match_mode => :extended + # + # Any will find results with any of the search terms. Phrase treats the search + # terms a single phrase instead of individual words. Boolean and extended allow + # for more complex query syntax, refer to the sphinx documentation for further + # details. + # + # == Weighting + # + # Sphinx has support for weighting, where matches in one field can be considered + # more important than in another. Weights are integers, with 1 as the default. + # They can be set per-search like this: + # + # User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 } + # + # If you're searching multiple models, you can set per-index weights: + # + # ThinkingSphinx::Search.search "pat", :index_weights => { User => 10 } + # + # See http://sphinxsearch.com/doc.html#weighting for further details. + # + # == Searching by Fields + # + # If you want to step it up a level, you can limit your search terms to + # specific fields: + # + # User.search :conditions => {:name => "pat"} + # + # This uses Sphinx's extended match mode, unless you specify a different + # match mode explicitly (but then this way of searching won't work). Also + # 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 three ways to do + # this. The first two techniques work in all scenarios - using the :with + # or :with_all options. + # + # 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 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. + # + # Article.search "East Timor", :conditions => {:rating => 3..5} + # + # == Excluding by Attributes + # + # Sphinx also supports negative filtering - where the filters are of + # attribute values to exclude. This is done with the :without option: + # + # User.search :without => {:role_id => 1} + # + # == Excluding by Primary Key + # + # There is a shortcut to exclude records by their ActiveRecord primary key: + # + # User.search :without_ids => 1 + # + # Pass an array or a single value. + # + # The primary key must be an integer as a negative filter is used. Note + # that for multi-model search, an id may occur in more than one model. + # + # == Infix (Star) Searching + # + # By default, Sphinx uses English stemming, e.g. matching "shoes" if you + # search for "shoe". It won't find "Melbourne" if you search for + # "elbourn", though. + # + # Enable infix searching by something like this in config/sphinx.yml: + # + # development: + # enable_star: 1 + # min_infix_length: 2 + # + # Note that this will make indexing take longer. + # + # With those settings (and after reindexing), wildcard asterisks can be used + # in queries: + # + # Location.search "*elbourn*" + # + # To automatically add asterisks around every token (but not operators), + # pass the :star option: + # + # Location.search "elbourn -ustrali", :star => true, :match_mode => :boolean + # + # This would become "*elbourn* -*ustrali*". The :star option only adds the + # asterisks. You need to make the config/sphinx.yml changes yourself. + # + # By default, the tokens are assumed to match the regular expression /\w+/u. + # If you've modified the charset_table, pass another regular expression, e.g. + # + # User.search("oo@bar.c", :star => /[\w@.]+/u) + # + # to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*". + # + # == Sorting + # + # Sphinx can only sort by attributes, so generally you will need to avoid + # using field names in your :order option. However, if you're searching + # on a single model, and have specified some fields as sortable, you can + # use those field names and Thinking Sphinx will interpret accordingly. + # Remember: this will only happen for single-model searches, and only + # through the :order option. + # + # Location.search "Melbourne", :order => :state + # User.search :conditions => {:role_id => 2}, :order => "name ASC" + # + # Keep in mind that if you use a string, you *must* specify the direction + # (ASC or DESC) else Sphinx won't return any results. If you use a symbol + # then Thinking Sphinx assumes ASC, but if you wish to state otherwise, + # use the :sort_mode option: + # + # Location.search "Melbourne", :order => :state, :sort_mode => :desc + # + # 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. + # + # Grouping is done via three parameters within the options hash + # * :group_function determines the way grouping is done + # * :group_by determines the field which is used for grouping + # * :group_clause determines the sorting order + # + # === group_function + # + # Valid values for :group_function are + # * :day, :week, :month, :year - Grouping is done by the respective timeframes. + # * :attr, :attrpair - 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 :sort_mode and :order parameters. + # The group matches themselves however, will be sorted by :group_clause. + # + # 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 @groupby special attribute will contain the date for that group. + # Depending on the :group_function parameter, the date format will be + # + # * :day - YYYYMMDD + # * :week - YYYYNNN (NNN is the first day of the week in question, + # counting from the start of the year ) + # * :month - YYYYMM + # * :year - YYYY + # + # + # === Grouping by attribute + # + # The syntax is the same as grouping by timestamp, except for the fact that the + # :group_function 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 + # attributes. To search with that point, you can then use one of the + # following syntax examples: + # + # Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc" + # Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc" + # :latitude_attr => "latit", :longitude_attr => "longit" + # + # The first example applies when your latitude and longitude attributes + # are named any of lat, latitude, lon, long or longitude. If that's not + # the case, you will need to explicitly state them in your search, _or_ + # you can do so in your model: + # + # define_index do + # has :latit # Float column, stored in radians + # has :longit # Float column, stored in radians + # + # set_property :latitude_attr => "latit" + # set_property :longitude_attr => "longit" + # end + # + # Now, geo-location searching really only has an affect if you have a + # filter, sort or grouping clause related to it - otherwise it's just a + # normal search, and _will not_ return a distance value otherwise. To + # make use of the positioning difference, use the special attribute + # "@geodist" in any of your filters or sorting or grouping clauses. + # + # And don't forget - both the latitude and longitude you use in your + # search, and the values in your indexes, need to be stored as a float in radians, + # _not_ degrees. Keep in mind that if you do this conversion in SQL + # you will need to explicitly declare a column type of :float. + # + # define_index do + # has 'RADIANS(lat)', :as => :lat, :type => :float + # # ... + # end + # + # Once you've got your results set, you can access the distances as + # follows: + # + # @results.each_with_geodist do |result, distance| + # # ... + # end + # + # The distance value is returned as a float, representing the distance in + # metres. + # + # == Handling a Stale Index + # + # Especially if you don't use delta indexing, you risk having records in the + # Sphinx index that are no longer in the database. By default, those will simply + # come back as nils: + # + # >> pat_user.delete + # >> User.search("pat") + # Sphinx Result: [1,2] + # => [nil, <#User id: 2>] + # + # (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.) + # + # You can simply Array#compact these results or handle the nils in some other way, but + # Sphinx will still report two results, and the missing records may upset your layout. + # + # If you pass :retry_stale => true to a single-model search, missing records will + # cause Thinking Sphinx to retry the query but excluding those records. Since search + # is paginated, the new search could potentially include missing records as well, so by + # default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five + # times, and so on. If there are still missing ids on the last retry, they are + # shown as nils. + # + def search(*args) + query = args.clone # an array + options = query.extract_options! + + retry_search_on_stale_index(query, options) do + results, client = search_results(*(query + [options])) + + ::ActiveRecord::Base.logger.error( + "Sphinx Error: #{results[:error]}" + ) if results[:error] + + klass = options[:class] + page = options[:page] ? options[:page].to_i : 1 + + ThinkingSphinx::Collection.create_from_results(results, page, client.limit, options) + end + 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 + 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. + options[:raise_on_stale] = stale_retries_left > 0 + block.call + # If ThinkingSphinx::Collection.create_from_results found records in Sphinx but not + # in the DB and the :raise_on_stale option is set, this exception is raised. We retry + # a limited number of times, excluding the stale ids from the search. + rescue StaleIdsException => e + stale_retries_left -= 1 + + stale_ids |= e.ids # For logging + options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion + + tries = stale_retries_left + ::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [ + tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ') + ]) + + retry + end + end + + def count(*args) + results, client = search_results(*args.clone) + results[:total_found] || 0 + end + + # Checks if a document with the given id exists within a specific index. + # Expected parameters: + # + # - ID of the document + # - Index to check within + # - Options hash (defaults to {}) + # + # Example: + # + # ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User) + # + def search_for_id(*args) + options = args.extract_options! + client = client_from_options options + + query, filters = search_conditions( + options[:class], options[:conditions] || {} + ) + client.filters += filters + client.match_mode = :extended unless query.empty? + client.id_range = args.first..args.first + + begin + return client.query(query, args[1])[:matches].length > 0 + 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) + options = args.extract_options! + + if options[:class] + facets_for_model options[:class], args, options + else + facets_for_all_models args, options + end + 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) + options = args.extract_options! + query = args.join(' ') + client = client_from_options options + + query = star_query(query, options[:star]) if options[:star] + + extra_query, filters = search_conditions( + options[:class], options[:conditions] || {} + ) + client.filters += filters + client.match_mode = :extended unless extra_query.empty? + query = [query, extra_query].join(' ') + query.strip! # Because "" and " " are not equivalent + + 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 + ::ActiveRecord::Base.logger.debug "Sphinx Result: #{results[:matches].collect{|m| m[:attributes]["sphinx_internal_id"]}.inspect}" + rescue Errno::ECONNREFUSED => err + raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed." + end + + return results, client + end + + # Set all the appropriate settings for the client, using the provided + # options hash. + # + def client_from_options(options = {}) + config = ThinkingSphinx::Configuration.instance + client = Riddle::Client.new config.address, config.port + klass = options[:class] + index_options = klass ? klass.sphinx_index_options : {} + + # The Riddle default is per-query max_matches=1000. If we set the + # per-server max to a smaller value in sphinx.yml, we need to override + # the Riddle default or else we get search errors like + # "per-query max_matches=1000 out of bounds (per-server max_matches=200)" + if per_server_max_matches = config.configuration.searchd.max_matches + options[:max_matches] ||= per_server_max_matches + end + + # Turn :index_weights => { "foo" => 2, User => 1 } + # into :index_weights => { "foo" => 2, "user_core" => 1, "user_delta" => 1 } + if iw = options[:index_weights] + options[:index_weights] = iw.inject({}) do |hash, (index,weight)| + if index.is_a?(Class) + name = ThinkingSphinx::Index.name(index) + hash["#{name}_core"] = weight + hash["#{name}_delta"] = weight + else + hash[index] = weight + end + hash + end + end + + [ + :max_matches, :match_mode, :sort_mode, :sort_by, :id_range, + :group_by, :group_function, :group_clause, :group_distinct, :cut_off, + :retry_count, :retry_delay, :index_weights, :rank_mode, + :max_query_time, :field_weights, :filters, :anchor, :limit + ].each do |key| + client.send( + key.to_s.concat("=").to_sym, + options[key] || index_options[key] || client.send(key) + ) + end + + options[:classes] = [klass] if klass + + client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty? + + client.filters << Riddle::Client::Filter.new( + "sphinx_deleted", [0] + ) + + # class filters + client.filters << Riddle::Client::Filter.new( + "class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten + ) if options[:classes] + + # normal attribute filters + client.filters += options[:with].collect { |attr,val| + Riddle::Client::Filter.new attr.to_s, filter_value(val) + } if options[:with] + + # 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] + + client + end + + def star_query(query, custom_token = nil) + token = custom_token.is_a?(Regexp) ? custom_token : /\w+/u + + query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do + pre, proper, post = $`, $&, $' + is_operator = pre.match(%r{(\W|^)[@~/]\Z}) # E.g. "@foo", "/2", "~3", but not as part of a token + is_quote = proper.starts_with?('"') && proper.ends_with?('"') # E.g. "foo bar", with quotes + has_star = pre.ends_with?("*") || post.starts_with?("*") + if is_operator || is_quote || has_star + proper + else + "*#{proper}*" + end + end + end + + def filter_value(value) + case value + when Range + value.first.is_a?(Time) ? timestamp(value.first)..timestamp(value.last) : value + when Array + value.collect { |val| val.is_a?(Time) ? timestamp(val) : val } + else + Array(value) + end + end + + # Returns the integer timestamp for a Time object. + # + # If using Rails 2.1+, need to handle timezones to translate them back to + # UTC, as that's what datetimes will be stored as by MySQL. + # + # in_time_zone is a method that was added for the timezone support in + # Rails 2.1, which is why it's used for testing. I'm sure there's better + # ways, but this does the job. + # + def timestamp(value) + value.respond_to?(:in_time_zone) ? value.utc.to_i : value.to_i + end + + # Translate field and attribute conditions to the relevant search string + # and filters. + # + def search_conditions(klass, conditions={}) + attributes = klass ? klass.sphinx_indexes.collect { |index| + index.attributes.collect { |attrib| attrib.unique_name } + }.flatten : [] + + search_string = [] + filters = [] + + conditions.each do |key,val| + if attributes.include?(key.to_sym) + filters << Riddle::Client::Filter.new( + key.to_s, filter_value(val) + ) + else + search_string << "@#{key} #{val}" + end + end + + return search_string.join(' '), filters + end + + # Return the appropriate latitude and longitude values, depending on + # whether the relevant attributes have been defined, and also whether + # there's actually any values. + # + def anchor_conditions(klass, options) + attributes = klass ? klass.sphinx_indexes.collect { |index| + index.attributes.collect { |attrib| attrib.unique_name } + }.flatten : [] + + lat_attr = klass ? klass.sphinx_indexes.collect { |index| + index.options[:latitude_attr] + }.compact.first : nil + + lon_attr = klass ? klass.sphinx_indexes.collect { |index| + index.options[:longitude_attr] + }.compact.first : nil + + lat_attr = options[:latitude_attr] if options[:latitude_attr] + lat_attr ||= :lat if attributes.include?(:lat) + lat_attr ||= :latitude if attributes.include?(:latitude) + + lon_attr = options[:longitude_attr] if options[:longitude_attr] + lon_attr ||= :lng if attributes.include?(:lng) + lon_attr ||= :lon if attributes.include?(:lon) + lon_attr ||= :long if attributes.include?(:long) + lon_attr ||= :longitude if attributes.include?(:longitude) + + lat = options[:lat] + lon = options[:lon] + + if options[:geo] + lat = options[:geo].first + lon = options[:geo].last + end + + lat && lon ? { + :latitude_attribute => lat_attr.to_s, + :latitude => lat, + :longitude_attribute => lon_attr.to_s, + :longitude => lon + } : nil + end + + # Set the sort options using the :order key as well as the appropriate + # Riddle settings. + # + def set_sort_options!(client, options) + klass = options[:class] + fields = klass ? klass.sphinx_indexes.collect { |index| + index.fields.collect { |field| field.unique_name } + }.flatten : [] + index_options = klass ? klass.sphinx_index_options : {} + + order = options[:order] || index_options[:order] + case order + when Symbol + client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil? + if fields.include?(order) + client.sort_by = order.to_s.concat("_sort") + else + client.sort_by = order.to_s + end + when String + client.sort_mode = :extended + client.sort_by = sorted_fields_to_attributes(order, fields) + else + # do nothing + end + + client.sort_mode = :attr_asc if client.sort_mode == :asc + client.sort_mode = :attr_desc if client.sort_mode == :desc + end + + # Search through a collection of fields and translate any appearances + # of them in a string to their attribute equivalent for sorting. + # + def sorted_fields_to_attributes(string, fields) + fields.each { |field| + string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match| + 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! :group_function => :attr + + 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! :group_function => :attr + + 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_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 diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb new file mode 100644 index 0000000..ab8990e --- /dev/null +++ b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb @@ -0,0 +1,128 @@ +require 'fileutils' + +namespace :thinking_sphinx do + task :app_env do + Rake::Task[:environment].invoke if defined?(RAILS_ROOT) + Rake::Task[:merb_env].invoke if defined?(Merb) + end + + desc "Stop if running, then start a Sphinx searchd daemon using Thinking Sphinx's settings" + task :running_start => :app_env do + Rake::Task["thinking_sphinx:stop"].invoke if sphinx_running? + Rake::Task["thinking_sphinx:start"].invoke + end + + desc "Start a Sphinx searchd daemon using Thinking Sphinx's settings" + task :start => :app_env do + config = ThinkingSphinx::Configuration.instance + + FileUtils.mkdir_p config.searchd_file_path + raise RuntimeError, "searchd is already running." if sphinx_running? + + Dir["#{config.searchd_file_path}/*.spl"].each { |file| File.delete(file) } + + cmd = "#{config.bin_path}searchd --pidfile --config #{config.config_file}" + puts cmd + system cmd + + sleep(2) + + if sphinx_running? + puts "Started successfully (pid #{sphinx_pid})." + else + puts "Failed to start searchd daemon. Check #{config.searchd_log_file}." + end + end + + desc "Stop Sphinx using Thinking Sphinx's settings" + task :stop => :app_env do + raise RuntimeError, "searchd is not running." unless sphinx_running? + config = ThinkingSphinx::Configuration.instance + pid = sphinx_pid + system "#{config.bin_path}searchd --stop --config #{config.config_file}" + puts "Stopped search daemon (pid #{pid})." + end + + desc "Restart Sphinx" + task :restart => [:app_env, :stop, :start] + + desc "Generate the Sphinx configuration file using Thinking Sphinx's settings" + task :configure => :app_env do + config = ThinkingSphinx::Configuration.instance + puts "Generating Configuration to #{config.config_file}" + config.build + end + + desc "Index data for Sphinx using Thinking Sphinx's settings" + task :index => :app_env do + ThinkingSphinx::Deltas::Job.cancel_thinking_sphinx_jobs + + config = ThinkingSphinx::Configuration.instance + unless ENV["INDEX_ONLY"] == "true" + puts "Generating Configuration to #{config.config_file}" + config.build + end + + FileUtils.mkdir_p config.searchd_file_path + cmd = "#{config.bin_path}indexer --config #{config.config_file} --all" + cmd << " --rotate" if sphinx_running? + puts cmd + system cmd + end + + namespace :index do + task :delta => :app_env do + ThinkingSphinx.indexed_models.select { |model| + model.constantize.sphinx_indexes.any? { |index| index.delta? } + }.each do |model| + model.constantize.sphinx_indexes.select { |index| + index.delta? && index.delta_object.respond_to?(:delayed_index) + }.each { |index| + index.delta_object.delayed_index(index.model) + } + end + end + end + + desc "Process stored delta index requests" + task :delayed_delta => :app_env do + require 'delayed/worker' + + Delayed::Worker.new( + :min_priority => ENV['MIN_PRIORITY'], + :max_priority => ENV['MAX_PRIORITY'] + ).start + end +end + +namespace :ts do + desc "Stop if running, then start a Sphinx searchd daemon using Thinking Sphinx's settings" + task :run => "thinking_sphinx:running_start" + desc "Start a Sphinx searchd daemon using Thinking Sphinx's settings" + task :start => "thinking_sphinx:start" + desc "Stop Sphinx using Thinking Sphinx's settings" + task :stop => "thinking_sphinx:stop" + desc "Index data for Sphinx using Thinking Sphinx's settings" + task :in => "thinking_sphinx:index" + namespace :in do + desc "Index Thinking Sphinx datetime delta indexes" + task :delta => "thinking_sphinx:index:delta" + end + task :index => "thinking_sphinx:index" + desc "Restart Sphinx" + task :restart => "thinking_sphinx:restart" + desc "Generate the Sphinx configuration file using Thinking Sphinx's settings" + task :conf => "thinking_sphinx:configure" + desc "Generate the Sphinx configuration file using Thinking Sphinx's settings" + task :config => "thinking_sphinx:configure" + desc "Process stored delta index requests" + task :dd => "thinking_sphinx:delayed_delta" +end + +def sphinx_pid + ThinkingSphinx.sphinx_pid +end + +def sphinx_running? + ThinkingSphinx.sphinx_running? +end -- cgit v1.3