DrMark-thinking-sphinx 0.9.9 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. data/README +64 -2
  2. data/lib/thinking_sphinx.rb +88 -11
  3. data/lib/thinking_sphinx/active_record.rb +136 -21
  4. data/lib/thinking_sphinx/active_record/delta.rb +43 -62
  5. data/lib/thinking_sphinx/active_record/has_many_association.rb +1 -1
  6. data/lib/thinking_sphinx/active_record/search.rb +7 -0
  7. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  8. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  9. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +130 -0
  10. data/lib/thinking_sphinx/association.rb +17 -0
  11. data/lib/thinking_sphinx/attribute.rb +171 -97
  12. data/lib/thinking_sphinx/collection.rb +126 -2
  13. data/lib/thinking_sphinx/configuration.rb +120 -171
  14. data/lib/thinking_sphinx/core/string.rb +15 -0
  15. data/lib/thinking_sphinx/deltas.rb +27 -0
  16. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  17. data/lib/thinking_sphinx/deltas/default_delta.rb +67 -0
  18. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  22. data/lib/thinking_sphinx/facet.rb +58 -0
  23. data/lib/thinking_sphinx/facet_collection.rb +60 -0
  24. data/lib/thinking_sphinx/field.rb +18 -52
  25. data/lib/thinking_sphinx/index.rb +246 -199
  26. data/lib/thinking_sphinx/index/builder.rb +85 -16
  27. data/lib/thinking_sphinx/rails_additions.rb +85 -5
  28. data/lib/thinking_sphinx/search.rb +459 -190
  29. data/lib/thinking_sphinx/tasks.rb +128 -0
  30. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +53 -124
  31. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +2 -2
  32. data/spec/unit/thinking_sphinx/active_record_spec.rb +110 -30
  33. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -149
  34. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  35. data/spec/unit/thinking_sphinx/configuration_spec.rb +54 -412
  36. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  37. data/spec/unit/thinking_sphinx/field_spec.rb +0 -79
  38. data/spec/unit/thinking_sphinx/index/builder_spec.rb +1 -29
  39. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +1 -39
  40. data/spec/unit/thinking_sphinx/index_spec.rb +78 -226
  41. data/spec/unit/thinking_sphinx/search_spec.rb +29 -228
  42. data/spec/unit/thinking_sphinx_spec.rb +23 -19
  43. data/tasks/distribution.rb +48 -0
  44. data/tasks/rails.rake +1 -0
  45. data/tasks/testing.rb +86 -0
  46. data/vendor/after_commit/LICENSE +20 -0
  47. data/vendor/after_commit/README +16 -0
  48. data/vendor/after_commit/Rakefile +22 -0
  49. data/vendor/after_commit/init.rb +8 -0
  50. data/vendor/after_commit/lib/after_commit.rb +45 -0
  51. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  52. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  53. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  54. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  55. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  56. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  57. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  58. data/{lib → vendor/riddle/lib}/riddle.rb +9 -5
  59. data/{lib → vendor/riddle/lib}/riddle/client.rb +6 -26
  60. data/{lib → vendor/riddle/lib}/riddle/client/filter.rb +10 -1
  61. data/{lib → vendor/riddle/lib}/riddle/client/message.rb +0 -0
  62. data/{lib → vendor/riddle/lib}/riddle/client/response.rb +0 -0
  63. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  64. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  65. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  66. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  67. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  68. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  69. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  70. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  71. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  72. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  73. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  74. metadata +63 -10
  75. data/lib/test.rb +0 -46
  76. data/tasks/thinking_sphinx_tasks.rake +0 -1
  77. data/tasks/thinking_sphinx_tasks.rb +0 -86
@@ -11,84 +11,65 @@ module ThinkingSphinx
11
11
  #
12
12
  def self.included(base)
13
13
  base.class_eval do
