freelancing-god-thinking-sphinx 1.1.2 → 1.1.3

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 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] :