freelancing-god-thinking-sphinx 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -99,3 +99,7 @@ Since I first released this library, there's been quite a few people who have su
99
99
  - Thibaut Barrere
100
100
  - Kristopher Chambers
101
101
  - Dmitrij Smalko
102
+ - Aleksey Yeschenko
103
+ - Lachie Cox
104
+ - Lourens Naude
105
+ - Tom Davies
@@ -6,11 +6,13 @@ require 'active_record'
6
6
  require 'riddle'
7
7
  require 'after_commit'
8
8
 
9
+ require 'thinking_sphinx/core/string'
9
10
  require 'thinking_sphinx/active_record'
10
11
  require 'thinking_sphinx/association'
11
12
  require 'thinking_sphinx/attribute'
12
13
  require 'thinking_sphinx/collection'
13
14
  require 'thinking_sphinx/configuration'
15
+ require 'thinking_sphinx/facet'
14
16
  require 'thinking_sphinx/field'
15
17
  require 'thinking_sphinx/index'
16
18
  require 'thinking_sphinx/rails_additions'
@@ -24,14 +26,14 @@ require 'thinking_sphinx/adapters/postgresql_adapter'
24
26
  ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
25
27
 
26
28
  Merb::Plugins.add_rakefiles(
27
- File.join(File.dirname(__FILE__), "..", "tasks", "thinking_sphinx_tasks")
29
+ File.join(File.dirname(__FILE__), "thinking_sphinx", "tasks")
28
30
  ) if defined?(Merb)
29
31
 
30
32
  module ThinkingSphinx
31
33
  module Version #:nodoc:
32
34
  Major = 1
33
35
  Minor = 1
34
- Tiny = 2
36
+ Tiny = 3
35
37
 
36
38
  String = [Major, Minor, Tiny].join('.')
37
39
  end
@@ -10,7 +10,7 @@ module ThinkingSphinx
10
10
  module ActiveRecord
11
11
  def self.included(base)
12
12
  base.class_eval do
13
- class_inheritable_array :sphinx_indexes
13
+ class_inheritable_array :sphinx_indexes, :sphinx_facets
14
14
  class << self
15
15
  # Allows creation of indexes for Sphinx. If you don't do this, there
16
16
  # isn't much point trying to search (or using this plugin at all,
@@ -94,14 +94,7 @@ module ThinkingSphinx
94
94
  # you in some other way, awesome.
95
95
  #
96
96
  def to_crc32
97
- result = 0xFFFFFFFF
98
- self.name.each_byte do |byte|
99
- result ^= byte
100
- 8.times do
101
- result = (result >> 1) ^ (0xEDB88320 * (result & 1))
102
- end
103
- end
104
- result ^ 0xFFFFFFFF
97
+ self.name.to_crc32
105
98
  end
106
99
 
107
100
  def to_crc32s
@@ -121,13 +114,18 @@ module ThinkingSphinx
121
114
  end
122
115
 
123
116
  def to_riddle(offset)
124
- ThinkingSphinx::AbstractAdapter.detect(self).setup
117
+ sphinx_database_adapter.setup
125
118
 
126
119
  indexes = [to_riddle_for_core(offset)]
127
120
  indexes << to_riddle_for_delta(offset) if sphinx_delta?
128
121
  indexes << to_riddle_for_distributed
129
122
  end
130
123
 
124
+ def sphinx_database_adapter
125
+ @sphinx_database_adapter ||=
126
+ ThinkingSphinx::AbstractAdapter.detect(self)
127
+ end
128
+
131
129
  private
132
130
 
133
131
  def sphinx_name
@@ -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
@@ -1,27 +1,33 @@
1
1
  module ThinkingSphinx
2
2
  class AbstractAdapter
3
- class << self
4
- def setup
5
- # Deliberately blank - subclasses should do something though. Well, if
6
- # they need to.
7
- end
8
-
9
- def detect(model)
10
- case model.connection.class.name
11
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
12
- ThinkingSphinx::MysqlAdapter
13
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
14
- ThinkingSphinx::PostgreSQLAdapter
15
- else
16
- raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
17
- end
18
- end
19
-
20
- protected
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
21
11
 
22
- def connection
23
- @connection ||= ::ActiveRecord::Base.connection
12
+ def self.detect(model)
13
+ case model.connection.class.name
14
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
15
+ ThinkingSphinx::MysqlAdapter.new model
16
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
17
+ ThinkingSphinx::PostgreSQLAdapter.new model
18
+ else
19
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
24
20
  end
25
21
  end
22
+
23
+ def quote_with_table(column)
24
+ "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
25
+ end
26
+
27
+ protected
28
+
29
+ def connection
30
+ @connection ||= @model.connection
31
+ end
26
32
  end
27
33
  end
@@ -1,9 +1,53 @@
1
1
  module ThinkingSphinx
2
2
  class MysqlAdapter < AbstractAdapter
