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.
- data/README +64 -2
- data/lib/thinking_sphinx.rb +88 -11
- data/lib/thinking_sphinx/active_record.rb +136 -21
- data/lib/thinking_sphinx/active_record/delta.rb +43 -62
- data/lib/thinking_sphinx/active_record/has_many_association.rb +1 -1
- data/lib/thinking_sphinx/active_record/search.rb +7 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +130 -0
- data/lib/thinking_sphinx/association.rb +17 -0
- data/lib/thinking_sphinx/attribute.rb +171 -97
- data/lib/thinking_sphinx/collection.rb +126 -2
- data/lib/thinking_sphinx/configuration.rb +120 -171
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas.rb +27 -0
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +67 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
- data/lib/thinking_sphinx/facet.rb +58 -0
- data/lib/thinking_sphinx/facet_collection.rb +60 -0
- data/lib/thinking_sphinx/field.rb +18 -52
- data/lib/thinking_sphinx/index.rb +246 -199
- data/lib/thinking_sphinx/index/builder.rb +85 -16
- data/lib/thinking_sphinx/rails_additions.rb +85 -5
- data/lib/thinking_sphinx/search.rb +459 -190
- data/lib/thinking_sphinx/tasks.rb +128 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +53 -124
- data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +2 -2
- data/spec/unit/thinking_sphinx/active_record_spec.rb +110 -30
- data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -149
- data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +54 -412
- data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +0 -79
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +1 -29
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +1 -39
- data/spec/unit/thinking_sphinx/index_spec.rb +78 -226
- data/spec/unit/thinking_sphinx/search_spec.rb +29 -228
- data/spec/unit/thinking_sphinx_spec.rb +23 -19
- data/tasks/distribution.rb +48 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +86 -0
- data/vendor/after_commit/LICENSE +20 -0
- data/vendor/after_commit/README +16 -0
- data/vendor/after_commit/Rakefile +22 -0
- data/vendor/after_commit/init.rb +8 -0
- data/vendor/after_commit/lib/after_commit.rb +45 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
- data/vendor/after_commit/test/after_commit_test.rb +53 -0
- data/vendor/delayed_job/lib/delayed/job.rb +251 -0
- data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
- data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
- data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
- data/{lib → vendor/riddle/lib}/riddle.rb +9 -5
- data/{lib → vendor/riddle/lib}/riddle/client.rb +6 -26
- data/{lib → vendor/riddle/lib}/riddle/client/filter.rb +10 -1
- data/{lib → vendor/riddle/lib}/riddle/client/message.rb +0 -0
- data/{lib → vendor/riddle/lib}/riddle/client/response.rb +0 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
- data/vendor/riddle/lib/riddle/controller.rb +44 -0
- metadata +63 -10
- data/lib/test.rb +0 -46
- data/tasks/thinking_sphinx_tasks.rake +0 -1
- 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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
31
|
-
|
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.
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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.
|
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
|
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
|
# )
|
@@ -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
|
63
|
-
@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
|
-
|
105
|
-
|
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
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
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
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
209
|
+
"query; #{query offset}"
|
147
210
|
end
|
148
211
|
end
|
149
212
|
|
150
|
-
def
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
162
|
-
|
163
|
-
|
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
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
"#{
|
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
|