dpickett-thinking-sphinx 1.1.4 → 1.1.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/README.textile +126 -0
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +14 -1
  4. data/lib/thinking_sphinx/active_record.rb +23 -5
  5. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
  6. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
  7. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +4 -3
  8. data/lib/thinking_sphinx/association.rb +17 -0
  9. data/lib/thinking_sphinx/attribute.rb +106 -95
  10. data/lib/thinking_sphinx/class_facet.rb +0 -5
  11. data/lib/thinking_sphinx/collection.rb +7 -1
  12. data/lib/thinking_sphinx/configuration.rb +9 -4
  13. data/lib/thinking_sphinx/core/string.rb +3 -10
  14. data/lib/thinking_sphinx/deltas/default_delta.rb +8 -5
  15. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  16. data/lib/thinking_sphinx/deltas.rb +7 -2
  17. data/lib/thinking_sphinx/deploy/capistrano.rb +80 -0
  18. data/lib/thinking_sphinx/facet.rb +22 -9
  19. data/lib/thinking_sphinx/facet_collection.rb +27 -12
  20. data/lib/thinking_sphinx/field.rb +4 -96
  21. data/lib/thinking_sphinx/index/builder.rb +46 -15
  22. data/lib/thinking_sphinx/index.rb +58 -66
  23. data/lib/thinking_sphinx/property.rb +133 -0
  24. data/lib/thinking_sphinx/rails_additions.rb +7 -4
  25. data/lib/thinking_sphinx/search.rb +181 -44
  26. data/lib/thinking_sphinx/tasks.rb +4 -4
  27. data/lib/thinking_sphinx.rb +47 -11
  28. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +2 -2
  29. data/spec/unit/thinking_sphinx/active_record_spec.rb +64 -4
  30. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -1
  31. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  32. data/spec/unit/thinking_sphinx/facet_spec.rb +46 -0
  33. data/spec/unit/thinking_sphinx/index_spec.rb +90 -0
  34. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
  35. data/spec/unit/thinking_sphinx/search_spec.rb +44 -0
  36. data/spec/unit/thinking_sphinx_spec.rb +10 -6
  37. data/tasks/distribution.rb +1 -1
  38. data/tasks/testing.rb +7 -15
  39. data/vendor/after_commit/init.rb +3 -0
  40. data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
  41. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
  42. data/vendor/after_commit/lib/after_commit.rb +4 -1
  43. metadata +12 -3
  44. data/README +0 -107
