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.
- data/README.textile +126 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
- data/lib/thinking_sphinx/active_record/delta.rb +14 -1
- data/lib/thinking_sphinx/active_record.rb +23 -5
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +4 -3
- data/lib/thinking_sphinx/association.rb +17 -0
- data/lib/thinking_sphinx/attribute.rb +106 -95
- data/lib/thinking_sphinx/class_facet.rb +0 -5
- data/lib/thinking_sphinx/collection.rb +7 -1
- data/lib/thinking_sphinx/configuration.rb +9 -4
- data/lib/thinking_sphinx/core/string.rb +3 -10
- data/lib/thinking_sphinx/deltas/default_delta.rb +8 -5
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
- data/lib/thinking_sphinx/deltas.rb +7 -2
- data/lib/thinking_sphinx/deploy/capistrano.rb +80 -0
- data/lib/thinking_sphinx/facet.rb +22 -9
- data/lib/thinking_sphinx/facet_collection.rb +27 -12
- data/lib/thinking_sphinx/field.rb +4 -96
- data/lib/thinking_sphinx/index/builder.rb +46 -15
- data/lib/thinking_sphinx/index.rb +58 -66
- data/lib/thinking_sphinx/property.rb +133 -0
- data/lib/thinking_sphinx/rails_additions.rb +7 -4
- data/lib/thinking_sphinx/search.rb +181 -44
- data/lib/thinking_sphinx/tasks.rb +4 -4
- data/lib/thinking_sphinx.rb +47 -11
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +2 -2
- data/spec/unit/thinking_sphinx/active_record_spec.rb +64 -4
- data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -1
- data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
- data/spec/unit/thinking_sphinx/facet_spec.rb +46 -0
- data/spec/unit/thinking_sphinx/index_spec.rb +90 -0
- data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +44 -0
- data/spec/unit/thinking_sphinx_spec.rb +10 -6
- data/tasks/distribution.rb +1 -1
- data/tasks/testing.rb +7 -15
- data/vendor/after_commit/init.rb +3 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
- data/vendor/after_commit/lib/after_commit.rb +4 -1
- metadata +12 -3
- 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
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
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.
|
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 :
|
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
|
19
|
-
# - :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
|
-
|
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
|
-
@
|
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
|
117
|
-
|
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
|
125
|
-
# the attribute
|
126
|
-
#
|
127
|
-
#
|
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
|
130
|
-
if
|
131
|
-
|
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
|
-
|
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 ||=
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
161
|
-
|
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
|
165
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
203
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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] :
|
@@ -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
|
}
|