3
- class << self
4
- def setup
5
- # Does MySQL actually need to do anything?
6
- end
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(#{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)
42
+ "CRC32(#{clause})"
43
+ end
44
+
45
+ def utf8_query_pre
46
+ "SET NAMES utf8"
47
+ end
48
+
49
+ def time_difference(diff)
50
+ "DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
7
51
  end
8
52
  end
9
53
  end
@@ -1,83 +1,129 @@
1
1
  module ThinkingSphinx
2
2
  class PostgreSQLAdapter < AbstractAdapter
3
- class << self
4
- def setup
5
- create_array_accum_function
6
- create_crc32_function
7
- end
8
-
9
- private
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(#{field}, '')"
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)
10
36
 
11
- def execute(command, output_error = false)
12
- connection.execute "begin"
13
- connection.execute "savepoint ts"
14
- begin
15
- connection.execute command
16
- rescue StandardError => err
17
- puts err if output_error
18
- connection.execute "rollback to savepoint ts"
19
- end
20
- connection.execute "release savepoint ts"
21
- connection.execute "commit"
37
+ "COALESCE(#{clause}, #{default})"
38
+ end
39
+
40
+ def boolean(value)
41
+ value ? 'TRUE' : 'FALSE'
42
+ end
43
+
44
+ def crc(clause)
45
+ "crc32(#{clause})"
46
+ end
47
+
48
+ def utf8_query_pre
49
+ nil
50
+ end
51
+
52
+ def time_difference(diff)
53
+ "current_timestamp - interval '#{diff} seconds'"
54
+ end
55
+
56
+ private
57
+
58
+ def execute(command, output_error = false)
59
+ connection.execute "begin"
60
+ connection.execute "savepoint ts"
61
+ begin
62
+ connection.execute command
63
+ rescue StandardError => err
64
+ puts err if output_error
65
+ connection.execute "rollback to savepoint ts"
22
66
  end
23
-
24
- def create_array_accum_function
25
- if connection.raw_connection.server_version > 80200
26
- execute <<-SQL
27
- CREATE AGGREGATE array_accum (anyelement)
28
- (
29
- sfunc = array_append,
30
- stype = anyarray,
31
- initcond = '{}'
32
- );
33
- SQL
34
- else
35
- execute <<-SQL
36
- CREATE AGGREGATE array_accum
37
- (
38
- basetype = anyelement,
39
- sfunc = array_append,
40
- stype = anyarray,
41
- initcond = '{}'
42
- );
43
- SQL
44
- end
67
+ connection.execute "release savepoint ts"
68
+ connection.execute "commit"
69
+ end
70
+
71
+ def create_array_accum_function
72
+ if connection.raw_connection.server_version > 80200
73
+ execute <<-SQL
74
+ CREATE AGGREGATE array_accum (anyelement)
75
+ (
76
+ sfunc = array_append,
77
+ stype = anyarray,
78
+ initcond = '{}'
79
+ );
80
+ SQL
81
+ else
82
+ execute <<-SQL
83
+ CREATE AGGREGATE array_accum
84
+ (
85
+ basetype = anyelement,
86
+ sfunc = array_append,
87
+ stype = anyarray,
88
+ initcond = '{}'
89
+ );
90
+ SQL
45
91
  end
46
-
47
- def create_crc32_function
48
- execute "CREATE LANGUAGE 'plpgsql';"
49
- function = <<-SQL
50
- CREATE OR REPLACE FUNCTION crc32(word text)
51
- RETURNS bigint AS $$
52
- DECLARE tmp bigint;
53
- DECLARE i int;
54
- DECLARE j int;
55
- DECLARE word_array bytea;
56
- BEGIN
57
- i = 0;
58
- tmp = 4294967295;
59
- word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
92
+ end
93
+
94
+ def create_crc32_function
95
+ execute "CREATE LANGUAGE 'plpgsql';"
96
+ function = <<-SQL
97
+ CREATE OR REPLACE FUNCTION crc32(word text)
98
+ RETURNS bigint AS $$
99
+ DECLARE tmp bigint;
100
+ DECLARE i int;
101
+ DECLARE j int;
102
+ DECLARE word_array bytea;
103
+ BEGIN
104
+ i = 0;
105
+ tmp = 4294967295;
106
+ word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
107
+ LOOP
108
+ tmp = (tmp # get_byte(word_array, i))::bigint;
109
+ i = i + 1;
110
+ j = 0;
60
111
  LOOP
61
- tmp = (tmp # get_byte(word_array, i))::bigint;
62
- i = i + 1;
63
- j = 0;
64
- LOOP
65
- tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
66
- j = j + 1;
67
- IF j >= 8 THEN
68
- EXIT;
69
- END IF;
70
- END LOOP;
71
- IF i >= char_length(word) THEN
112
+ tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
113
+ j = j + 1;
114
+ IF j >= 8 THEN
72
115
  EXIT;
73
116
  END IF;
74
117
  END LOOP;
75
- return (tmp # 4294967295);
76
- END
77
- $$ IMMUTABLE STRICT LANGUAGE plpgsql;
78
- SQL
79
- execute function, true
80
- end
118
+ IF i >= char_length(word) THEN
119
+ EXIT;
120
+ END IF;
121
+ END LOOP;
122
+ return (tmp # 4294967295);
123
+ END
124
+ $$ IMMUTABLE STRICT LANGUAGE plpgsql;
125
+ SQL
126
+ execute function, true
81
127
  end
82
128
  end
83
129
  end
@@ -9,7 +9,7 @@ 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
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.
@@ -59,8 +59,9 @@ module ThinkingSphinx
59
59
 
60
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) }
61
61
 
62
- @alias = options[:as]
63
- @type = options[:type]
62
+ @alias = options[:as]
63
+ @type = options[:type]
64
+ @faceted = options[:facet]
64
65
  end
65
66
 
66
67
  # Get the part of the SELECT clause related to this attribute. Don't forget
@@ -76,10 +77,10 @@ module ThinkingSphinx
76
77
 
77
78
  separator = all_ints? ? ',' : ' '
78
79
 
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
80
+ clause = adapter.concatenate(clause, separator) if concat_ws?
81
+ clause = adapter.group_concatenate(clause, separator) if is_many?
82
+ clause = adapter.cast_to_datetime(clause) if type == :datetime
83
+ clause = adapter.convert_nulls(clause) if type == :string
83
84
 
84
85
  "#{clause} AS #{quote_column(unique_name)}"
85
86
  end
@@ -133,63 +134,31 @@ module ThinkingSphinx
133
134
  end
134
135
  end
135
136
 
136
- private
137
-
138
- def concatenate(clause, separator = ' ')
139
- case @model.connection.class.name
140
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
141
- "CONCAT_WS('#{separator}', #{clause})"
142
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
143
- clause.split(', ').collect { |attribute|
144
- "COALESCE(#{attribute}, '')"
145
- }.join(" || ' ' || ")
146
- else
147
- clause
148
- end
149
- end
150
-
151
- def group_concatenate(clause, separator = ' ')
152
- case @model.connection.class.name
153
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
154
- "GROUP_CONCAT(#{clause} SEPARATOR '#{separator}')"
155
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
156
- "array_to_string(array_accum(#{clause}), '#{separator}')"
137
+ # Returns the type of the column. If that's not already set, it returns
138
+ # :multi if there's the possibility of more than one value, :string if
139
+ # there's more than one association, otherwise it figures out what the
140
+ # actual column's datatype is and returns that.
141
+ def type
142
+ @type ||= case
143
+ when is_many?
144
+ :multi
145
+ when @associations.values.flatten.length > 1
146
+ :string
157
147
  else
158
- clause
148
+ translated_type_from_database
159
149
  end
160
150
  end
161
151
 
162
- def cast_to_string(clause)
163
- case @model.connection.class.name
164
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
165
- "CAST(#{clause} AS CHAR)"
166
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
167
- clause
168
- else
169
- clause
170
- end
152
+ def to_facet
153
+ return nil unless @faceted
154
+
155
+ ThinkingSphinx::Facet.new(unique_name, @columns, self)
171
156
  end
172
157
 
173
- def cast_to_datetime(clause)
174
- case @model.connection.class.name
175
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
176
- "UNIX_TIMESTAMP(#{clause})"
177
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
178
- "cast(extract(epoch from #{clause}) as int)"
179
- else
180
- clause
181
- end
182
- end
158
+ private
183
159
 
184
- def convert_nulls(clause)
185
- case @model.connection.class.name
186
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
187
- "IFNULL(#{clause}, '')"
188
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
189
- "COALESCE(#{clause}, '')"
190
- else
191
- clause
192
- end
160
+ def adapter
161
+ @adapter ||= @model.sphinx_database_adapter
193
162
  end
194
163
 
195
164
  def quote_column(column)
@@ -203,16 +172,7 @@ module ThinkingSphinx
203
172
  def concat_ws?
204
173
  multiple_associations? || @columns.length > 1
205
174
  end
206
-
207
- # Checks the association tree for each column - if they're all the same,
208
- # returns false.
209
- #
210
- def multiple_sources?
211
- first = associations[@columns.first]
212
-
213
- !@columns.all? { |col| associations[col] == first }
214
- end
215
-
175
+
216
176
  # Checks whether any column requires multiple associations (which only
217
177
  # happens for polymorphic situations).
218
178
  #
@@ -252,21 +212,6 @@ module ThinkingSphinx
252
212
  columns.all? { |col| col.is_string? }
253
213
  end
254
214
 
255
- # Returns the type of the column. If that's not already set, it returns
256
- # :multi if there's the possibility of more than one value, :string if
257
- # there's more than one association, otherwise it figures out what the
258
- # actual column's datatype is and returns that.
259
- def type
260
- @type ||= case
261
- when is_many?
262
- :multi
263
- when @associations.values.flatten.length > 1
264
- :string
265
- else
266
- translated_type_from_database
267
- end
268
- end
269
-
270
215
  def all_ints?
271
216
  @columns.all? { |col|
272
217
  klasses = @associations[col].empty? ? [@model] :