data/README.textile ADDED
@@ -0,0 +1,126 @@
1
+ h1. Thinking Sphinx
2
+
3
+ h2. Usage
4
+
5
+ First, if you haven't done so already, check out the main "usage":http://ts.freelancing-gods.com/usage.html page. Once you've done that, the next place to look for information is the specific method docs - ThinkingSphinx::Search and ThinkingSphinx::Index::Builder in particular.
6
+
7
+ Keep in mind that while Thinking Sphinx works for ActiveRecord with Merb, it doesn't yet support DataMapper (although that is planned).
8
+
9
+ h2. Contributing
10
+
11
+ Fork on GitHub and after you've committed tested patches, send a pull request.
12
+
13
+ To quickly see if your system is ready to run the thinking sphinx specs, run the contribute.rb script found in the project root directory. Use the following instructions to install any missing requirements.
14
+
15
+ To get the spec suite running, you will need to install the not-a-mock gem if you don't already have it:
16
+
17
+ git clone git://github.com/freelancing-god/not-a-mock.git
18
+ cd not-a-mock
19
+ rake gem
20
+ gem install pkg/not_a_mock-1.1.0.gem
21
+
22
+ Then install the ginger gem. The steps are the same, except that you might need to sudo the gem install:
23
+
24
+ git clone git://github.com/freelancing-god/ginger.git
25
+ cd ginger
26
+ rake gem
27
+ sudo gem install pkg/ginger-1.1.0.gem
28
+
29
+ Then set up your database:
30
+
31
+ cp spec/fixtures/database.yml.default spec/fixtures/database.yml
32
+ mysqladmin -u root create thinking_sphinx
33
+
34
+ Make sure you don't have another Sphinx daemon (searchd) running. If you do, quit it with "rake ts:stop"
35
+ in the app root.
36
+
37
+ You should now have a passing test suite from which to build your patch on.
38
+
39
+ rake spec
40
+
41
+ If you get the message "Failed to start searchd daemon", run the spec with sudo:
42
+
43
+ sudo rake spec
44
+
45
+ If you quit the spec suite before it's completed, you may be left with data in the test
46
+ database, causing the next run to have failures. Let that run complete and then try again.
47
+
48
+ h2. Contributors
49
+
50
+ Since I first released this library, there's been quite a few people who have submitted patches, to my immense gratitude. Others have suggested syntax changes and general improvements. So my thanks to the following people:
51
+
52
+ * Joost Hietbrink
53
+ * Jonathan Conway
54
+ * Gregory Mirzayantz
55
+ * Tung Nguyen
56
+ * Sean Cribbs
57
+ * Benoit Caccinolo
58
+ * John Barton
59
+ * Oliver Beddows
60
+ * Arthur Zapparoli
61
+ * Dusty Doris
62
+ * Marcus Crafter
63
+ * Patrick Lenz
64
+ * Björn Andreasson
65
+ * James Healy
66
+ * Jae-Jun Hwang
67
+ * Xavier Shay
68
+ * Jason Rust
69
+ * Gopal Patel
70
+ * Chris Heald
71
+ * Peter Vandenberk
72
+ * Josh French
73
+ * Andrew Bennett
74
+ * Jordan Fowler
75
+ * Seth Walker
76
+ * Joe Noon
77
+ * Wolfgang Postler
78
+ * Rick Olson
79
+ * Killian Murphy
80
+ * Morten Primdahl
81
+ * Ryan Bates
82
+ * David Eisinger
83
+ * Shay Arnett
84
+ * Minh Tran
85
+ * Jeremy Durham
86
+ * Piotr Sarnacki
87
+ * Matt Johnson
88
+ * Nicolas Blanco
89
+ * Max Lapshin
90
+ * Josh Natanson
91
+ * Philip Hallstrom
92
+ * Christian Rishøj
93
+ * Mike Flester
94
+ * Jim Remsik
95
+ * Kennon Ballou
96
+ * Henrik Nyh
97
+ * Emil Tin
98
+ * Doug Cole
99
+ * Ed Hickey
100
+ * Evan Weaver
101
+ * Thibaut Barrere
102
+ * Kristopher Chambers
103
+ * Dmitrij Smalko
104
+ * Aleksey Yeschenko
105
+ * Lachie Cox
106
+ * Lourens Naude
107
+ * Tom Davies
108
+ * Dan Pickett
109
+ * Alex Caudill
110
+ * Jim Benton
111
+ * John Aughey
112
+ * Keith Pitty
113
+ * Jeff Talbot
114
+ * Dana Contreras
115
+ * Menno van der Sman
116
+ * Bill Harding
117
+ * Isaac Feliu
118
+ * Andrei Bocan
119
+ * László Bácsi
120
+ * Peter Wagenet
121
+ * Max Lapshin
122
+ * Martin Emde
123
+ * David Wennergren
124
+ * Mark Lane
125
+ * Eric Lindvall
126
+ * Lawrence Pit
@@ -0,0 +1,48 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module AttributeUpdates
4
+ def self.included(base)
5
+ base.class_eval do
6
+ after_commit :update_attribute_values
7
+ end
8
+ end
9
+
10
+ private
11
+
12
+ def update_attribute_values
13
+ return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running?
14
+
15
+ config = ThinkingSphinx::Configuration.instance
16
+ client = Riddle::Client.new config.address, config.port
17
+
18
+ self.sphinx_indexes.each do |index|
19
+ attribute_pairs = attribute_values_for_index(index)
20
+ attribute_names = attribute_pairs.keys
21
+ attribute_values = attribute_names.collect { |key|
22
+ attribute_pairs[key]
23
+ }
24
+
25
+ client.update "#{index.name}_core", attribute_names, {
26
+ sphinx_document_id => attribute_values
27
+ } if in_core_index?
28
+ end
29
+ end
30
+
31
+ def updatable_attributes(index)
32
+ index.attributes.select { |attrib| attrib.updatable? }
33
+ end
34
+
35
+ def attribute_values_for_index(index)
36
+ updatable_attributes(index).inject({}) { |hash, attrib|
37
+ if attrib.type == :datetime
38
+ hash[attrib.unique_name.to_s] = attrib.live_value(self).to_time.to_i
39
+ else
40
+ hash[attrib.unique_name.to_s] = attrib.live_value self
41
+ end
42
+
43
+ hash
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -50,6 +50,10 @@ module ThinkingSphinx
50
50
  end
51
51
  end
52
52
 
53
+ def toggled_delta?
54
+ self.class.delta_object.toggled(self)
55
+ end
56
+
53
57
  private
54
58
 
55
59
  # Set the delta value for the model to be true.
@@ -65,7 +69,16 @@ module ThinkingSphinx
65
69
  end
66
70
 
67
71
  def should_toggle_delta?
68
- !self.respond_to?(:changed?) || self.changed? || self.new_record?
72
+ self.new_record? || indexed_data_changed?
73
+ end
74
+
75
+ def indexed_data_changed?
76
+ sphinx_indexes.any? { |index|
77
+ index.fields.any? { |field| field.changed?(self) } ||
78
+ index.attributes.any? { |attrib|
79
+ attrib.public? && attrib.changed?(self) && !attrib.updatable?
80
+ }
81
+ }
69
82
  end
70
83
  end
71
84
  end
@@ -1,3 +1,4 @@
1
+ require 'thinking_sphinx/active_record/attribute_updates'
1
2
  require 'thinking_sphinx/active_record/delta'
2
3
  require 'thinking_sphinx/active_record/search'
3
4
  require 'thinking_sphinx/active_record/has_many_association'
@@ -79,6 +80,8 @@ module ThinkingSphinx
79
80
 
80
81
  after_destroy :toggle_deleted
81
82
 
83
+ include ThinkingSphinx::ActiveRecord::AttributeUpdates
84
+
82
85
  index
83
86
  end
84
87
  alias_method :sphinx_index, :define_index
@@ -207,11 +210,20 @@ module ThinkingSphinx
207
210
  )
