dpickett-thinking-sphinx 1.1.4 → 1.1.12
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.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
@@ -37,6 +37,19 @@ module ThinkingSphinx
|
|
37
37
|
@delta_object = nil
|
38
38
|
|
39
39
|
initialize_from_builder(&block) if block_given?
|
40
|
+
|
41
|
+
add_internal_attributes_and_facets
|
42
|
+
|
43
|
+
# We want to make sure that if the database doesn't exist, then Thinking
|
44
|
+
# Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
|
45
|
+
# and db:migrate). It's a bit hacky, but I can't think of a better way.
|
46
|
+
rescue StandardError => err
|
47
|
+
case err.class.name
|
48
|
+
when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
|
49
|
+
return
|
50
|
+
else
|
51
|
+
raise err
|
52
|
+
end
|
40
53
|
end
|
41
54
|
|
42
55
|
def name
|
@@ -48,7 +61,6 @@ module ThinkingSphinx
|
|
48
61
|
end
|
49
62
|
|
50
63
|
def to_riddle_for_core(offset, index)
|
51
|
-
add_internal_attributes
|
52
64
|
link!
|
53
65
|
|
54
66
|
source = Riddle::Configuration::SQLSource.new(
|
@@ -56,7 +68,7 @@ module ThinkingSphinx
|
|
56
68
|
)
|
57
69
|
|
58
70
|
set_source_database_settings source
|
59
|
-
set_source_attributes source
|
71
|
+
set_source_attributes source, offset
|
60
72
|
set_source_sql source, offset
|
61
73
|
set_source_settings source
|
62
74
|
|
@@ -64,7 +76,6 @@ module ThinkingSphinx
|
|
64
76
|
end
|
65
77
|
|
66
78
|
def to_riddle_for_delta(offset, index)
|
67
|
-
add_internal_attributes
|
68
79
|
link!
|
69
80
|
|
70
81
|
source = Riddle::Configuration::SQLSource.new(
|
@@ -73,7 +84,7 @@ module ThinkingSphinx
|
|
73
84
|
source.parent = "#{name}_core_#{index}"
|
74
85
|
|
75
86
|
set_source_database_settings source
|
76
|
-
set_source_attributes source
|
87
|
+
set_source_attributes source, offset
|
77
88
|
set_source_sql source, offset, true
|
78
89
|
|
79
90
|
source
|
@@ -175,17 +186,6 @@ module ThinkingSphinx
|
|
175
186
|
@model.sphinx_facets ||= []
|
176
187
|
@fields.select( &is_faceted).each &add_facet
|
177
188
|
@attributes.select(&is_faceted).each &add_facet
|
178
|
-
|
179
|
-
# We want to make sure that if the database doesn't exist, then Thinking
|
180
|
-
# Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
|
181
|
-
# and db:migrate). It's a bit hacky, but I can't think of a better way.
|
182
|
-
rescue StandardError => err
|
183
|
-
case err.class.name
|
184
|
-
when "Mysql::Error", "ActiveRecord::StatementInvalid"
|
185
|
-
return
|
186
|
-
else
|
187
|
-
raise err
|
188
|
-
end
|
189
189
|
end
|
190
190
|
|
191
191
|
# Returns all associations used amongst all the fields and attributes.
|
@@ -200,8 +200,8 @@ module ThinkingSphinx
|
|
200
200
|
}.flatten +
|
201
201
|
# attribute associations
|
202
202
|
@attributes.collect { |attrib|
|
203
|
-
attrib.associations.values
|
204
|
-
}.flatten
|
203
|
+
attrib.associations.values if attrib.include_as_association?
|
204
|
+
}.compact.flatten
|
205
205
|
).uniq.collect { |assoc|
|
206
206
|
# get ancestors as well as column-level associations
|
207
207
|
assoc.ancestors
|
@@ -238,7 +238,7 @@ module ThinkingSphinx
|
|
238
238
|
def crc_column
|
239
239
|
if @model.column_names.include?(@model.inheritance_column)
|
240
240
|
adapter.cast_to_unsigned(adapter.convert_nulls(
|
241
|
-
adapter.crc(adapter.quote_with_table(@model.inheritance_column)),
|
241
|
+
adapter.crc(adapter.quote_with_table(@model.inheritance_column), true),
|
242
242
|
@model.to_crc32
|
243
243
|
))
|
244
244
|
else
|
@@ -246,50 +246,45 @@ module ThinkingSphinx
|
|
246
246
|
end
|
247
247
|
end
|
248
248
|
|
249
|
-
def
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
) unless @attributes.detect { |attr| attr.alias == :sphinx_internal_id }
|
255
|
-
|
256
|
-
unless @attributes.detect { |attr| attr.alias == :class_crc }
|
257
|
-
@attributes << Attribute.new(
|
258
|
-
FauxColumn.new(crc_column),
|
259
|
-
:type => :integer,
|
260
|
-
:as => :class_crc,
|
261
|
-
:facet => true
|
262
|
-
)
|
263
|
-
|
264
|
-
@model.sphinx_facets << ThinkingSphinx::ClassFacet.new(@attributes.last)
|
265
|
-
end
|
266
|
-
|
267
|
-
if @model.column_names.include?(@model.inheritance_column)
|
268
|
-
class_col = FauxColumn.new(
|
269
|
-
adapter.convert_nulls(adapter.quote_with_table(@model.inheritance_column), @model.to_s)
|
270
|
-
)
|
271
|
-
else
|
272
|
-
class_col = FauxColumn.new("'#{@model.to_s}'")
|
273
|
-
end
|
249
|
+
def add_internal_attributes_and_facets
|
250
|
+
add_internal_attribute :sphinx_internal_id, :integer, @model.primary_key.to_sym
|
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"
|
274
254
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
255
|
+
add_internal_facet :class_crc
|
256
|
+
end
|
257
|
+
|
258
|
+
def add_internal_attribute(name, type, contents, facet = false)
|
259
|
+
return unless attribute_by_alias(name).nil?
|
279
260
|
|
280
261
|
@attributes << Attribute.new(
|
281
|
-
FauxColumn.new(
|
282
|
-
|
283
|
-
|
284
|
-
:
|
285
|
-
:
|
286
|
-
)
|
262
|
+
FauxColumn.new(contents),
|
263
|
+
:type => type,
|
264
|
+
:as => name,
|
265
|
+
:facet => facet,
|
266
|
+
:admin => true
|
267
|
+
)
|
268
|
+
end
|
269
|
+
|
270
|
+
def add_internal_facet(name)
|
271
|
+
return unless facet_by_alias(name).nil?
|
287
272
|
|
288
|
-
@
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
273
|
+
@model.sphinx_facets << ClassFacet.new(attribute_by_alias(name))
|
274
|
+
end
|
275
|
+
|
276
|
+
def attribute_by_alias(attr_alias)
|
277
|
+
@attributes.detect { |attrib| attrib.alias == attr_alias }
|
278
|
+
end
|
279
|
+
|
280
|
+
def facet_by_alias(name)
|
281
|
+
@model.sphinx_facets.detect { |facet| facet.name == name }
|
282
|
+
end
|
283
|
+
|
284
|
+
def subclasses_to_s
|
285
|
+
"'" + (@model.send(:subclasses).collect { |klass|
|
286
|
+
klass.to_crc32.to_s
|
287
|
+
} << @model.to_crc32.to_s).join(",") + "'"
|
293
288
|
end
|
294
289
|
|
295
290
|
def set_source_database_settings(source)
|
@@ -303,9 +298,9 @@ module ThinkingSphinx
|
|
303
298
|
source.sql_sock = config[:socket]
|
304
299
|
end
|
305
300
|
|
306
|
-
def set_source_attributes(source)
|
301
|
+
def set_source_attributes(source, offset = nil)
|
307
302
|
attributes.each do |attrib|
|
308
|
-
source.send(attrib.type_to_config) << attrib.config_value
|
303
|
+
source.send(attrib.type_to_config) << attrib.config_value(offset)
|
309
304
|
end
|
310
305
|
end
|
311
306
|
|
@@ -372,14 +367,14 @@ module ThinkingSphinx
|
|
372
367
|
internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
|
373
368
|
end
|
374
369
|
|
375
|
-
unique_id_expr =
|
370
|
+
unique_id_expr = ThinkingSphinx.unique_id_expression(options[:offset])
|
376
371
|
|
377
372
|
sql = <<-SQL
|
378
373
|
SELECT #{ (
|
379
374
|
["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
|
380
375
|
@fields.collect { |field| field.to_select_sql } +
|
381
376
|
@attributes.collect { |attribute| attribute.to_select_sql }
|
382
|
-
).join(", ") }
|
377
|
+
).compact.join(", ") }
|
383
378
|
FROM #{ @model.table_name }
|
384
379
|
#{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
|
385
380
|
WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
|
@@ -393,10 +388,7 @@ GROUP BY #{ (
|
|
393
388
|
).join(", ") }
|
394
389
|
SQL
|
395
390
|
|
396
|
-
if
|
397
|
-
sql += " ORDER BY NULL"
|
398
|
-
end
|
399
|
-
|
391
|
+
sql += " ORDER BY NULL" if adapter.sphinx_identifier == "mysql"
|
400
392
|
sql
|
401
393
|
end
|
402
394
|
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
class Property
|
3
|
+
attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
|
4
|
+
|
5
|
+
def initialize(columns, options = {})
|
6
|
+
@columns = Array(columns)
|
7
|
+
@associations = {}
|
8
|
+
|
9
|
+
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) }
|
10
|
+
|
11
|
+
@alias = options[:as]
|
12
|
+
@faceted = options[:facet]
|
13
|
+
@admin = options[:admin]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the unique name of the attribute - which is either the alias of
|
17
|
+
# the attribute, or the name of the only column - if there is only one. If
|
18
|
+
# there isn't, there should be an alias. Else things probably won't work.
|
19
|
+
# Consider yourself warned.
|
20
|
+
#
|
21
|
+
def unique_name
|
22
|
+
if @columns.length == 1
|
23
|
+
@alias || @columns.first.__name
|
24
|
+
else
|
25
|
+
@alias
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_facet
|
30
|
+
return nil unless @faceted
|
31
|
+
|
32
|
+
ThinkingSphinx::Facet.new(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the part of the GROUP BY clause related to this attribute - if one is
|
36
|
+
# needed. If not, all you'll get back is nil. The latter will happen if
|
37
|
+
# there isn't actually a real column to get data from, or if there's
|
38
|
+
# multiple data values (read: a has_many or has_and_belongs_to_many
|
39
|
+
# association).
|
40
|
+
#
|
41
|
+
def to_group_sql
|
42
|
+
case
|
43
|
+
when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
|
44
|
+
nil
|
45
|
+
else
|
46
|
+
@columns.collect { |column|
|
47
|
+
column_with_prefix(column)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def changed?(instance)
|
53
|
+
return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
|
54
|
+
|
55
|
+
!@columns.all? { |col|
|
56
|
+
instance.respond_to?("#{col.__name.to_s}_changed?") &&
|
57
|
+
!instance.send("#{col.__name.to_s}_changed?")
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def admin?
|
62
|
+
admin
|
63
|
+
end
|
64
|
+
|
65
|
+
def public?
|
66
|
+
!admin
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Could there be more than one value related to the parent record? If so,
|
72
|
+
# then this will return true. If not, false. It's that simple.
|
73
|
+
#
|
74
|
+
def is_many?
|
75
|
+
associations.values.flatten.any? { |assoc| assoc.is_many? }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns true if any of the columns are string values, instead of database
|
79
|
+
# column references.
|
80
|
+
def is_string?
|
81
|
+
columns.all? { |col| col.is_string? }
|
82
|
+
end
|
83
|
+
|
84
|
+
def adapter
|
85
|
+
@adapter ||= @model.sphinx_database_adapter
|
86
|
+
end
|
87
|
+
|
88
|
+
def quote_with_table(table, column)
|
89
|
+
"#{quote_table_name(table)}.#{quote_column(column)}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def quote_column(column)
|
93
|
+
@model.connection.quote_column_name(column)
|
94
|
+
end
|
95
|
+
|
96
|
+
def quote_table_name(table_name)
|
97
|
+
@model.connection.quote_table_name(table_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Indication of whether the columns should be concatenated with a space
|
101
|
+
# between each value. True if there's either multiple sources or multiple
|
102
|
+
# associations.
|
103
|
+
#
|
104
|
+
def concat_ws?
|
105
|
+
multiple_associations? || @columns.length > 1
|
106
|
+
end
|
107
|
+
|
108
|
+
# Checks whether any column requires multiple associations (which only
|
109
|
+
# happens for polymorphic situations).
|
110
|
+
#
|
111
|
+
def multiple_associations?
|
112
|
+
associations.any? { |col,assocs| assocs.length > 1 }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Builds a column reference tied to the appropriate associations. This
|
116
|
+
# dives into the associations hash and their corresponding joins to
|
117
|
+
# figure out how to correctly reference a column in SQL.
|
118
|
+
#
|
119
|
+
def column_with_prefix(column)
|
120
|
+
if column.is_string?
|
121
|
+
column.__name
|
122
|
+
elsif associations[column].empty?
|
123
|
+
"#{@model.quoted_table_name}.#{quote_column(column.__name)}"
|
124
|
+
else
|
125
|
+
associations[column].collect { |assoc|
|
126
|
+
assoc.has_column?(column.__name) ?
|
127
|
+
"#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
|
128
|
+
nil
|
129
|
+
}.compact.join(', ')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -49,10 +49,13 @@ module ThinkingSphinx
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
-
if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter")
|
53
|
-
ActiveRecord::ConnectionAdapters
|
54
|
-
:
|
55
|
-
)
|
52
|
+
if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") or ActiveRecord::Base.respond_to?(:jdbcmysql_connection)
|
53
|
+
adapter = ActiveRecord::ConnectionAdapters.const_get(
|
54
|
+
defined?(JRUBY_VERSION) ? :JdbcAdapter : :MysqlAdapter
|
55
|
+
)
|
56
|
+
unless adapter.instance_methods.include?("quote_table_name")
|
57
|
+
adapter.send(:include, ThinkingSphinx::MysqlQuotedTableName)
|
58
|
+
end
|
56
59
|
end
|
57
60
|
|
58
61
|
module ThinkingSphinx
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module ThinkingSphinx
|
1
|
+
module ThinkingSphinx
|
2
2
|
# Once you've got those indexes in and built, this is the stuff that
|
3
3
|
# matters - how to search! This class provides a generic search
|
4
4
|
# interface - which you can use to search all your indexed models at once.
|
@@ -7,6 +7,11 @@ module ThinkingSphinx
|
|
7
7
|
# called from a model.
|
8
8
|
#
|
9
9
|
class Search
|
10
|
+
GlobalFacetOptions = {
|
11
|
+
:all_attributes => false,
|
12
|
+
:class_facet => true
|
13
|
+
}
|
14
|
+
|
10
15
|
class << self
|
11
16
|
# Searches for results that match the parameters provided. Will only
|
12
17
|
# return the ids for the matching objects. See #search for syntax
|
@@ -94,16 +99,24 @@ module ThinkingSphinx
|
|
94
99
|
# == Searching by Attributes
|
95
100
|
#
|
96
101
|
# Also known as filters, you can limit your searches to documents that
|
97
|
-
# have specific values for their attributes. There are
|
98
|
-
# this. The first
|
99
|
-
#
|
100
|
-
#
|
101
|
-
# ThinkingSphinx::Search.search :with => {:
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
102
|
+
# have specific values for their attributes. There are three ways to do
|
103
|
+
# this. The first two techniques work in all scenarios - using the :with
|
104
|
+
# or :with_all options.
|
105
|
+
#
|
106
|
+
# ThinkingSphinx::Search.search :with => {:tag_ids => 10}
|
107
|
+
# ThinkingSphinx::Search.search :with => {:tag_ids => [10,12]}
|
108
|
+
# ThinkingSphinx::Search.search :with_all => {:tag_ids => [10,12]}
|
109
|
+
#
|
110
|
+
# The first :with search will match records with a tag_id attribute of 10.
|
111
|
+
# The second :with will match records with a tag_id attribute of 10 OR 12.
|
112
|
+
# If you need to find records that are tagged with ids 10 AND 12, you
|
113
|
+
# will need to use the :with_all search parameter. This is particuarly
|
114
|
+
# useful in conjunction with Multi Value Attributes (MVAs).
|
115
|
+
#
|
116
|
+
# The third filtering technique is only viable if you're searching with a
|
117
|
+
# specific model (not multi-model searching). With a single model,
|
118
|
+
# Thinking Sphinx can figure out what attributes and fields are available,
|
119
|
+
# so you can put it all in the :conditions hash, and it will sort it out.
|
107
120
|
#
|
108
121
|
# Node.search :conditions => {:parent_id => 10}
|
109
122
|
#
|
@@ -186,6 +199,12 @@ module ThinkingSphinx
|
|
186
199
|
# documentation[http://sphinxsearch.com/doc.html] for that level of
|
187
200
|
# detail though.
|
188
201
|
#
|
202
|
+
# If desired, you can sort by a column in your model instead of a sphinx
|
203
|
+
# field or attribute. This sort only applies to the current page, so is
|
204
|
+
# most useful when performing a search with a single page of results.
|
205
|
+
#
|
206
|
+
# User.search("pat", :sql_order => "name")
|
207
|
+
#
|
189
208
|
# == Grouping
|
190
209
|
#
|
191
210
|
# For this you can use the group_by, group_clause and group_function
|
@@ -194,7 +213,66 @@ module ThinkingSphinx
|
|
194
213
|
# you read all the relevant
|
195
214
|
# documentation[http://sphinxsearch.com/doc.html#clustering] first.
|
196
215
|
#
|
197
|
-
#
|
216
|
+
# Grouping is done via three parameters within the options hash
|
217
|
+
# * <tt>:group_function</tt> determines the way grouping is done
|
218
|
+
# * <tt>:group_by</tt> determines the field which is used for grouping
|
219
|
+
# * <tt>:group_clause</tt> determines the sorting order
|
220
|
+
#
|
221
|
+
# === group_function
|
222
|
+
#
|
223
|
+
# Valid values for :group_function are
|
224
|
+
# * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
|
225
|
+
# * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
|
226
|
+
#
|
227
|
+
# === group_by
|
228
|
+
#
|
229
|
+
# This parameter denotes the field by which grouping is done. Note that the
|
230
|
+
# specified field must be a sphinx attribute or index.
|
231
|
+
#
|
232
|
+
# === group_clause
|
233
|
+
#
|
234
|
+
# This determines the sorting order of the groups. In a grouping search,
|
235
|
+
# the matches within a group will sorted by the <tt>:sort_mode</tt> and <tt>:order</tt> parameters.
|
236
|
+
# The group matches themselves however, will be sorted by <tt>:group_clause</tt>.
|
237
|
+
#
|
238
|
+
# The syntax for this is the same as an order parameter in extended sort mode.
|
239
|
+
# Namely, you can specify an SQL-like sort expression with up to 5 attributes
|
240
|
+
# (including internal attributes), eg: "@relevance DESC, price ASC, @id DESC"
|
241
|
+
#
|
242
|
+
# === Grouping by timestamp
|
243
|
+
#
|
244
|
+
# Timestamp grouping groups off items by the day, week, month or year of the
|
245
|
+
# attribute given. In order to do this you need to define a timestamp attribute,
|
246
|
+
# which pretty much looks like the standard defintion for any attribute.
|
247
|
+
#
|
248
|
+
# define_index do
|
249
|
+
# #
|
250
|
+
# # All your other stuff
|
251
|
+
# #
|
252
|
+
# has :created_at
|
253
|
+
# end
|
254
|
+
#
|
255
|
+
# When you need to fire off your search, it'll go something to the tune of
|
256
|
+
#
|
257
|
+
# Fruit.search "apricot", :group_function => :day, :group_by => 'created_at'
|
258
|
+
#
|
259
|
+
# The <tt>@groupby</tt> special attribute will contain the date for that group.
|
260
|
+
# Depending on the <tt>:group_function</tt> parameter, the date format will be
|
261
|
+
#
|
262
|
+
# * <tt>:day</tt> - YYYYMMDD
|
263
|
+
# * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
|
264
|
+
# counting from the start of the year )
|
265
|
+
# * <tt>:month</tt> - YYYYMM
|
266
|
+
# * <tt>:year</tt> - YYYY
|
267
|
+
#
|
268
|
+
#
|
269
|
+
# === Grouping by attribute
|
270
|
+
#
|
271
|
+
# The syntax is the same as grouping by timestamp, except for the fact that the
|
272
|
+
# <tt>:group_function</tt> parameter is changed
|
273
|
+
#
|
274
|
+
# Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
|
275
|
+
#
|
198
276
|
#
|
199
277
|
# == Geo/Location Searching
|
200
278
|
#
|
@@ -291,8 +369,10 @@ module ThinkingSphinx
|
|
291
369
|
def retry_search_on_stale_index(query, options, &block)
|
292
370
|
stale_ids = []
|
293
371
|
stale_retries_left = case options[:retry_stale]
|
294
|
-
when true
|
295
|
-
|
372
|
+
when true
|
373
|
+
3 # default to three retries
|
374
|
+
when nil, false
|
375
|
+
0 # no retries
|
296
376
|
else options[:retry_stale].to_i
|
297
377
|
end
|
298
378
|
begin
|
@@ -352,43 +432,23 @@ module ThinkingSphinx
|
|
352
432
|
end
|
353
433
|
end
|
354
434
|
|
435
|
+
# Model.facets *args
|
436
|
+
# ThinkingSphinx::Search.facets *args
|
437
|
+
# ThinkingSphinx::Search.facets *args, :all_attributes => true
|
438
|
+
# ThinkingSphinx::Search.facets *args, :class_facet => false
|
439
|
+
#
|
355
440
|
def facets(*args)
|
356
|
-
|
357
|
-
options = args.extract_options!.clone.merge! :group_function => :attr
|
358
|
-
|
359
|
-
klasses = options[:classes] || [options[:class]]
|
360
|
-
klasses = [] if options[:class].nil?
|
361
|
-
|
362
|
-
#no classes specified so get classes from resultset
|
363
|
-
if klasses.empty?
|
364
|
-
options[:group_by] = "class_crc"
|
365
|
-
results = search(*(args + [options]))
|
366
|
-
|
367
|
-
hash[:class] = {}
|
368
|
-
results.each_with_groupby_and_count do |result, group, count|
|
369
|
-
hash[:class][result.class.name] = count
|
370
|
-
klasses << result.class
|
371
|
-
end
|
372
|
-
end
|
441
|
+
options = args.extract_options!
|
373
442
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
search(*(args +
|
379
|
-
[options.merge(:group_by => facet.attribute_name)]))
|
380
|
-
end
|
381
|
-
|
382
|
-
hash
|
383
|
-
end
|
443
|
+
if options[:class]
|
444
|
+
facets_for_model options[:class], args, options
|
445
|
+
else
|
446
|
+
facets_for_all_models args, options
|
384
447
|
end
|
385
|
-
|
386
|
-
hash
|
387
448
|
end
|
388
449
|
|
389
450
|
private
|
390
451
|
|
391
|
-
|
392
452
|
# This method handles the common search functionality, and returns both
|
393
453
|
# the result hash and the client. Not super elegant, but it'll do for
|
394
454
|
# the moment.
|
@@ -412,6 +472,7 @@ module ThinkingSphinx
|
|
412
472
|
|
413
473
|
client.limit = options[:per_page].to_i if options[:per_page]
|
414
474
|
page = options[:page] ? options[:page].to_i : 1
|
475
|
+
page = 1 if page <= 0
|
415
476
|
client.offset = (page - 1) * client.limit
|
416
477
|
|
417
478
|
begin
|
@@ -492,6 +553,13 @@ module ThinkingSphinx
|
|
492
553
|
Riddle::Client::Filter.new attr.to_s, filter_value(val), true
|
493
554
|
} if options[:without]
|
494
555
|
|
556
|
+
# every-match attribute filters
|
557
|
+
client.filters += options[:with_all].collect { |attr,vals|
|
558
|
+
Array(vals).collect { |val|
|
559
|
+
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
560
|
+
}
|
561
|
+
}.flatten if options[:with_all]
|
562
|
+
|
495
563
|
# exclusive attribute filter on primary key
|
496
564
|
client.filters += Array(options[:without_ids]).collect { |id|
|
497
565
|
Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
|
@@ -649,6 +717,75 @@ module ThinkingSphinx
|
|
649
717
|
|
650
718
|
string
|
651
719
|
end
|
720
|
+
|
721
|
+
def facets_for_model(klass, args, options)
|
722
|
+
hash = ThinkingSphinx::FacetCollection.new args + [options]
|
723
|
+
options = options.clone.merge! facet_query_options
|
724
|
+
|
725
|
+
klass.sphinx_facets.inject(hash) do |hash, facet|
|
726
|
+
unless facet.name == :class && !options[:class_facet]
|
727
|
+
options[:group_by] = facet.attribute_name
|
728
|
+
hash.add_from_results facet, search(*(args + [options]))
|
729
|
+
end
|
730
|
+
|
731
|
+
hash
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
def facets_for_all_models(args, options)
|
736
|
+
options = GlobalFacetOptions.merge(options)
|
737
|
+
hash = ThinkingSphinx::FacetCollection.new args + [options]
|
738
|
+
options = options.merge! facet_query_options
|
739
|
+
|
740
|
+
facet_names(options).inject(hash) do |hash, name|
|
741
|
+
options[:group_by] = name
|
742
|
+
hash.add_from_results name, search(*(args + [options]))
|
743
|
+
hash
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
def facet_query_options
|
748
|
+
config = ThinkingSphinx::Configuration.instance
|
749
|
+
max = config.configuration.searchd.max_matches || 1000
|
750
|
+
|
751
|
+
{
|
752
|
+
:group_function => :attr,
|
753
|
+
:limit => max,
|
754
|
+
:max_matches => max
|
755
|
+
}
|
756
|
+
end
|
757
|
+
|
758
|
+
def facet_classes(options)
|
759
|
+
options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
|
760
|
+
model.constantize
|
761
|
+
}
|
762
|
+
end
|
763
|
+
|
764
|
+
def facet_names(options)
|
765
|
+
classes = facet_classes(options)
|
766
|
+
names = options[:all_attributes] ?
|
767
|
+
facet_names_for_all_classes(classes) :
|
768
|
+
facet_names_common_to_all_classes(classes)
|
769
|
+
|
770
|
+
names.delete "class_crc" unless options[:class_facet]
|
771
|
+
names
|
772
|
+
end
|
773
|
+
|
774
|
+
def facet_names_for_all_classes(classes)
|
775
|
+
classes.collect { |klass|
|
776
|
+
klass.sphinx_facets.collect { |facet| facet.attribute_name }
|
777
|
+
}.flatten.uniq
|
778
|
+
end
|
779
|
+
|
780
|
+
def facet_names_common_to_all_classes(classes)
|
781
|
+
facet_names_for_all_classes(classes).select { |name|
|
782
|
+
classes.all? { |klass|
|
783
|
+
klass.sphinx_facets.detect { |facet|
|
784
|
+
facet.attribute_name == name
|
785
|
+
}
|
786
|
+
}
|
787
|
+
}
|
788
|
+
end
|
652
789
|
end
|
653
790
|
end
|
654
791
|
end
|
@@ -8,8 +8,8 @@ namespace :thinking_sphinx do
|
|
8
8
|
|
9
9
|
desc "Stop if running, then start a Sphinx searchd daemon using Thinking Sphinx's settings"
|
10
10
|
task :running_start => :app_env do
|
11
|
-
|
12
|
-
|
11
|
+
Rake::Task["thinking_sphinx:stop"].invoke if sphinx_running?
|
12
|
+
Rake::Task["thinking_sphinx:start"].invoke
|
13
13
|
end
|
14
14
|
|
15
15
|
desc "Start a Sphinx searchd daemon using Thinking Sphinx's settings"
|
@@ -30,7 +30,7 @@ namespace :thinking_sphinx do
|
|
30
30
|
if sphinx_running?
|
31
31
|
puts "Started successfully (pid #{sphinx_pid})."
|
32
32
|
else
|
33
|
-
puts "Failed to start searchd daemon. Check #{config.searchd_log_file}
|
33
|
+
puts "Failed to start searchd daemon. Check #{config.searchd_log_file}"
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
@@ -39,7 +39,7 @@ namespace :thinking_sphinx do
|
|
39
39
|
raise RuntimeError, "searchd is not running." unless sphinx_running?
|
40
40
|
config = ThinkingSphinx::Configuration.instance
|
41
41
|
pid = sphinx_pid
|
42
|
-
system "searchd --stop --config #{config.config_file}"
|
42
|
+
system "#{config.bin_path}searchd --stop --config #{config.config_file}"
|
43
43
|
puts "Stopped search daemon (pid #{pid})."
|
44
44
|
end
|
45
45
|
|