DrMark-thinking-sphinx 1.1.6 → 1.1.14
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 → README.textile} +84 -84
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
- data/lib/thinking_sphinx/active_record/delta.rb +10 -1
- data/lib/thinking_sphinx/active_record.rb +10 -3
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +1 -1
- data/lib/thinking_sphinx/attribute.rb +44 -134
- data/lib/thinking_sphinx/class_facet.rb +15 -0
- data/lib/thinking_sphinx/collection.rb +1 -0
- data/lib/thinking_sphinx/configuration.rb +7 -3
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +1 -1
- data/lib/thinking_sphinx/deltas/default_delta.rb +3 -2
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
- data/lib/thinking_sphinx/deltas.rb +9 -6
- data/lib/thinking_sphinx/deploy/capistrano.rb +82 -0
- data/lib/thinking_sphinx/facet.rb +68 -18
- data/lib/thinking_sphinx/facet_collection.rb +16 -17
- data/lib/thinking_sphinx/field.rb +7 -97
- data/lib/thinking_sphinx/index/builder.rb +255 -232
- data/lib/thinking_sphinx/index.rb +37 -349
- data/lib/thinking_sphinx/property.rb +160 -0
- data/lib/thinking_sphinx/search/facets.rb +98 -0
- data/lib/thinking_sphinx/search.rb +4 -73
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +124 -0
- data/lib/thinking_sphinx/source.rb +150 -0
- data/lib/thinking_sphinx/tasks.rb +1 -1
- data/lib/thinking_sphinx.rb +3 -1
- data/spec/unit/thinking_sphinx/active_record_spec.rb +14 -12
- data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -11
- data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
- data/spec/unit/thinking_sphinx/facet_spec.rb +278 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +18 -9
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +347 -1
- data/spec/unit/thinking_sphinx/index_spec.rb +22 -27
- data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +71 -0
- data/spec/unit/thinking_sphinx/source_spec.rb +156 -0
- data/tasks/distribution.rb +1 -1
- data/tasks/testing.rb +7 -15
- metadata +19 -3
@@ -9,8 +9,7 @@ module ThinkingSphinx
|
|
9
9
|
# Enjoy.
|
10
10
|
#
|
11
11
|
class Index
|
12
|
-
attr_accessor :model, :
|
13
|
-
:delta_object, :options
|
12
|
+
attr_accessor :model, :sources, :delta_object
|
14
13
|
|
15
14
|
# Create a new index instance by passing in the model it is tied to, and
|
16
15
|
# a block to build it with (optional but recommended). For documentation
|
@@ -28,153 +27,11 @@ module ThinkingSphinx
|
|
28
27
|
#
|
29
28
|
def initialize(model, &block)
|
30
29
|
@model = model
|
31
|
-
@
|
32
|
-
@fields = []
|
33
|
-
@attributes = []
|
34
|
-
@conditions = []
|
35
|
-
@groupings = []
|
30
|
+
@sources = []
|
36
31
|
@options = {}
|
37
32
|
@delta_object = nil
|
38
33
|
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
def name
|
43
|
-
self.class.name(@model)
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.name(model)
|
47
|
-
model.name.underscore.tr(':/\\', '_')
|
48
|
-
end
|
49
|
-
|
50
|
-
def to_riddle_for_core(offset, index)
|
51
|
-
add_internal_attributes_and_facets
|
52
|
-
link!
|
53
|
-
|
54
|
-
source = Riddle::Configuration::SQLSource.new(
|
55
|
-
"#{name}_core_#{index}", adapter.sphinx_identifier
|
56
|
-
)
|
57
|
-
|
58
|
-
set_source_database_settings source
|
59
|
-
set_source_attributes source, offset
|
60
|
-
set_source_sql source, offset
|
61
|
-
set_source_settings source
|
62
|
-
|
63
|
-
source
|
64
|
-
end
|
65
|
-
|
66
|
-
def to_riddle_for_delta(offset, index)
|
67
|
-
add_internal_attributes_and_facets
|
68
|
-
link!
|
69
|
-
|
70
|
-
source = Riddle::Configuration::SQLSource.new(
|
71
|
-
"#{name}_delta_#{index}", adapter.sphinx_identifier
|
72
|
-
)
|
73
|
-
source.parent = "#{name}_core_#{index}"
|
74
|
-
|
75
|
-
set_source_database_settings source
|
76
|
-
set_source_attributes source, offset
|
77
|
-
set_source_sql source, offset, true
|
78
|
-
|
79
|
-
source
|
80
|
-
end
|
81
|
-
|
82
|
-
# Link all the fields and associations to their corresponding
|
83
|
-
# associations and joins. This _must_ be called before interrogating
|
84
|
-
# the index's fields and associations for anything that may reference
|
85
|
-
# their SQL structure.
|
86
|
-
#
|
87
|
-
def link!
|
88
|
-
base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
|
89
|
-
@model, [], nil
|
90
|
-
)
|
91
|
-
|
92
|
-
@fields.each { |field|
|
93
|
-
field.model ||= @model
|
94
|
-
field.columns.each { |col|
|
95
|
-
field.associations[col] = associations(col.__stack.clone)
|
96
|
-
field.associations[col].each { |assoc| assoc.join_to(base) }
|
97
|
-
}
|
98
|
-
}
|
99
|
-
|
100
|
-
@attributes.each { |attribute|
|
101
|
-
attribute.model ||= @model
|
102
|
-
attribute.columns.each { |col|
|
103
|
-
attribute.associations[col] = associations(col.__stack.clone)
|
104
|
-
attribute.associations[col].each { |assoc| assoc.join_to(base) }
|
105
|
-
}
|
106
|
-
}
|
107
|
-
end
|
108
|
-
|
109
|
-
# Flag to indicate whether this index has a corresponding delta index.
|
110
|
-
#
|
111
|
-
def delta?
|
112
|
-
!@delta_object.nil?
|
113
|
-
end
|
114
|
-
|
115
|
-
def adapter
|
116
|
-
@adapter ||= @model.sphinx_database_adapter
|
117
|
-
end
|
118
|
-
|
119
|
-
def prefix_fields
|
120
|
-
@fields.select { |field| field.prefixes }
|
121
|
-
end
|
122
|
-
|
123
|
-
def infix_fields
|
124
|
-
@fields.select { |field| field.infixes }
|
125
|
-
end
|
126
|
-
|
127
|
-
def index_options
|
128
|
-
all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone
|
129
|
-
@options.keys.select { |key|
|
130
|
-
ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
|
131
|
-
}.each { |key| all_index_options[key.to_sym] = @options[key] }
|
132
|
-
all_index_options
|
133
|
-
end
|
134
|
-
|
135
|
-
def quote_column(column)
|
136
|
-
@model.connection.quote_column_name(column)
|
137
|
-
end
|
138
|
-
|
139
|
-
private
|
140
|
-
|
141
|
-
def utf8?
|
142
|
-
self.index_options[:charset_type] == "utf-8"
|
143
|
-
end
|
144
|
-
|
145
|
-
# Does all the magic with the block provided to the base #initialize.
|
146
|
-
# Creates a new class subclassed from Builder, and evaluates the block
|
147
|
-
# on it, then pulls all relevant settings - fields, attributes, conditions,
|
148
|
-
# properties - into the new index.
|
149
|
-
#
|
150
|
-
# Also creates a CRC attribute for the model.
|
151
|
-
#
|
152
|
-
def initialize_from_builder(&block)
|
153
|
-
builder = Class.new(Builder)
|
154
|
-
builder.setup
|
155
|
-
|
156
|
-
builder.instance_eval &block
|
157
|
-
|
158
|
-
unless @model.descends_from_active_record?
|
159
|
-
stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize
|
160
|
-
builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'")
|
161
|
-
end
|
162
|
-
|
163
|
-
set_model = Proc.new { |item| item.model = @model }
|
164
|
-
|
165
|
-
@fields = builder.fields &set_model
|
166
|
-
@attributes = builder.attributes.each &set_model
|
167
|
-
@conditions = builder.conditions
|
168
|
-
@groupings = builder.groupings
|
169
|
-
@delta_object = ThinkingSphinx::Deltas.parse self, builder.properties
|
170
|
-
@options = builder.properties
|
171
|
-
|
172
|
-
is_faceted = Proc.new { |item| item.faceted }
|
173
|
-
add_facet = Proc.new { |item| @model.sphinx_facets << item.to_facet }
|
174
|
-
|
175
|
-
@model.sphinx_facets ||= []
|
176
|
-
@fields.select( &is_faceted).each &add_facet
|
177
|
-
@attributes.select(&is_faceted).each &add_facet
|
34
|
+
# add_internal_attributes_and_facets
|
178
35
|
|
179
36
|
# We want to make sure that if the database doesn't exist, then Thinking
|
180
37
|
# Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
|
@@ -188,236 +45,67 @@ module ThinkingSphinx
|
|
188
45
|
end
|
189
46
|
end
|
190
47
|
|
191
|
-
|
192
|
-
|
193
|
-
# columns are from.
|
194
|
-
#
|
195
|
-
def all_associations
|
196
|
-
@all_associations ||= (
|
197
|
-
# field associations
|
198
|
-
@fields.collect { |field|
|
199
|
-
field.associations.values
|
200
|
-
}.flatten +
|
201
|
-
# attribute associations
|
202
|
-
@attributes.collect { |attrib|
|
203
|
-
attrib.associations.values if attrib.include_as_association?
|
204
|
-
}.compact.flatten
|
205
|
-
).uniq.collect { |assoc|
|
206
|
-
# get ancestors as well as column-level associations
|
207
|
-
assoc.ancestors
|
208
|
-
}.flatten.uniq
|
48
|
+
def fields
|
49
|
+
@sources.collect { |source| source.fields }.flatten
|
209
50
|
end
|
210
51
|
|
211
|
-
|
212
|
-
|
213
|
-
def associations(path, parent = nil)
|
214
|
-
assocs = []
|
215
|
-
|
216
|
-
if parent.nil?
|
217
|
-
assocs = association(path.shift)
|
218
|
-
else
|
219
|
-
assocs = parent.children(path.shift)
|
220
|
-
end
|
221
|
-
|
222
|
-
until path.empty?
|
223
|
-
point = path.shift
|
224
|
-
assocs = assocs.collect { |assoc|
|
225
|
-
assoc.children(point)
|
226
|
-
}.flatten
|
227
|
-
end
|
228
|
-
|
229
|
-
assocs
|
52
|
+
def attributes
|
53
|
+
@sources.collect { |source| source.attributes }.flatten
|
230
54
|
end
|
231
55
|
|
232
|
-
|
233
|
-
|
234
|
-
def association(key)
|
235
|
-
@associations[key] ||= Association.children(@model, key)
|
236
|
-
end
|
237
|
-
|
238
|
-
def crc_column
|
239
|
-
if @model.column_names.include?(@model.inheritance_column)
|
240
|
-
adapter.cast_to_unsigned(adapter.convert_nulls(
|
241
|
-
adapter.crc(adapter.quote_with_table(@model.inheritance_column), true),
|
242
|
-
@model.to_crc32
|
243
|
-
))
|
244
|
-
else
|
245
|
-
@model.to_crc32.to_s
|
246
|
-
end
|
56
|
+
def name
|
57
|
+
self.class.name(@model)
|
247
58
|
end
|
248
59
|
|
249
|
-
def
|
250
|
-
|
251
|
-
add_internal_attribute :class_crc, :integer, crc_column, true
|
252
|
-
add_internal_attribute :subclass_crcs, :multi, subclasses_to_s
|
253
|
-
add_internal_attribute :sphinx_deleted, :integer, "0"
|
254
|
-
|
255
|
-
add_internal_facet :class_crc
|
60
|
+
def self.name(model)
|
61
|
+
model.name.underscore.tr(':/\\', '_')
|
256
62
|
end
|
257
63
|
|
258
|
-
def
|
259
|
-
|
260
|
-
|
261
|
-
@attributes << Attribute.new(
|
262
|
-
FauxColumn.new(contents),
|
263
|
-
:type => type,
|
264
|
-
:as => name,
|
265
|
-
:facet => facet
|
266
|
-
)
|
64
|
+
def prefix_fields
|
65
|
+
fields.select { |field| field.prefixes }
|
267
66
|
end
|
268
67
|
|
269
|
-
def
|
270
|
-
|
271
|
-
|
272
|
-
@model.sphinx_facets << ClassFacet.new(attribute_by_alias(name))
|
68
|
+
def infix_fields
|
69
|
+
fields.select { |field| field.infixes }
|
273
70
|
end
|
274
71
|
|
275
|
-
def
|
276
|
-
@
|
72
|
+
def local_options
|
73
|
+
@options
|
277
74
|
end
|
278
75
|
|
279
|
-
def
|
280
|
-
|
76
|
+
def options
|
77
|
+
all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone
|
78
|
+
@options.keys.select { |key|
|
79
|
+
ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
|
80
|
+
}.each { |key| all_index_options[key.to_sym] = @options[key] }
|
81
|
+
all_index_options
|
281
82
|
end
|
282
83
|
|
283
|
-
def
|
284
|
-
|
285
|
-
klass.to_crc32.to_s
|
286
|
-
} << @model.to_crc32.to_s).join(",") + "'"
|
287
|
-
end
|
288
|
-
|
289
|
-
def set_source_database_settings(source)
|
290
|
-
config = @model.connection.instance_variable_get(:@config)
|
291
|
-
|
292
|
-
source.sql_host = config[:host] || "localhost"
|
293
|
-
source.sql_user = config[:username] || config[:user] || ""
|
294
|
-
source.sql_pass = (config[:password].to_s || "").gsub('#', '\#')
|
295
|
-
source.sql_db = config[:database]
|
296
|
-
source.sql_port = config[:port]
|
297
|
-
source.sql_sock = config[:socket]
|
84
|
+
def delta?
|
85
|
+
!@delta_object.nil?
|
298
86
|
end
|
299
87
|
|
300
|
-
|
301
|
-
attributes.each do |attrib|
|
302
|
-
source.send(attrib.type_to_config) << attrib.config_value(offset)
|
303
|
-
end
|
304
|
-
end
|
88
|
+
private
|
305
89
|
|
306
|
-
def
|
307
|
-
|
308
|
-
source.sql_query_range = to_sql_query_range(:delta => delta)
|
309
|
-
source.sql_query_info = to_sql_query_info(offset)
|
310
|
-
|
311
|
-
source.sql_query_pre += send(!delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta)
|
312
|
-
|
313
|
-
if @options[:group_concat_max_len]
|
314
|
-
source.sql_query_pre << "SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}"
|
315
|
-
end
|
316
|
-
|
317
|
-
source.sql_query_pre += [adapter.utf8_query_pre].compact if utf8?
|
90
|
+
def adapter
|
91
|
+
@adapter ||= @model.sphinx_database_adapter
|
318
92
|
end
|
319
93
|
|
320
|
-
def
|
321
|
-
|
322
|
-
source.send("#{key}=".to_sym, value)
|
323
|
-
end
|
324
|
-
|
325
|
-
@options.each do |key, value|
|
326
|
-
source.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::SourceOptions.include?(key.to_s) && !value.nil?
|
327
|
-
end
|
94
|
+
def utf8?
|
95
|
+
options[:charset_type] == "utf-8"
|
328
96
|
end
|
329
97
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
98
|
+
# Does all the magic with the block provided to the base #initialize.
|
99
|
+
# Creates a new class subclassed from Builder, and evaluates the block
|
100
|
+
# on it, then pulls all relevant settings - fields, attributes, conditions,
|
101
|
+
# properties - into the new index.
|
102
|
+
#
|
103
|
+
def initialize_from_builder(&block)
|
104
|
+
#
|
336
105
|
end
|
337
106
|
|
338
107
|
def sql_query_pre_for_delta
|
339
108
|
[""]
|
340
109
|
end
|
341
|
-
|
342
|
-
# Generates the big SQL statement to get the data back for all the fields
|
343
|
-
# and attributes, using all the relevant association joins. If you want
|
344
|
-
# the version filtered for delta values, send through :delta => true in the
|
345
|
-
# options. Won't do much though if the index isn't set up to support a
|
346
|
-
# delta sibling.
|
347
|
-
#
|
348
|
-
# Examples:
|
349
|
-
#
|
350
|
-
# index.to_sql
|
351
|
-
# index.to_sql(:delta => true)
|
352
|
-
#
|
353
|
-
def to_sql(options={})
|
354
|
-
assocs = all_associations
|
355
|
-
|
356
|
-
where_clause = ""
|
357
|
-
if self.delta? && !@delta_object.clause(@model, options[:delta]).blank?
|
358
|
-
where_clause << " AND #{@delta_object.clause(@model, options[:delta])}"
|
359
|
-
end
|
360
|
-
unless @conditions.empty?
|
361
|
-
where_clause << " AND " << @conditions.join(" AND ")
|
362
|
-
end
|
363
|
-
|
364
|
-
internal_groupings = []
|
365
|
-
if @model.column_names.include?(@model.inheritance_column)
|
366
|
-
internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
|
367
|
-
end
|
368
|
-
|
369
|
-
unique_id_expr = ThinkingSphinx.unique_id_expression(options[:offset])
|
370
|
-
|
371
|
-
sql = <<-SQL
|
372
|
-
SELECT #{ (
|
373
|
-
["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
|
374
|
-
@fields.collect { |field| field.to_select_sql } +
|
375
|
-
@attributes.collect { |attribute| attribute.to_select_sql }
|
376
|
-
).compact.join(", ") }
|
377
|
-
FROM #{ @model.table_name }
|
378
|
-
#{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
|
379
|
-
WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
|
380
|
-
AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end
|
381
|
-
#{ where_clause }
|
382
|
-
GROUP BY #{ (
|
383
|
-
["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
|
384
|
-
@fields.collect { |field| field.to_group_sql }.compact +
|
385
|
-
@attributes.collect { |attribute| attribute.to_group_sql }.compact +
|
386
|
-
@groupings + internal_groupings
|
387
|
-
).join(", ") }
|
388
|
-
SQL
|
389
|
-
|
390
|
-
sql += " ORDER BY NULL" if adapter.sphinx_identifier == "mysql"
|
391
|
-
sql
|
392
|
-
end
|
393
|
-
|
394
|
-
# Simple helper method for the query info SQL - which is a statement that
|
395
|
-
# returns the single row for a corresponding id.
|
396
|
-
#
|
397
|
-
def to_sql_query_info(offset)
|
398
|
-
"SELECT * FROM #{@model.quoted_table_name} WHERE " +
|
399
|
-
" #{quote_column(@model.primary_key)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})"
|
400
|
-
end
|
401
|
-
|
402
|
-
# Simple helper method for the query range SQL - which is a statement that
|
403
|
-
# returns minimum and maximum id values. These can be filtered by delta -
|
404
|
-
# so pass in :delta => true to get the delta version of the SQL.
|
405
|
-
#
|
406
|
-
def to_sql_query_range(options={})
|
407
|
-
min_statement = adapter.convert_nulls(
|
408
|
-
"MIN(#{quote_column(@model.primary_key)})", 1
|
409
|
-
)
|
410
|
-
max_statement = adapter.convert_nulls(
|
411
|
-
"MAX(#{quote_column(@model.primary_key)})", 1
|
412
|
-
)
|
413
|
-
|
414
|
-
sql = "SELECT #{min_statement}, #{max_statement} " +
|
415
|
-
"FROM #{@model.quoted_table_name} "
|
416
|
-
if self.delta? && !@delta_object.clause(@model, options[:delta]).blank?
|
417
|
-
sql << "WHERE #{@delta_object.clause(@model, options[:delta])}"
|
418
|
-
end
|
419
|
-
|
420
|
-
sql
|
421
|
-
end
|
422
110
|
end
|
423
111
|
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
class Property
|
3
|
+
attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
|
4
|
+
|
5
|
+
def initialize(source, columns, options = {})
|
6
|
+
@source = source
|
7
|
+
@model = source.model
|
8
|
+
@columns = Array(columns)
|
9
|
+
@associations = {}
|
10
|
+
|
11
|
+
raise "Cannot define a field or attribute in #{source.model.name} 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) }
|
12
|
+
|
13
|
+
@alias = options[:as]
|
14
|
+
@faceted = options[:facet]
|
15
|
+
@admin = options[:admin]
|
16
|
+
|
17
|
+
@columns.each { |col|
|
18
|
+
@associations[col] = association_stack(col.__stack.clone).each { |assoc|
|
19
|
+
assoc.join_to(source.base)
|
20
|
+
}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the unique name of the attribute - which is either the alias of
|
25
|
+
# the attribute, or the name of the only column - if there is only one. If
|
26
|
+
# there isn't, there should be an alias. Else things probably won't work.
|
27
|
+
# Consider yourself warned.
|
28
|
+
#
|
29
|
+
def unique_name
|
30
|
+
if @columns.length == 1
|
31
|
+
@alias || @columns.first.__name
|
32
|
+
else
|
33
|
+
@alias
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_facet
|
38
|
+
return nil unless @faceted
|
39
|
+
|
40
|
+
ThinkingSphinx::Facet.new(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get the part of the GROUP BY clause related to this attribute - if one is
|
44
|
+
# needed. If not, all you'll get back is nil. The latter will happen if
|
45
|
+
# there isn't actually a real column to get data from, or if there's
|
46
|
+
# multiple data values (read: a has_many or has_and_belongs_to_many
|
47
|
+
# association).
|
48
|
+
#
|
49
|
+
def to_group_sql
|
50
|
+
case
|
51
|
+
when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
|
52
|
+
nil
|
53
|
+
else
|
54
|
+
@columns.collect { |column|
|
55
|
+
column_with_prefix(column)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def changed?(instance)
|
61
|
+
return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
|
62
|
+
|
63
|
+
!@columns.all? { |col|
|
64
|
+
instance.respond_to?("#{col.__name.to_s}_changed?") &&
|
65
|
+
!instance.send("#{col.__name.to_s}_changed?")
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def admin?
|
70
|
+
admin
|
71
|
+
end
|
72
|
+
|
73
|
+
def public?
|
74
|
+
!admin
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Could there be more than one value related to the parent record? If so,
|
80
|
+
# then this will return true. If not, false. It's that simple.
|
81
|
+
#
|
82
|
+
def is_many?
|
83
|
+
associations.values.flatten.any? { |assoc| assoc.is_many? }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns true if any of the columns are string values, instead of database
|
87
|
+
# column references.
|
88
|
+
def is_string?
|
89
|
+
columns.all? { |col| col.is_string? }
|
90
|
+
end
|
91
|
+
|
92
|
+
def adapter
|
93
|
+
@adapter ||= @model.sphinx_database_adapter
|
94
|
+
end
|
95
|
+
|
96
|
+
def quote_with_table(table, column)
|
97
|
+
"#{quote_table_name(table)}.#{quote_column(column)}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def quote_column(column)
|
101
|
+
@model.connection.quote_column_name(column)
|
102
|
+
end
|
103
|
+
|
104
|
+
def quote_table_name(table_name)
|
105
|
+
@model.connection.quote_table_name(table_name)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Indication of whether the columns should be concatenated with a space
|
109
|
+
# between each value. True if there's either multiple sources or multiple
|
110
|
+
# associations.
|
111
|
+
#
|
112
|
+
def concat_ws?
|
113
|
+
multiple_associations? || @columns.length > 1
|
114
|
+
end
|
115
|
+
|
116
|
+
# Checks whether any column requires multiple associations (which only
|
117
|
+
# happens for polymorphic situations).
|
118
|
+
#
|
119
|
+
def multiple_associations?
|
120
|
+
associations.any? { |col,assocs| assocs.length > 1 }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Builds a column reference tied to the appropriate associations. This
|
124
|
+
# dives into the associations hash and their corresponding joins to
|
125
|
+
# figure out how to correctly reference a column in SQL.
|
126
|
+
#
|
127
|
+
def column_with_prefix(column)
|
128
|
+
if column.is_string?
|
129
|
+
column.__name
|
130
|
+
elsif associations[column].empty?
|
131
|
+
"#{@model.quoted_table_name}.#{quote_column(column.__name)}"
|
132
|
+
else
|
133
|
+
associations[column].collect { |assoc|
|
134
|
+
assoc.has_column?(column.__name) ?
|
135
|
+
"#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
|
136
|
+
nil
|
137
|
+
}.compact.join(', ')
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Gets a stack of associations for a specific path.
|
142
|
+
#
|
143
|
+
def association_stack(path, parent = nil)
|
144
|
+
assocs = []
|
145
|
+
|
146
|
+
if parent.nil?
|
147
|
+
assocs = @source.association(path.shift)
|
148
|
+
else
|
149
|
+
assocs = parent.children(path.shift)
|
150
|
+
end
|
151
|
+
|
152
|
+
until path.empty?
|
153
|
+
point = path.shift
|
154
|
+
assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
|
155
|
+
end
|
156
|
+
|
157
|
+
assocs
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
class Search
|
3
|
+
module Facets
|
4
|
+
# Model.facets *args
|
5
|
+
# ThinkingSphinx::Search.facets *args
|
6
|
+
# ThinkingSphinx::Search.facets *args, :all_attributes => true
|
7
|
+
# ThinkingSphinx::Search.facets *args, :class_facet => false
|
8
|
+
#
|
9
|
+
def facets(*args)
|
10
|
+
options = args.extract_options!
|
11
|
+
|
12
|
+
if options[:class]
|
13
|
+
facets_for_model options[:class], args, options
|
14
|
+
else
|
15
|
+
facets_for_all_models args, options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def facets_for_model(klass, args, options)
|
22
|
+
hash = ThinkingSphinx::FacetCollection.new args + [options]
|
23
|
+
options = options.clone.merge! facet_query_options
|
24
|
+
|
25
|
+
klass.sphinx_facets.inject(hash) do |hash, facet|
|
26
|
+
unless facet.name == :class && !options[:class_facet]
|
27
|
+
options[:group_by] = facet.attribute_name
|
28
|
+
hash.add_from_results facet, search(*(args + [options]))
|
29
|
+
end
|
30
|
+
|
31
|
+
hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def facets_for_all_models(args, options)
|
36
|
+
options = GlobalFacetOptions.merge(options)
|
37
|
+
hash = ThinkingSphinx::FacetCollection.new args + [options]
|
38
|
+
options = options.merge! facet_query_options
|
39
|
+
|
40
|
+
facet_names(options).inject(hash) do |hash, name|
|
41
|
+
options[:group_by] = name
|
42
|
+
hash.add_from_results name, search(*(args + [options]))
|
43
|
+
hash
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def facet_query_options
|
48
|
+
config = ThinkingSphinx::Configuration.instance
|
49
|
+
max = config.configuration.searchd.max_matches || 1000
|
50
|
+
|
51
|
+
{
|
52
|
+
:group_function => :attr,
|
53
|
+
:limit => max,
|
54
|
+
:max_matches => max
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def facet_classes(options)
|
59
|
+
options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
|
60
|
+
model.constantize
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def facet_names(options)
|
65
|
+
classes = facet_classes(options)
|
66
|
+
names = options[:all_attributes] ?
|
67
|
+
facet_names_for_all_classes(classes) :
|
68
|
+
facet_names_common_to_all_classes(classes)
|
69
|
+
|
70
|
+
names.delete "class_crc" unless options[:class_facet]
|
71
|
+
names
|
72
|
+
end
|
73
|
+
|
74
|
+
def facet_names_for_all_classes(classes)
|
75
|
+
all_facets = classes.collect { |klass| klass.sphinx_facets }.flatten
|
76
|
+
|
77
|
+
all_facets.group_by { |facet|
|
78
|
+
facet.name
|
79
|
+
}.collect { |name, facets|
|
80
|
+
if facets.collect { |facet| facet.type }.uniq.length > 1
|
81
|
+
raise "Facet #{name} exists in more than one model with different types"
|
82
|
+
end
|
83
|
+
facets.first.attribute_name
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def facet_names_common_to_all_classes(classes)
|
88
|
+
facet_names_for_all_classes(classes).select { |name|
|
89
|
+
classes.all? { |klass|
|
90
|
+
klass.sphinx_facets.detect { |facet|
|
91
|
+
facet.attribute_name == name
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|