14
- # The define_callbacks method was added post Rails 2.0.2 - if it
15
- # doesn't exist, we define the callback manually
16
- #
17
- if respond_to?(:define_callbacks)
18
- define_callbacks :after_commit
19
- else
20
- class << self
21
- # Handle after_commit callbacks - call all the registered callbacks.
22
- #
23
- def after_commit(*callbacks, &block)
24
- callbacks << block if block_given?
25
- write_inheritable_array(:after_commit, callbacks)
14
+ class << self
15
+ # Temporarily disable delta indexing inside a block, then perform a single
16
+ # rebuild of index at the end.
17
+ #
18
+ # Useful when performing updates to batches of models to prevent
19
+ # the delta index being rebuilt after each individual update.
20
+ #
21
+ # In the following example, the delta index will only be rebuilt once,
22
+ # not 10 times.
23
+ #
24
+ # SomeModel.suspended_delta do
25
+ # 10.times do
26
+ # SomeModel.create( ... )
27
+ # end
28
+ # end
29
+ #
30
+ def suspended_delta(reindex_after = true, &block)
31
+ original_setting = ThinkingSphinx.deltas_enabled?
32
+ ThinkingSphinx.deltas_enabled = false
33
+ begin
34
+ yield
35
+ ensure
36
+ ThinkingSphinx.deltas_enabled = original_setting
37
+ self.index_delta if reindex_after
26
38
  end
27
39
  end
40
+
41
+ # Build the delta index for the related model. This won't be called
42
+ # if running in the test environment.
43
+ #
44
+ def index_delta(instance = nil)
45
+ delta_object.index(self, instance)
46
+ end
47
+
48
+ def delta_object
49
+ self.sphinx_indexes.first.delta_object
50
+ end
28
51
  end
29
52
 
30
- def after_commit
31
- # Deliberately blank.
32
- end
33
-
34
- # Normal boolean save wrapped in a handler for the after_commit
35
- # callback.
36
- #
37
- def save_with_after_commit_callback(*args)
38
- value = save_without_after_commit_callback(*args)
39
- callback(:after_commit) if value
40
- return value
41
- end
42
-
43
- alias_method_chain :save, :after_commit_callback
44
-
45
- # Forceful save wrapped in a handler for the after_commit callback.
46
- #
47
- def save_with_after_commit_callback!(*args)
48
- value = save_without_after_commit_callback!(*args)
49
- callback(:after_commit) if value
50
- return value
51
- end
52
-
53
- alias_method_chain :save!, :after_commit_callback
54
-
55
- # Normal destroy wrapped in a handler for the after_commit callback.
56
- #
57
- def destroy_with_after_commit_callback
58
- value = destroy_without_after_commit_callback
59
- callback(:after_commit) if value
60
- return value
53
+ def toggled_delta?
54
+ self.class.delta_object.toggled(self)
61
55
  end
62
56
 
63
- alias_method_chain :destroy, :after_commit_callback
64
-
65
57
  private
66
58
 
67
59
  # Set the delta value for the model to be true.
68
60
  def toggle_delta
69
- self.delta = true
61
+ self.class.delta_object.toggle(self) if should_toggle_delta?
70
62
  end
71
63
 
72
64
  # Build the delta index for the related model. This won't be called
73
65
  # if running in the test environment.
74
66
  #
75
67
  def index_delta
76
- return true unless ThinkingSphinx.updates_enabled? &&
77
- ThinkingSphinx.deltas_enabled?
78
-
79
- config = ThinkingSphinx::Configuration.new
80
- client = Riddle::Client.new config.address, config.port
81
-
82
- client.update(
83
- "#{self.class.indexes.first.name}_core",
84
- ['sphinx_deleted'],
85
- {self.id => 1}
86
- ) if self.in_core_index?
87
-
88
- configuration = ThinkingSphinx::Configuration.new
89
- system "indexer --config #{configuration.config_file} --rotate #{self.class.indexes.first.name}_delta"
90
-
91
- true
68
+ self.class.index_delta(self) if self.class.delta_object.toggled(self)
69
+ end
70
+
71
+ def should_toggle_delta?
72
+ !self.respond_to?(:changed?) || self.changed? || self.new_record?
92
73
  end
93
74
  end
94
75
  end
@@ -6,7 +6,7 @@ module ThinkingSphinx
6
6
  stack = [@reflection.options[:through]].compact
7
7
 
8
8
  attribute = nil