208
211
  end
209
212
 
213
+ def in_index?(suffix)
214
+ self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
215
+ end
216
+
210
217
  def in_core_index?
211
- self.class.search_for_id(
212
- self.sphinx_document_id,
213
- "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
214
- )
218
+ in_index? "core"
219
+ end
220
+
221
+ def in_delta_index?
222
+ in_index? "delta"
223
+ end
224
+
225
+ def in_both_indexes?
226
+ in_core_index? && in_delta_index?
215
227
  end
216
228
 
217
229
  def toggle_deleted
@@ -232,7 +244,7 @@ module ThinkingSphinx
232
244
  {self.sphinx_document_id => 1}
233
245
  ) if ThinkingSphinx.deltas_enabled? &&
234
246
  self.class.sphinx_indexes.any? { |index| index.delta? } &&
235
- self.delta
247
+ self.toggled_delta?
236
248
  rescue ::ThinkingSphinx::ConnectionError
237
249
  # nothing
238
250
  end
@@ -241,5 +253,11 @@ module ThinkingSphinx
241
253
  (self.id * ThinkingSphinx.indexed_models.size) +
242
254
  ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name)
243
255
  end
256
+
257
+ private
258
+
259
+ def sphinx_index_name(suffix)
260
+ "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
261
+ end
244
262
  end
245
263
  end
@@ -16,8 +16,16 @@ module ThinkingSphinx
16
16
  ThinkingSphinx::MysqlAdapter.new model
17
17
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
18
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
19
27
  else
20
- raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
28
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
21
29
  end
22
30
  end