9
- (@reflection.klass.indexes || []).each do |index|
9
+ (@reflection.klass.sphinx_indexes || []).each do |index|
10
10
  attribute = index.attributes.detect { |attrib|
11
11
  attrib.columns.length == 1 &&
12
12
  attrib.columns.first.__name == foreign_key.to_sym &&
@@ -42,6 +42,13 @@ module ThinkingSphinx
42
42
  args << options
43
43
  ThinkingSphinx::Search.search_for_id(*args)
44
44
  end
45
+
46
+ def facets(*args)
47
+ options = args.extract_options!
48
+ options[:class] = self
49
+ args << options
50
+ ThinkingSphinx::Search.facets(*args)
51
+ end
45
52
  end
46
53
  end
47
54
  end
@@ -0,0 +1,42 @@
1
+ module ThinkingSphinx
2
+ class AbstractAdapter
3
+ def initialize(model)
4
+ @model = model
5
+ end
6
+
7
+ def setup
8
+ # Deliberately blank - subclasses should do something though. Well, if
9
+ # they need to.
10
+ end
11
+
12
+ def self.detect(model)
13
+ case model.connection.class.name
14
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
15
+ "ActiveRecord::ConnectionAdapters::MysqlplusAdapter"
16
+ ThinkingSphinx::MysqlAdapter.new model
17
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
18
+ ThinkingSphinx::PostgreSQLAdapter.new model
19
+ when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
20
+ if model.connection.config[:adapter] == "jdbcmysql"
21
+ ThinkingSphinx::MysqlAdapter.new model
22
+ elsif model.connection.config[:adapter] == "jdbcpostgresql"
23
+ ThinkingSphinx::PostgreSQLAdapter.new model
24
+ else
25
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
26
+ end
27
+ else
28
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
29
+ end
30
+ end
31
+
32
+ def quote_with_table(column)
33
+ "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
34
+ end
35
+
36
+ protected
37
+
38
+ def connection
39
+ @connection ||= @model.connection
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ module ThinkingSphinx
2
+ class MysqlAdapter < AbstractAdapter
3
+ def setup
4
+ # Does MySQL actually need to do anything?
5
+ end
6
+
7
+ def sphinx_identifier
8
+ "mysql"
9
+ end
10
+
11
+ def concatenate(clause, separator = ' ')
12
+ "CONCAT_WS('#{separator}', #{clause})"
13
+ end
14
+
15
+ def group_concatenate(clause, separator = ' ')
16
+ "GROUP_CONCAT(DISTINCT #{clause} SEPARATOR '#{separator}')"
17
+ end
18
+
19
+ def cast_to_string(clause)
20
+ "CAST(#{clause} AS CHAR)"
21
+ end
22
+
23
+ def cast_to_datetime(clause)
24
+ "UNIX_TIMESTAMP(#{clause})"
25
+ end
26
+
27
+ def cast_to_unsigned(clause)
28
+ "CAST(#{clause} AS UNSIGNED)"
29
+ end
30
+
31
+ def convert_nulls(clause, default = '')
32
+ default = "'#{default}'" if default.is_a?(String)
33
+
34
+ "IFNULL(#{clause}, #{default})"
35
+ end
36
+
37
+ def boolean(value)
38
+ value ? 1 : 0
39
+ end
40
+
41
+ def crc(clause, blank_to_null = false)
42
+ clause = "NULLIF(#{clause},'')" if blank_to_null
43
+ "CRC32(#{clause})"
44
+ end
45
+
46
+ def utf8_query_pre
47
+ "SET NAMES utf8"
48
+ end
49
+
50
+ def time_difference(diff)
51
+ "DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,130 @@
1
+ module ThinkingSphinx
2
+ class PostgreSQLAdapter < AbstractAdapter
3
+ def setup
4
+ create_array_accum_function
5
+ create_crc32_function
6
+ end
7
+
8
+ def sphinx_identifier
9
+ "pgsql"
10
+ end
11
+
12
+ def concatenate(clause, separator = ' ')
13
+ clause.split(', ').collect { |field|
14
+ "COALESCE(CAST(#{field} as varchar), '')"
15
+ }.join(" || '#{separator}' || ")
16
+ end
17
+
18
+ def group_concatenate(clause, separator = ' ')
19
+ "array_to_string(array_accum(#{clause}), '#{separator}')"
20
+ end
21
+
22
+ def cast_to_string(clause)
23
+ clause
24
+ end
25
+
26
+ def cast_to_datetime(clause)
27
+ "cast(extract(epoch from #{clause}) as int)"
28
+ end
29
+
30
+ def cast_to_unsigned(clause)
31
+ clause
32
+ end
33
+
34
+ def convert_nulls(clause, default = '')
35
+ default = "'#{default}'" if default.is_a?(String)
36
+
37
+ "COALESCE(#{clause}, #{default})"
38
+ end
39
+
40
+ def boolean(value)
41
+ value ? 'TRUE' : 'FALSE'
42
+ end
43
+
44
+ def crc(clause, blank_to_null = false)
45
+ clause = "NULLIF(#{clause},'')" if blank_to_null
46
+ "crc32(#{clause})"
47
+ end
48
+
49
+ def utf8_query_pre
50
+ nil
51
+ end
52
+
53
+ def time_difference(diff)
54
+ "current_timestamp - interval '#{diff} seconds'"
55
+ end
56
+
57
+ private
58
+
59
+ def execute(command, output_error = false)
60
+ connection.execute "begin"
61
+ connection.execute "savepoint ts"
62
+ begin
63
+ connection.execute command
64
+ rescue StandardError => err
65
+ puts err if output_error
66
+ connection.execute "rollback to savepoint ts"
67
+ end
68
+ connection.execute "release savepoint ts"
69
+ connection.execute "commit"
70
+ end
71
+
72
+ def create_array_accum_function
73
+ if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
74
+ execute <<-SQL
75
+ CREATE AGGREGATE array_accum (anyelement)
76
+ (
77
+ sfunc = array_append,
78
+ stype = anyarray,
79
+ initcond = '{}'
80
+ );
81
+ SQL
82
+ else
83
+ execute <<-SQL
84
+ CREATE AGGREGATE array_accum
85
+ (
86
+ basetype = anyelement,
87
+ sfunc = array_append,
88
+ stype = anyarray,
89
+ initcond = '{}'
90
+ );
91
+ SQL
92
+ end
93
+ end
94
+
95
+ def create_crc32_function
96
+ execute "CREATE LANGUAGE 'plpgsql';"
97
+ function = <<-SQL
98
+ CREATE OR REPLACE FUNCTION crc32(word text)
99
+ RETURNS bigint AS $$
100
+ DECLARE tmp bigint;
101
+ DECLARE i int;
102
+ DECLARE j int;
103
+ DECLARE word_array bytea;
104
+ BEGIN
105
+ i = 0;
106
+ tmp = 4294967295;
107
+ word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
108
+ LOOP
109
+ tmp = (tmp # get_byte(word_array, i))::bigint;
110
+ i = i + 1;
111
+ j = 0;
112
+ LOOP
113
+ tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
114
+ j = j + 1;
115
+ IF j >= 8 THEN
116
+ EXIT;
117
+ END IF;
118
+ END LOOP;
119
+ IF i >= char_length(word) THEN
120
+ EXIT;
121
+ END IF;
122
+ END LOOP;
123
+ return (tmp # 4294967295);
124
+ END
125
+ $$ IMMUTABLE STRICT LANGUAGE plpgsql;
126
+ SQL
127
+ execute function, true
128
+ end
129
+ end
130
+ end
@@ -99,6 +99,23 @@ module ThinkingSphinx
99
99
  @reflection.klass.column_names.include?(column.to_s)
100
100
  end
101
101
 
102
+ def primary_key_from_reflection
103
+ if @reflection.options[:through]
104
+ @reflection.source_reflection.options[:foreign_key] ||
105
+ @reflection.source_reflection.primary_key_name
106
+ else
107
+ nil
108
+ end
109
+ end
110
+
111
+ def table
112
+ if @reflection.options[:through]
113
+ @join.aliased_join_table_name
114
+ else
115
+ @join.aliased_table_name
116
+ end
117
+ end
118
+
102
119
  private
103
120
 
104
121
  # Returns all the objects that could be currently instantiated from a
@@ -9,14 +9,15 @@ module ThinkingSphinx
9
9
  # associations. Which can get messy. Use Index.link!, it really helps.
10
10
  #
11
11
  class Attribute
12
- attr_accessor :alias, :columns, :associations, :model
12
+ attr_accessor :alias, :columns, :associations, :model, :faceted, :source
13
13
 
14
14
  # To create a new attribute, you'll need to pass in either a single Column
15
15
  # or an array of them, and some (optional) options.
16
16
  #
17
17
  # Valid options are:
18
- # - :as => :alias_name
19
- # - :type => :attribute_type
18
+ # - :as => :alias_name
19
+ # - :type => :attribute_type
20
+ # - :source => :field, :query, :ranged_query
20
21
  #
21
22
  # Alias is only required in three circumstances: when there's
22
23
  # another attribute or field with the same name, when the column name is
@@ -28,6 +29,13 @@ module ThinkingSphinx
28
29
  # can't be figured out by the column - ie: when not actually using a
29
30
  # database column as your source.
30
31
  #
32
+ # Source is only used for multi-value attributes (MVA). By default this will
33
+ # use a left-join and a group_concat to obtain the values. For better performance
34
+ # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
35
+ # all document,value-pairs.
36
+ # Either :query or :ranged_query will enable this feature, where :ranged_query will cause
37
+ # the query to be executed incremental.
38
+ #
31
39
  # Example usage:
32
40
  #
33
41
  # Attribute.new(
@@ -40,6 +48,12 @@ module ThinkingSphinx
40
48
  # )
41
49
  #
42
50
  # Attribute.new(
51
+ # Column.new(:posts, :id),
52
+ # :as => :post_ids,
53
+ # :source => :ranged_query
54
+ # )
55
+ #
56
+ # Attribute.new(
43
57
  # [Column.new(:pages, :id), Column.new(:articles, :id)],
44
58
  # :as => :content_ids
45
59
  # )
@@ -59,8 +73,14 @@ module ThinkingSphinx
59
73
 
60
74
  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) }
61
75
 
62
- @alias = options[:as]
63
- @type = options[:type]
76
+ @alias = options[:as]
77
+ @type = options[:type]
78
+ @faceted = options[:facet]
79
+ @source = options[:source]
80
+ @crc = options[:crc]
81
+
82
+ @type ||= :multi unless @source.nil?
83
+ @type = :integer if @type == :string && @crc
64
84
  end
65
85
 
66
86
  # Get the part of the SELECT clause related to this attribute. Don't forget
@@ -70,16 +90,19 @@ module ThinkingSphinx
70
90
  # datetimes to timestamps, as needed.
71
91
  #
72
92
  def to_select_sql
93
+ return nil unless include_as_association?
94
+
73
95
  clause = @columns.collect { |column|
74
96
  column_with_prefix(column)
75
97
  }.join(', ')
76
98
 
77
99
  separator = all_ints? ? ',' : ' '
78
100
 
79
- clause = concatenate(clause, separator) if concat_ws?
80
- clause = group_concatenate(clause, separator) if is_many?
81
- clause = cast_to_datetime(clause) if type == :datetime
82
- clause = convert_nulls(clause) if type == :string
101
+ clause = adapter.concatenate(clause, separator) if concat_ws?
102
+ clause = adapter.group_concatenate(clause, separator) if is_many?
103
+ clause = adapter.cast_to_datetime(clause) if type == :datetime
104
+ clause = adapter.convert_nulls(clause) if type == :string
105
+ clause = adapter.crc(clause) if @crc
83
106
 
84
107
  "#{clause} AS #{quote_column(unique_name)}"
85
108
  end
@@ -101,23 +124,33 @@ module ThinkingSphinx
101
124
  end
102
125
  end
103
126
 
104
- # Generates the appropriate attribute statement for a Sphinx configuration
105
- # file, depending on the attribute's type.
127
+ def type_to_config
128
+ {
129
+ :multi => :sql_attr_multi,
130
+ :datetime => :sql_attr_timestamp,
131
+ :string => :sql_attr_str2ordinal,
132
+ :float => :sql_attr_float,
133
+ :boolean => :sql_attr_bool,
134
+ :integer => :sql_attr_uint
135
+ }[type]
136
+ end
137
+
138
+ def include_as_association?
139
+ ! (type == :multi && (source == :query || source == :ranged_query))
140
+ end
141
+
142
+ # Returns the configuration value that should be used for
143
+ # the attribute.
144
+ # Special case is the multi-valued attribute that needs some
145
+ # extra configuration.
106
146
  #
107
- def to_sphinx_clause
108
- case type
109
- when :multi
110
- "sql_attr_multi = uint #{unique_name} from field"
111
- when :datetime
112
- "sql_attr_timestamp = #{unique_name}"
113
- when :string
114
- "sql_attr_str2ordinal = #{unique_name}"
115
- when :float
116
- "sql_attr_float = #{unique_name}"
117
- when :boolean
118
- "sql_attr_bool = #{unique_name}"
147
+ def config_value(offset = nil)
148
+ if type == :multi
149
+ multi_config = include_as_association? ? "field" :
150
+ source_value(offset).gsub(/\n\s*/, " ")
151
+ "uint #{unique_name} from #{multi_config}"
119
152
  else
120
- "sql_attr_uint = #{unique_name}"
153
+ unique_name
121
154
  end
122
155
  end
123
156
 
@@ -134,67 +167,104 @@ module ThinkingSphinx
134
167
  end
135
168
  end
136
169
 
170
+ # Returns the type of the column. If that's not already set, it returns
171
+ # :multi if there's the possibility of more than one value, :string if
172
+ # there's more than one association, otherwise it figures out what the
173
+ # actual column's datatype is and returns that.
174
+ #
175
+ def type
176
+ @type ||= begin
177
+ base_type = case
178
+ when is_many?, is_many_ints?
179
+ :multi
180
+ when @associations.values.flatten.length > 1
181
+ :string
182
+ else
183
+ translated_type_from_database
184
+ end
185
+
186
+ if base_type == :string && @crc
187
+ :integer
188
+ else
189
+ @crc = false
190
+ base_type
191
+ end
192
+ end
193
+ end
194
+
195
+ def to_facet
196
+ return nil unless @faceted
197
+
198
+ ThinkingSphinx::Facet.new(self)
199
+ end
200
+
137
201
  private
138
202
 
139
- def concatenate(clause, separator = ' ')
140
- case @model.connection.class.name
141
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
142
- "CONCAT_WS('#{separator}', #{clause})"
143
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
144
- clause.split(', ').join(" || '#{separator}' || ")
203
+ def source_value(offset)
204
+ if is_string?
205
+ "#{source.to_s.dasherize}; #{columns.first.__name}"
206
+ elsif source == :ranged_query
207
+ "ranged-query; #{query offset} #{query_clause}; #{range_query}"
145
208
  else
146
- clause
209
+ "query; #{query offset}"
147
210
  end
148
211
  end
149
212
 
150
- def group_concatenate(clause, separator = ' ')
151
- case @model.connection.class.name
152
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
153
- "GROUP_CONCAT(#{clause} SEPARATOR '#{separator}')"
154
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
155
- "array_to_string(array_accum(#{clause}), '#{separator}')"
156
- else
157
- clause
158
- end
213
+ def query(offset)
214
+ assoc = association_for_mva
215
+ raise "Could not determine SQL for MVA" if assoc.nil?
216
+
217
+ <<-SQL
218
+ SELECT #{foreign_key_for_mva assoc}
219
+ #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')},
220
+ #{primary_key_for_mva(assoc)} AS #{quote_column(unique_name)}
221
+ FROM #{quote_table_name assoc.table}
222
+ SQL
159
223
  end
160
224
 
161
- def cast_to_string(clause)
162
- case @model.connection.class.name
163
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
164
- "CAST(#{clause} AS CHAR)"
165
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
166
- clause
167
- else
168
- clause
169
- end
225
+ def query_clause
226
+ foreign_key = foreign_key_for_mva association_for_mva
227
+ "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
170
228
  end
171
229
 
172
- def cast_to_datetime(clause)
173
- case @model.connection.class.name
174
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
175
- "UNIX_TIMESTAMP(#{clause})"
176
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
177
- clause # Rails' datetimes are timestamps in PostgreSQL
178
- else
179
- clause
180
- end
230
+ def range_query
231
+ assoc = association_for_mva
232
+ foreign_key = foreign_key_for_mva assoc
233
+ "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
181
234
  end
182
235
 
183
- def convert_nulls(clause)
184
- case @model.connection.class.name
185
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
186
- "IFNULL(#{clause}, '')"
187
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
188
- "COALESCE(#{clause}, '')"
189
- else
190
- clause
191
- end
236
+ def primary_key_for_mva(assoc)
237
+ quote_with_table(
238
+ assoc.table, assoc.primary_key_from_reflection || columns.first.__name
239
+ )
240
+ end
241
+
242
+ def foreign_key_for_mva(assoc)
243
+ quote_with_table assoc.table, assoc.reflection.primary_key_name
244
+ end
245
+
246
+ def association_for_mva
247
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
248
+ assoc.has_column?(columns.first.__name)
249
+ }
250
+ end
251
+
252
+ def adapter
253
+ @adapter ||= @model.sphinx_database_adapter
254
+ end
255
+
256
+ def quote_with_table(table, column)
257
+ "#{quote_table_name(table)}.#{quote_column(column)}"
192
258
  end
193
259
 
194
260
  def quote_column(column)
195
261
  @model.connection.quote_column_name(column)
196
262
  end
197
263
 
264
+ def quote_table_name(table_name)
265
+ @model.connection.quote_table_name(table_name)
266
+ end
267
+
198
268
  # Indication of whether the columns should be concatenated with a space
199
269
  # between each value. True if there's either multiple sources or multiple
200
270
  # associations.
@@ -202,16 +272,7 @@ module ThinkingSphinx
202
272
  def concat_ws?
203
273
  multiple_associations? || @columns.length > 1
204
274
  end
205
-
206
- # Checks the association tree for each column - if they're all the same,
207
- # returns false.
208
- #
209
- def multiple_sources?
210
- first = associations[@columns.first]
211
-
212
- !@columns.all? { |col| associations[col] == first }
213
- end
214
-
275
+
215
276
  # Checks whether any column requires multiple associations (which only
216
277
  # happens for polymorphic situations).
217
278
  #
@@ -231,7 +292,7 @@ module ThinkingSphinx
231
292
  else
232
293
  associations[column].collect { |assoc|
233
294
  assoc.has_column?(column.__name) ?
234
- "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
295
+ "#{quote_table_name(assoc.join.aliased_table_name)}" +
235
296
  ".#{quote_column(column.__name)}" :
236
297
  nil
237
298
  }.compact.join(', ')