23
31
 
@@ -13,7 +13,7 @@ module ThinkingSphinx
13
13
  end
14
14
 
15
15
  def group_concatenate(clause, separator = ' ')
16
- "GROUP_CONCAT(#{clause} SEPARATOR '#{separator}')"
16
+ "GROUP_CONCAT(DISTINCT #{clause} SEPARATOR '#{separator}')"
17
17
  end
18
18
 
19
19
  def cast_to_string(clause)
@@ -38,7 +38,8 @@ module ThinkingSphinx
38
38
  value ? 1 : 0
39
39
  end
40
40
 
41
- def crc(clause)
41
+ def crc(clause, blank_to_null = false)
42
+ clause = "NULLIF(#{clause},'')" if blank_to_null
42
43
  "CRC32(#{clause})"
43
44
  end
44
45
 
@@ -11,7 +11,7 @@ module ThinkingSphinx
11
11
 
12
12
  def concatenate(clause, separator = ' ')
13
13
  clause.split(', ').collect { |field|
14
- "COALESCE(#{field}, '')"
14
+ "COALESCE(CAST(#{field} as varchar), '')"
15
15
  }.join(" || '#{separator}' || ")
16
16
  end
17
17
 
@@ -41,7 +41,8 @@ module ThinkingSphinx
41
41
  value ? 'TRUE' : 'FALSE'
42
42
  end
43
43
 
44
- def crc(clause)
44
+ def crc(clause, blank_to_null = false)
45
+ clause = "NULLIF(#{clause},'')" if blank_to_null
45
46
  "crc32(#{clause})"
46
47
  end
47
48
 
@@ -69,7 +70,7 @@ module ThinkingSphinx
69
70
  end
70
71
 
71
72
  def create_array_accum_function
72
- if connection.raw_connection.server_version > 80200
73
+ if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
73
74
  execute <<-SQL
74
75
  CREATE AGGREGATE array_accum (anyelement)
75
76
  (
@@ -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
@@ -8,15 +8,16 @@ module ThinkingSphinx
8
8
  # generate SQL statements, you'll need to set the base model, and all the
9
9
  # associations. Which can get messy. Use Index.link!, it really helps.
10
10
  #
11
- class Attribute
12
- attr_accessor :alias, :columns, :associations, :model, :faceted
11
+ class Attribute < ThinkingSphinx::Property
12
+ attr_accessor :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
  # )
@@ -54,14 +68,14 @@ module ThinkingSphinx
54
68
  # that Sphinx expects these values to be in radians.
55
69
  #
56
70
  def initialize(columns, options = {})
57
- @columns = Array(columns)
58
- @associations = {}
59
-
60
- 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) }
71
+ super
61
72
 
62
- @alias = options[:as]
63
73
  @type = options[:type]
64
- @faceted = options[:facet]
74
+ @source = options[:source]
75
+ @crc = options[:crc]
76
+
77
+ @type ||= :multi unless @source.nil?
78
+ @type = :integer if @type == :string && @crc
65
79
  end
66
80
 
67
81
  # Get the part of the SELECT clause related to this attribute. Don't forget
@@ -71,6 +85,8 @@ module ThinkingSphinx
71
85
  # datetimes to timestamps, as needed.
72
86
  #
73
87
  def to_select_sql
88
+ return nil unless include_as_association?
89
+
74
90
  clause = @columns.collect { |column|
75
91
  column_with_prefix(column)
76
92
  }.join(', ')
@@ -81,27 +97,11 @@ module ThinkingSphinx
81
97
  clause = adapter.group_concatenate(clause, separator) if is_many?
82
98
  clause = adapter.cast_to_datetime(clause) if type == :datetime
83
99
  clause = adapter.convert_nulls(clause) if type == :string
100
+ clause = adapter.crc(clause) if @crc
84
101
 
85
102
  "#{clause} AS #{quote_column(unique_name)}"
86
103
  end
87
104
 
88
- # Get the part of the GROUP BY clause related to this attribute - if one is
89
- # needed. If not, all you'll get back is nil. The latter will happen if
90
- # there isn't actually a real column to get data from, or if there's
91
- # multiple data values (read: a has_many or has_and_belongs_to_many
92
- # association).
93
- #
94
- def to_group_sql
95
- case
96
- when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
97
- nil
98
- else
99
- @columns.collect { |column|
100
- column_with_prefix(column)
101
- }
102
- end
103
- end
104
-
105
105
  def type_to_config
106
106
  {
107
107
  :multi => :sql_attr_multi,
@@ -113,105 +113,116 @@ module ThinkingSphinx
113
113
  }[type]
114
114
  end
115
115
 
116
- def config_value
117
- if type == :multi
118
- "uint #{unique_name} from field"
119
- else
120
- unique_name
121
- end
116
+ def include_as_association?
117
+ ! (type == :multi && (source == :query || source == :ranged_query))
122
118
  end
123
119
 
124
- # Returns the unique name of the attribute - which is either the alias of
125
- # the attribute, or the name of the only column - if there is only one. If
126
- # there isn't, there should be an alias. Else things probably won't work.
127
- # Consider yourself warned.
120
+ # Returns the configuration value that should be used for
121
+ # the attribute.
122
+ # Special case is the multi-valued attribute that needs some
123
+ # extra configuration.
128
124
  #
129
- def unique_name
130
- if @columns.length == 1
131
- @alias || @columns.first.__name
125
+ def config_value(offset = nil)
126
+ if type == :multi
127
+ multi_config = include_as_association? ? "field" :
128
+ source_value(offset).gsub(/\n\s*/, " ")
129
+ "uint #{unique_name} from #{multi_config}"
132
130
  else
133
- @alias
131
+ unique_name
134
132
  end
135
133
  end
136
-
134
+
137
135
  # Returns the type of the column. If that's not already set, it returns
138
136
  # :multi if there's the possibility of more than one value, :string if
139
137
  # there's more than one association, otherwise it figures out what the
140
138
  # actual column's datatype is and returns that.
139
+ #
141
140
  def type
142
- @type ||= case
143
- when is_many?
144
- :multi
145
- when @associations.values.flatten.length > 1
146
- :string
147
- else
148
- translated_type_from_database
141
+ @type ||= begin
142
+ base_type = case
143
+ when is_many?, is_many_ints?
144
+ :multi
145
+ when @associations.values.flatten.length > 1
146
+ :string
147
+ else
148
+ translated_type_from_database
149
+ end
150
+
151
+ if base_type == :string && @crc
152
+ :integer
153
+ else
154
+ @crc = false
155
+ base_type
156
+ end
149
157
  end
150
158
  end
151
159
 
152
- def to_facet
153
- return nil unless @faceted
154
-
155
- ThinkingSphinx::Facet.new(self)
160
+ def updatable?
161
+ [:integer, :datetime, :boolean].include?(type) && !is_string?
162
+ end
163
+
164
+ def live_value(instance)
165
+ object = instance
166
+ column = @columns.first
167
+ column.__stack.each { |method| object = object.send(method) }
168
+ object.send(column.__name)
156
169
  end
157
170
 
158
171
  private
159
172
 
160
- def adapter
161
- @adapter ||= @model.sphinx_database_adapter
173
+ def source_value(offset)
174
+ if is_string?
175
+ "#{source.to_s.dasherize}; #{columns.first.__name}"
176
+ elsif source == :ranged_query
177
+ "ranged-query; #{query offset} #{query_clause}; #{range_query}"
178
+ else
179
+ "query; #{query offset}"
180
+ end
162
181
  end
163
182
 
164
- def quote_column(column)
165
- @model.connection.quote_column_name(column)
183
+ def query(offset)
184
+ assoc = association_for_mva
185
+ raise "Could not determine SQL for MVA" if assoc.nil?
186
+
187
+ <<-SQL
188
+ SELECT #{foreign_key_for_mva assoc}
189
+ #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')},
190
+ #{primary_key_for_mva(assoc)} AS #{quote_column(unique_name)}
191
+ FROM #{quote_table_name assoc.table}
192
+ SQL
166
193
  end