@@ -245,31 +306,16 @@ module ThinkingSphinx
245
306
  associations.values.flatten.any? { |assoc| assoc.is_many? }
246
307
  end
247
308
 
309
+ def is_many_ints?
310
+ concat_ws? && all_ints?
311
+ end
312
+
248
313
  # Returns true if any of the columns are string values, instead of database
249
314
  # column references.
250
315
  def is_string?
251
316
  columns.all? { |col| col.is_string? }
252
317
  end
253
318
 
254
- # Returns the type of the column. If that's not already set, it returns
255
- # :multi if there's the possibility of more than one value, :string if
256
- # there's more than one association, otherwise it figures out what the
257
- # actual column's datatype is and returns that.
258
- def type
259
- @type ||= case
260
- when is_many?
261
- :multi
262
- when @associations.values.flatten.length > 1
263
- :string
264
- else
265
- klass = @associations.values.flatten.first ?
266
- @associations.values.flatten.first.reflection.klass : @model
267
- klass.columns.detect { |col|
268
- @columns.collect { |c| c.__name.to_s }.include? col.name
269
- }.type
270
- end
271
- end
272
-
273
319
  def all_ints?
274
320
  @columns.all? { |col|
275
321
  klasses = @associations[col].empty? ? [@model] :
@@ -280,5 +326,33 @@ module ThinkingSphinx
280
326
  }
281
327
  }
282
328
  end
329
+
330
+ def type_from_database
331
+ klass = @associations.values.flatten.first ?
332
+ @associations.values.flatten.first.reflection.klass : @model
333
+
334
+ klass.columns.detect { |col|
335
+ @columns.collect { |c| c.__name.to_s }.include? col.name
336
+ }.type
337
+ end
338
+
339
+ def translated_type_from_database
340
+ case type_from_db = type_from_database
341
+ when :datetime, :string, :float, :boolean, :integer
342
+ type_from_db
343
+ when :decimal
344
+ :float
345
+ when :timestamp, :date
346
+ :datetime
347
+ else
348
+ raise <<-MESSAGE
349
+
350
+ Cannot automatically map column type #{type_from_db} to an equivalent Sphinx
351
+ type (integer, float, boolean, datetime, string as ordinal). You could try to
352
+ explicitly convert the column's value in your define_index block:
353
+ has "CAST(column AS INT)", :type => :integer, :as => :column
354
+ MESSAGE
355
+ end
356
+ end
283
357
  end
284
358
  end