167
194
 
168
- # Indication of whether the columns should be concatenated with a space
169
- # between each value. True if there's either multiple sources or multiple
170
- # associations.
171
- #
172
- def concat_ws?
173
- multiple_associations? || @columns.length > 1
195
+ def query_clause
196
+ foreign_key = foreign_key_for_mva association_for_mva
197
+ "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
174
198
  end
175
-
176
- # Checks whether any column requires multiple associations (which only
177
- # happens for polymorphic situations).
178
- #
179
- def multiple_associations?
180
- associations.any? { |col,assocs| assocs.length > 1 }
199
+
200
+ def range_query
201
+ assoc = association_for_mva
202
+ foreign_key = foreign_key_for_mva assoc
203
+ "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
181
204
  end
182
205
 
183
- # Builds a column reference tied to the appropriate associations. This
184
- # dives into the associations hash and their corresponding joins to
185
- # figure out how to correctly reference a column in SQL.
186
- #
187
- def column_with_prefix(column)
188
- if column.is_string?
189
- column.__name
190
- elsif associations[column].empty?
191
- "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
192
- else
193
- associations[column].collect { |assoc|
194
- assoc.has_column?(column.__name) ?
195
- "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
196
- ".#{quote_column(column.__name)}" :
197
- nil
198
- }.compact.join(', ')
199
- end
206
+ def primary_key_for_mva(assoc)
207
+ quote_with_table(
208
+ assoc.table, assoc.primary_key_from_reflection || columns.first.__name
209
+ )
200
210
  end
201
211
 
202
- # Could there be more than one value related to the parent record? If so,
203
- # then this will return true. If not, false. It's that simple.
204
- #
205
- def is_many?
206
- associations.values.flatten.any? { |assoc| assoc.is_many? }
212
+ def foreign_key_for_mva(assoc)
213
+ quote_with_table assoc.table, assoc.reflection.primary_key_name
207
214
  end
208
215
 
209
- # Returns true if any of the columns are string values, instead of database
210
- # column references.
211
- def is_string?
212
- columns.all? { |col| col.is_string? }
216
+ def association_for_mva
217
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
218
+ assoc.has_column?(columns.first.__name)
219
+ }
213
220
  end
214
221
 
222
+ def is_many_ints?
223
+ concat_ws? && all_ints?
224
+ end
225
+
215
226
  def all_ints?
216
227
  @columns.all? { |col|
217
228
  klasses = @associations[col].empty? ? [@model] :
@@ -10,11 +10,6 @@ module ThinkingSphinx
10
10
 
11
11
  def value(object, attribute_value)
12
12
  object.class.name
13
- # ThinkingSphinx.indexed_models.each do |i|
14
- # return i if i.to_crc32 == attribute_value
15
- # end
16
- #
17
- # raise "Unknown class crc"
18
13
  end
19
14
  end
20
15
  end
@@ -46,9 +46,11 @@ module ThinkingSphinx
46
46
  ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
47
47
  instances = ids.length > 0 ? klass.find(
48
48
  :all,
49
+ :joins => options[:joins],
49
50
  :conditions => {klass.primary_key.to_sym => ids},
50
51
  :include => (options[:include] || index_options[:include]),
51
- :select => (options[:select] || index_options[:select])
52
+ :select => (options[:select] || index_options[:select]),
53
+ :order => (options[:sql_order] || index_options[:sql_order])
52
54
  ) : []
53
55
 
54
56
  # Raise an exception if we find records in Sphinx but not in the DB, so
@@ -59,6 +61,10 @@ module ThinkingSphinx
59
61
  raise StaleIdsException, stale_ids
60
62
  end
61
63
 
64
+ # if the user has specified an SQL order, return the collection
65
+ # without rearranging it into the Sphinx order
66
+ return instances if options[:sql_order]
67
+
62
68
  ids.collect { |obj_id|
63
69
  instances.detect { |obj| obj.id == obj_id }
64
70
  }