thinking-sphinx 1.2.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/LICENCE +20 -0
- data/README.textile +157 -0
- data/VERSION.yml +4 -0
- data/lib/thinking_sphinx.rb +211 -0
- data/lib/thinking_sphinx/active_record.rb +307 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
- data/lib/thinking_sphinx/active_record/delta.rb +87 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +28 -0
- data/lib/thinking_sphinx/active_record/scopes.rb +39 -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 +136 -0
- data/lib/thinking_sphinx/association.rb +164 -0
- data/lib/thinking_sphinx/attribute.rb +342 -0
- data/lib/thinking_sphinx/class_facet.rb +15 -0
- data/lib/thinking_sphinx/configuration.rb +282 -0
- data/lib/thinking_sphinx/core/array.rb +7 -0
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas.rb +30 -0
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -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/deploy/capistrano.rb +100 -0
- data/lib/thinking_sphinx/excerpter.rb +22 -0
- data/lib/thinking_sphinx/facet.rb +125 -0
- data/lib/thinking_sphinx/facet_search.rb +134 -0
- data/lib/thinking_sphinx/field.rb +82 -0
- data/lib/thinking_sphinx/index.rb +99 -0
- data/lib/thinking_sphinx/index/builder.rb +286 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/property.rb +162 -0
- data/lib/thinking_sphinx/rails_additions.rb +150 -0
- data/lib/thinking_sphinx/search.rb +707 -0
- data/lib/thinking_sphinx/search_methods.rb +421 -0
- data/lib/thinking_sphinx/source.rb +150 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +128 -0
- data/lib/thinking_sphinx/tasks.rb +165 -0
- data/rails/init.rb +14 -0
- data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +130 -0
- data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +49 -0
- data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
- data/spec/lib/thinking_sphinx/active_record_spec.rb +364 -0
- data/spec/lib/thinking_sphinx/association_spec.rb +239 -0
- data/spec/lib/thinking_sphinx/attribute_spec.rb +500 -0
- data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
- data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
- data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
- data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
- data/spec/lib/thinking_sphinx/facet_spec.rb +333 -0
- data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
- data/spec/lib/thinking_sphinx/index/builder_spec.rb +455 -0
- data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
- data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
- data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
- data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
- data/spec/lib/thinking_sphinx/search_spec.rb +1092 -0
- data/spec/lib/thinking_sphinx/source_spec.rb +227 -0
- data/spec/lib/thinking_sphinx_spec.rb +162 -0
- data/tasks/distribution.rb +50 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +83 -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/vendor/riddle/lib/riddle.rb +30 -0
- data/vendor/riddle/lib/riddle/client.rb +635 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
- data/vendor/riddle/lib/riddle/client/message.rb +66 -0
- data/vendor/riddle/lib/riddle/client/response.rb +84 -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 +43 -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 +53 -0
- metadata +172 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
class Index
|
|
3
|
+
# Instances of this class represent database columns and the stack of
|
|
4
|
+
# associations that lead from the base model to them.
|
|
5
|
+
#
|
|
6
|
+
# The name and stack are accessible through methods starting with __ to
|
|
7
|
+
# avoid conflicting with the method_missing calls that build the stack.
|
|
8
|
+
#
|
|
9
|
+
class FauxColumn
|
|
10
|
+
# Create a new column with a pre-defined stack. The top element in the
|
|
11
|
+
# stack will get shifted to be the name value.
|
|
12
|
+
#
|
|
13
|
+
def initialize(*stack)
|
|
14
|
+
@name = stack.pop
|
|
15
|
+
@stack = stack
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.coerce(columns)
|
|
19
|
+
case columns
|
|
20
|
+
when Symbol, String
|
|
21
|
+
FauxColumn.new(columns)
|
|
22
|
+
when Array
|
|
23
|
+
columns.collect { |col| FauxColumn.coerce(col) }
|
|
24
|
+
when FauxColumn
|
|
25
|
+
columns
|
|
26
|
+
else
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Can't use normal method name, as that could be an association or
|
|
32
|
+
# column name.
|
|
33
|
+
#
|
|
34
|
+
def __name
|
|
35
|
+
@name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Can't use normal method name, as that could be an association or
|
|
39
|
+
# column name.
|
|
40
|
+
#
|
|
41
|
+
def __stack
|
|
42
|
+
@stack
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns true if the stack is empty *and* if the name is a string -
|
|
46
|
+
# which is an indication that of raw SQL, as opposed to a value from a
|
|
47
|
+
# table's column.
|
|
48
|
+
#
|
|
49
|
+
def is_string?
|
|
50
|
+
@name.is_a?(String) && @stack.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# This handles any 'invalid' method calls and sets them as the name,
|
|
54
|
+
# and pushing the previous name into the stack. The object returns
|
|
55
|
+
# itself.
|
|
56
|
+
#
|
|
57
|
+
# If there's a single argument, it becomes the name, and the method
|
|
58
|
+
# symbol goes into the stack as well. Multiple arguments means new
|
|
59
|
+
# columns with the original stack and new names (from each argument) gets
|
|
60
|
+
# returned.
|
|
61
|
+
#
|
|
62
|
+
# Easier to explain with examples:
|
|
63
|
+
#
|
|
64
|
+
# col = FauxColumn.new :a, :b, :c
|
|
65
|
+
# col.__name #=> :c
|
|
66
|
+
# col.__stack #=> [:a, :b]
|
|
67
|
+
#
|
|
68
|
+
# col.whatever #=> col
|
|
69
|
+
# col.__name #=> :whatever
|
|
70
|
+
# col.__stack #=> [:a, :b, :c]
|
|
71
|
+
#
|
|
72
|
+
# col.something(:id) #=> col
|
|
73
|
+
# col.__name #=> :id
|
|
74
|
+
# col.__stack #=> [:a, :b, :c, :whatever, :something]
|
|
75
|
+
#
|
|
76
|
+
# cols = col.short(:x, :y, :z)
|
|
77
|
+
# cols[0].__name #=> :x
|
|
78
|
+
# cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
|
|
79
|
+
# cols[1].__name #=> :y
|
|
80
|
+
# cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
|
|
81
|
+
# cols[2].__name #=> :z
|
|
82
|
+
# cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
|
|
83
|
+
#
|
|
84
|
+
# Also, this allows method chaining to build up a relevant stack:
|
|
85
|
+
#
|
|
86
|
+
# col = FauxColumn.new :a, :b
|
|
87
|
+
# col.__name #=> :b
|
|
88
|
+
# col.__stack #=> [:a]
|
|
89
|
+
#
|
|
90
|
+
# col.one.two.three #=> col
|
|
91
|
+
# col.__name #=> :three
|
|
92
|
+
# col.__stack #=> [:a, :b, :one, :two]
|
|
93
|
+
#
|
|
94
|
+
def method_missing(method, *args)
|
|
95
|
+
@stack << @name
|
|
96
|
+
@name = method
|
|
97
|
+
|
|
98
|
+
if (args.empty?)
|
|
99
|
+
self
|
|
100
|
+
elsif (args.length == 1)
|
|
101
|
+
method_missing(args.first)
|
|
102
|
+
else
|
|
103
|
+
args.collect { |arg|
|
|
104
|
+
FauxColumn.new(@stack + [@name, arg])
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
@alias = @alias.to_sym unless @alias.blank?
|
|
18
|
+
|
|
19
|
+
@columns.each { |col|
|
|
20
|
+
@associations[col] = association_stack(col.__stack.clone).each { |assoc|
|
|
21
|
+
assoc.join_to(source.base)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the unique name of the attribute - which is either the alias of
|
|
27
|
+
# the attribute, or the name of the only column - if there is only one. If
|
|
28
|
+
# there isn't, there should be an alias. Else things probably won't work.
|
|
29
|
+
# Consider yourself warned.
|
|
30
|
+
#
|
|
31
|
+
def unique_name
|
|
32
|
+
if @columns.length == 1
|
|
33
|
+
@alias || @columns.first.__name
|
|
34
|
+
else
|
|
35
|
+
@alias
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_facet
|
|
40
|
+
return nil unless @faceted
|
|
41
|
+
|
|
42
|
+
ThinkingSphinx::Facet.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get the part of the GROUP BY clause related to this attribute - if one is
|
|
46
|
+
# needed. If not, all you'll get back is nil. The latter will happen if
|
|
47
|
+
# there isn't actually a real column to get data from, or if there's
|
|
48
|
+
# multiple data values (read: a has_many or has_and_belongs_to_many
|
|
49
|
+
# association).
|
|
50
|
+
#
|
|
51
|
+
def to_group_sql
|
|
52
|
+
case
|
|
53
|
+
when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
|
|
54
|
+
nil
|
|
55
|
+
else
|
|
56
|
+
@columns.collect { |column|
|
|
57
|
+
column_with_prefix(column)
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def changed?(instance)
|
|
63
|
+
return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
|
|
64
|
+
|
|
65
|
+
!@columns.all? { |col|
|
|
66
|
+
instance.respond_to?("#{col.__name.to_s}_changed?") &&
|
|
67
|
+
!instance.send("#{col.__name.to_s}_changed?")
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def admin?
|
|
72
|
+
admin
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def public?
|
|
76
|
+
!admin
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Could there be more than one value related to the parent record? If so,
|
|
82
|
+
# then this will return true. If not, false. It's that simple.
|
|
83
|
+
#
|
|
84
|
+
def is_many?
|
|
85
|
+
associations.values.flatten.any? { |assoc| assoc.is_many? }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns true if any of the columns are string values, instead of database
|
|
89
|
+
# column references.
|
|
90
|
+
def is_string?
|
|
91
|
+
columns.all? { |col| col.is_string? }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def adapter
|
|
95
|
+
@adapter ||= @model.sphinx_database_adapter
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def quote_with_table(table, column)
|
|
99
|
+
"#{quote_table_name(table)}.#{quote_column(column)}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def quote_column(column)
|
|
103
|
+
@model.connection.quote_column_name(column)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def quote_table_name(table_name)
|
|
107
|
+
@model.connection.quote_table_name(table_name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Indication of whether the columns should be concatenated with a space
|
|
111
|
+
# between each value. True if there's either multiple sources or multiple
|
|
112
|
+
# associations.
|
|
113
|
+
#
|
|
114
|
+
def concat_ws?
|
|
115
|
+
multiple_associations? || @columns.length > 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Checks whether any column requires multiple associations (which only
|
|
119
|
+
# happens for polymorphic situations).
|
|
120
|
+
#
|
|
121
|
+
def multiple_associations?
|
|
122
|
+
associations.any? { |col,assocs| assocs.length > 1 }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Builds a column reference tied to the appropriate associations. This
|
|
126
|
+
# dives into the associations hash and their corresponding joins to
|
|
127
|
+
# figure out how to correctly reference a column in SQL.
|
|
128
|
+
#
|
|
129
|
+
def column_with_prefix(column)
|
|
130
|
+
if column.is_string?
|
|
131
|
+
column.__name
|
|
132
|
+
elsif associations[column].empty?
|
|
133
|
+
"#{@model.quoted_table_name}.#{quote_column(column.__name)}"
|
|
134
|
+
else
|
|
135
|
+
associations[column].collect { |assoc|
|
|
136
|
+
assoc.has_column?(column.__name) ?
|
|
137
|
+
"#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
|
|
138
|
+
nil
|
|
139
|
+
}.compact.join(', ')
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Gets a stack of associations for a specific path.
|
|
144
|
+
#
|
|
145
|
+
def association_stack(path, parent = nil)
|
|
146
|
+
assocs = []
|
|
147
|
+
|
|
148
|
+
if parent.nil?
|
|
149
|
+
assocs = @source.association(path.shift)
|
|
150
|
+
else
|
|
151
|
+
assocs = parent.children(path.shift)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
until path.empty?
|
|
155
|
+
point = path.shift
|
|
156
|
+
assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
assocs
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module HashExcept
|
|
3
|
+
# Returns a new hash without the given keys.
|
|
4
|
+
def except(*keys)
|
|
5
|
+
rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
|
|
6
|
+
reject { |key,| rejected.include?(key) }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Replaces the hash without only the given keys.
|
|
10
|
+
def except!(*keys)
|
|
11
|
+
replace(except(*keys))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Hash.send(
|
|
17
|
+
:include, ThinkingSphinx::HashExcept
|
|
18
|
+
) unless Hash.instance_methods.include?("except")
|
|
19
|
+
|
|
20
|
+
module ThinkingSphinx
|
|
21
|
+
module ArrayExtractOptions
|
|
22
|
+
def extract_options!
|
|
23
|
+
last.is_a?(::Hash) ? pop : {}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Array.send(
|
|
29
|
+
:include, ThinkingSphinx::ArrayExtractOptions
|
|
30
|
+
) unless Array.instance_methods.include?("extract_options!")
|
|
31
|
+
|
|
32
|
+
module ThinkingSphinx
|
|
33
|
+
module AbstractQuotedTableName
|
|
34
|
+
def quote_table_name(name)
|
|
35
|
+
quote_column_name(name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
|
|
41
|
+
:include, ThinkingSphinx::AbstractQuotedTableName
|
|
42
|
+
) unless ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_methods.include?("quote_table_name")
|
|
43
|
+
|
|
44
|
+
module ThinkingSphinx
|
|
45
|
+
module MysqlQuotedTableName
|
|
46
|
+
def quote_table_name(name) #:nodoc:
|
|
47
|
+
quote_column_name(name).gsub('.', '`.`')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
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
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module ThinkingSphinx
|
|
62
|
+
module ActiveRecordQuotedName
|
|
63
|
+
def quoted_table_name
|
|
64
|
+
self.connection.quote_table_name(self.table_name)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
ActiveRecord::Base.extend(
|
|
70
|
+
ThinkingSphinx::ActiveRecordQuotedName
|
|
71
|
+
) unless ActiveRecord::Base.respond_to?("quoted_table_name")
|
|
72
|
+
|
|
73
|
+
module ThinkingSphinx
|
|
74
|
+
module ActiveRecordStoreFullSTIClass
|
|
75
|
+
def store_full_sti_class
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
ActiveRecord::Base.extend(
|
|
82
|
+
ThinkingSphinx::ActiveRecordStoreFullSTIClass
|
|
83
|
+
) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
|
|
84
|
+
|
|
85
|
+
module ThinkingSphinx
|
|
86
|
+
module ClassAttributeMethods
|
|
87
|
+
def cattr_reader(*syms)
|
|
88
|
+
syms.flatten.each do |sym|
|
|
89
|
+
next if sym.is_a?(Hash)
|
|
90
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
|
91
|
+
unless defined? @@#{sym}
|
|
92
|
+
@@#{sym} = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.#{sym}
|
|
96
|
+
@@#{sym}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def #{sym}
|
|
100
|
+
@@#{sym}
|
|
101
|
+
end
|
|
102
|
+
EOS
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def cattr_writer(*syms)
|
|
107
|
+
options = syms.extract_options!
|
|
108
|
+
syms.flatten.each do |sym|
|
|
109
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
|
110
|
+
unless defined? @@#{sym}
|
|
111
|
+
@@#{sym} = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.#{sym}=(obj)
|
|
115
|
+
@@#{sym} = obj
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#{"
|
|
119
|
+
def #{sym}=(obj)
|
|
120
|
+
@@#{sym} = obj
|
|
121
|
+
end
|
|
122
|
+
" unless options[:instance_writer] == false }
|
|
123
|
+
EOS
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def cattr_accessor(*syms)
|
|
128
|
+
cattr_reader(*syms)
|
|
129
|
+
cattr_writer(*syms)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Class.extend(
|
|
135
|
+
ThinkingSphinx::ClassAttributeMethods
|
|
136
|
+
) unless Class.respond_to?(:cattr_reader)
|
|
137
|
+
|
|
138
|
+
module ThinkingSphinx
|
|
139
|
+
module MetaClass
|
|
140
|
+
def metaclass
|
|
141
|
+
class << self
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
unless Object.new.respond_to?(:metaclass)
|
|
149
|
+
Object.send(:include, ThinkingSphinx::MetaClass)
|
|
150
|
+
end
|
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
module ThinkingSphinx
|
|
3
|
+
# Once you've got those indexes in and built, this is the stuff that
|
|
4
|
+
# matters - how to search! This class provides a generic search
|
|
5
|
+
# interface - which you can use to search all your indexed models at once.
|
|
6
|
+
# Most times, you will just want a specific model's results - to search and
|
|
7
|
+
# search_for_ids methods will do the job in exactly the same manner when
|
|
8
|
+
# called from a model.
|
|
9
|
+
#
|
|
10
|
+
class Search
|
|
11
|
+
CoreMethods = %w( == class class_eval extend frozen? id instance_eval
|
|
12
|
+
instance_of? instance_values instance_variable_defined?
|
|
13
|
+
instance_variable_get instance_variable_set instance_variables is_a?
|
|
14
|
+
kind_of? member? method methods nil? object_id respond_to? send should
|
|
15
|
+
type )
|
|
16
|
+
SafeMethods = %w( partition private_methods protected_methods
|
|
17
|
+
public_methods send )
|
|
18
|
+
|
|
19
|
+
instance_methods.select { |method|
|
|
20
|
+
method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
|
|
21
|
+
}.each { |method|
|
|
22
|
+
undef_method method
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
HashOptions = [:conditions, :with, :without, :with_all]
|
|
26
|
+
ArrayOptions = [:classes, :without_ids]
|
|
27
|
+
|
|
28
|
+
attr_reader :args, :options
|
|
29
|
+
|
|
30
|
+
# Deprecated. Use ThinkingSphinx.search
|
|
31
|
+
def self.search(*args)
|
|
32
|
+
log 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
|
|
33
|
+
ThinkingSphinx.search *args
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Deprecated. Use ThinkingSphinx.search_for_ids
|
|
37
|
+
def self.search_for_ids(*args)
|
|
38
|
+
log 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
|
|
39
|
+
ThinkingSphinx.search_for_ids *args
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Deprecated. Use ThinkingSphinx.search_for_ids
|
|
43
|
+
def self.search_for_id(*args)
|
|
44
|
+
log 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
|
|
45
|
+
ThinkingSphinx.search_for_id *args
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Deprecated. Use ThinkingSphinx.count
|
|
49
|
+
def self.count(*args)
|
|
50
|
+
log 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
|
|
51
|
+
ThinkingSphinx.count *args
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Deprecated. Use ThinkingSphinx.facets
|
|
55
|
+
def self.facets(*args)
|
|
56
|
+
log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
|
|
57
|
+
ThinkingSphinx.facets *args
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(*args)
|
|
61
|
+
@array = []
|
|
62
|
+
@options = args.extract_options!
|
|
63
|
+
@args = args
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_a
|
|
67
|
+
populate
|
|
68
|
+
@array
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Indication of whether the request has been made to Sphinx for the search
|
|
72
|
+
# query.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] true if the results have been requested.
|
|
75
|
+
#
|
|
76
|
+
def populated?
|
|
77
|
+
!!@populated
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The query result hash from Riddle.
|
|
81
|
+
#
|
|
82
|
+
# @return [Hash] Raw Sphinx results
|
|
83
|
+
#
|
|
84
|
+
def results
|
|
85
|
+
populate
|
|
86
|
+
@results
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def method_missing(method, *args, &block)
|
|
90
|
+
if is_scope?(method)
|
|
91
|
+
add_scope(method, *args, &block)
|
|
92
|
+
return self
|
|
93
|
+
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
|
94
|
+
super
|
|
95
|
+
elsif !SafeMethods.include?(method.to_s)
|
|
96
|
+
populate
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
|
|
100
|
+
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
|
|
101
|
+
else
|
|
102
|
+
@array.send(method, *args, &block)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns true if the Search object or the underlying Array object respond
|
|
107
|
+
# to the requested method.
|
|
108
|
+
#
|
|
109
|
+
# @param [Symbol] method The method name
|
|
110
|
+
# @return [Boolean] true if either Search or Array responds to the method.
|
|
111
|
+
#
|
|
112
|
+
def respond_to?(method)
|
|
113
|
+
super || @array.respond_to?(method)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The current page number of the result set. Defaults to 1 if no page was
|
|
117
|
+
# explicitly requested.
|
|
118
|
+
#
|
|
119
|
+
# @return [Integer]
|
|
120
|
+
#
|
|
121
|
+
def current_page
|
|
122
|
+
@options[:page].blank? ? 1 : @options[:page].to_i
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The next page number of the result set. If there are no more pages
|
|
126
|
+
# available, nil is returned.
|
|
127
|
+
#
|
|
128
|
+
# @return [Integer, nil]
|
|
129
|
+
#
|
|
130
|
+
def next_page
|
|
131
|
+
current_page >= total_pages ? nil : current_page + 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The previous page number of the result set. If this is the first page,
|
|
135
|
+
# then nil is returned.
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer, nil]
|
|
138
|
+
#
|
|
139
|
+
def previous_page
|
|
140
|
+
current_page == 1 ? nil : current_page - 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The amount of records per set of paged results. Defaults to 20 unless a
|
|
144
|
+
# specific page size is requested.
|
|
145
|
+
#
|
|
146
|
+
# @return [Integer]
|
|
147
|
+
#
|
|
148
|
+
def per_page
|
|
149
|
+
@options[:limit] || @options[:per_page] || 20
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The total number of pages available if the results are paginated.
|
|
153
|
+
#
|
|
154
|
+
# @return [Integer]
|
|
155
|
+
#
|
|
156
|
+
def total_pages
|
|
157
|
+
populate
|
|
158
|
+
@total_pages ||= (@results[:total] / per_page.to_f).ceil
|
|
159
|
+
end
|
|
160
|
+
# Compatibility with older versions of will_paginate
|
|
161
|
+
alias_method :page_count, :total_pages
|
|
162
|
+
|
|
163
|
+
# The total number of search results available.
|
|
164
|
+
#
|
|
165
|
+
# @return [Integer]
|
|
166
|
+
#
|
|
167
|
+
def total_entries
|
|
168
|
+
populate
|
|
169
|
+
@total_entries ||= @results[:total_found]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The current page's offset, based on the number of records per page.
|
|
173
|
+
#
|
|
174
|
+
# @return [Integer]
|
|
175
|
+
#
|
|
176
|
+
def offset
|
|
177
|
+
(current_page - 1) * per_page
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def indexes
|
|
181
|
+
return options[:index] if options[:index]
|
|
182
|
+
return '*' if classes.empty?
|
|
183
|
+
|
|
184
|
+
classes.collect { |klass| klass.sphinx_index_names }.flatten.join(',')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def each_with_groupby_and_count(&block)
|
|
188
|
+
populate
|
|
189
|
+
results[:matches].each_with_index do |match, index|
|
|
190
|
+
yield self[index],
|
|
191
|
+
match[:attributes]["@groupby"],
|
|
192
|
+
match[:attributes]["@count"]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def each_with_weighting(&block)
|
|
197
|
+
populate
|
|
198
|
+
results[:matches].each_with_index do |match, index|
|
|
199
|
+
yield self[index], match[:weight]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def excerpt_for(string, model = nil)
|
|
204
|
+
if model.nil? && one_class
|
|
205
|
+
model ||= one_class
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
populate
|
|
209
|
+
client.excerpts(
|
|
210
|
+
:docs => [string],
|
|
211
|
+
:words => results[:words].keys.join(' '),
|
|
212
|
+
:index => "#{model.source_of_sphinx_index.sphinx_name}_core"
|
|
213
|
+
).first
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def search(*args)
|
|
217
|
+
merge_search ThinkingSphinx::Search.new(*args)
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def config
|
|
224
|
+
ThinkingSphinx::Configuration.instance
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def populate
|
|
228
|
+
return if @populated
|
|
229
|
+
@populated = true
|
|
230
|
+
|
|
231
|
+
retry_on_stale_index do
|
|
232
|
+
begin
|
|
233
|
+
log "Querying Sphinx: #{query}"
|
|
234
|
+
@results = client.query query, indexes, comment
|
|
235
|
+
rescue Errno::ECONNREFUSED => err
|
|
236
|
+
raise ThinkingSphinx::ConnectionError,
|
|
237
|
+
'Connection to Sphinx Daemon (searchd) failed.'
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if options[:ids_only]
|
|
241
|
+
replace @results[:matches].collect { |match|
|
|
242
|
+
match[:attributes]["sphinx_internal_id"]
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
replace instances_from_matches
|
|
246
|
+
add_excerpter
|
|
247
|
+
add_sphinx_attributes
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def add_excerpter
|
|
253
|
+
each do |object|
|
|
254
|
+
next if object.respond_to?(:excerpts)
|
|
255
|
+
|
|
256
|
+
excerpter = ThinkingSphinx::Excerpter.new self, object
|
|
257
|
+
block = lambda { excerpter }
|
|
258
|
+
|
|
259
|
+
object.metaclass.instance_eval do
|
|
260
|
+
define_method(:excerpts, &block)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def add_sphinx_attributes
|
|
266
|
+
each do |object|
|
|
267
|
+
next if object.nil? || object.respond_to?(:sphinx_attributes)
|
|
268
|
+
|
|
269
|
+
match = @results[:matches].detect { |match|
|
|
270
|
+
match[:attributes]['sphinx_internal_id'] == object.
|
|
271
|
+
primary_key_for_sphinx &&
|
|
272
|
+
match[:attributes]['class_crc'] == object.class.to_crc32
|
|
273
|
+
}
|
|
274
|
+
next if match.nil?
|
|
275
|
+
|
|
276
|
+
object.metaclass.instance_eval do
|
|
277
|
+
define_method(:sphinx_attributes) { match[:attributes] }
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def self.log(message, method = :debug)
|
|
283
|
+
return if ::ActiveRecord::Base.logger.nil?
|
|
284
|
+
::ActiveRecord::Base.logger.send method, message
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def log(message, method = :debug)
|
|
288
|
+
self.class.log(message, method)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def client
|
|
292
|
+
client = config.client
|
|
293
|
+
|
|
294
|
+
index_options = one_class ?
|
|
295
|
+
one_class.sphinx_indexes.first.local_options : {}
|
|
296
|
+
|
|
297
|
+
[
|
|
298
|
+
:max_matches, :group_by, :group_function, :group_clause,
|
|
299
|
+
:group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
|
|
300
|
+
:rank_mode, :max_query_time, :field_weights
|
|
301
|
+
].each do |key|
|
|
302
|
+
# puts "key: #{key}"
|
|
303
|
+
value = options[key] || index_options[key]
|
|
304
|
+
# puts "value: #{value.inspect}"
|
|
305
|
+
client.send("#{key}=", value) if value
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
client.limit = per_page
|
|
309
|
+
client.offset = offset
|
|
310
|
+
client.match_mode = match_mode
|
|
311
|
+
client.filters = filters
|
|
312
|
+
client.sort_mode = sort_mode
|
|
313
|
+
client.sort_by = sort_by
|
|
314
|
+
client.group_by = group_by if group_by
|
|
315
|
+
client.group_function = group_function if group_function
|
|
316
|
+
client.index_weights = index_weights
|
|
317
|
+
client.anchor = anchor
|
|
318
|
+
|
|
319
|
+
client
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def retry_on_stale_index(&block)
|
|
323
|
+
stale_ids = []
|
|
324
|
+
retries = stale_retries
|
|
325
|
+
|
|
326
|
+
begin
|
|
327
|
+
options[:raise_on_stale] = retries > 0
|
|
328
|
+
block.call
|
|
329
|
+
|
|
330
|
+
# If ThinkingSphinx::Search#instances_from_matches found records in
|
|
331
|
+
# Sphinx but not in the DB and the :raise_on_stale option is set, this
|
|
332
|
+
# exception is raised. We retry a limited number of times, excluding the
|
|
333
|
+
# stale ids from the search.
|
|
334
|
+
rescue StaleIdsException => err
|
|
335
|
+
retries -= 1
|
|
336
|
+
|
|
337
|
+
# For logging
|
|
338
|
+
stale_ids |= err.ids
|
|
339
|
+
# ID exclusion
|
|
340
|
+
options[:without_ids] = Array(options[:without_ids]) | err.ids
|
|
341
|
+
|
|
342
|
+
log 'Sphinx Stale Ids (%s %s left): %s' % [
|
|
343
|
+
retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
|
|
344
|
+
]
|
|
345
|
+
retry
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def classes
|
|
350
|
+
@classes ||= options[:classes] || []
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def one_class
|
|
354
|
+
@one_class ||= classes.length != 1 ? nil : classes.first
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def query
|
|
358
|
+
@query ||= begin
|
|
359
|
+
q = @args.join(' ') << conditions_as_query
|
|
360
|
+
(options[:star] ? star_query(q) : q).strip
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def conditions_as_query
|
|
365
|
+
return '' if @options[:conditions].blank?
|
|
366
|
+
|
|
367
|
+
# Soon to be deprecated.
|
|
368
|
+
keys = @options[:conditions].keys.reject { |key|
|
|
369
|
+
attributes.include?(key.to_sym)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
' ' + keys.collect { |key|
|
|
373
|
+
"@#{key} #{options[:conditions][key]}"
|
|
374
|
+
}.join(' ')
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def star_query(query)
|
|
378
|
+
token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
|
|
379
|
+
|
|
380
|
+
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
|
381
|
+
pre, proper, post = $`, $&, $'
|
|
382
|
+
# E.g. "@foo", "/2", "~3", but not as part of a token
|
|
383
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
|
384
|
+
# E.g. "foo bar", with quotes
|
|
385
|
+
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
|
386
|
+
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
|
387
|
+
if is_operator || is_quote || has_star
|
|
388
|
+
proper
|
|
389
|
+
else
|
|
390
|
+
"*#{proper}*"
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def comment
|
|
396
|
+
options[:comment] || ''
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def match_mode
|
|
400
|
+
options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def sort_mode
|
|
404
|
+
@sort_mode ||= case options[:sort_mode]
|
|
405
|
+
when :asc
|
|
406
|
+
:attr_asc
|
|
407
|
+
when :desc
|
|
408
|
+
:attr_desc
|
|
409
|
+
when nil
|
|
410
|
+
case options[:order]
|
|
411
|
+
when String
|
|
412
|
+
:extended
|
|
413
|
+
when Symbol
|
|
414
|
+
:attr_asc
|
|
415
|
+
else
|
|
416
|
+
:relevance
|
|
417
|
+
end
|
|
418
|
+
else
|
|
419
|
+
options[:sort_mode]
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def sort_by
|
|
424
|
+
case @sort_by = (options[:sort_by] || options[:order])
|
|
425
|
+
when String
|
|
426
|
+
sorted_fields_to_attributes(@sort_by)
|
|
427
|
+
when Symbol
|
|
428
|
+
field_names.include?(@sort_by) ?
|
|
429
|
+
@sort_by.to_s.concat('_sort') : @sort_by.to_s
|
|
430
|
+
else
|
|
431
|
+
''
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def field_names
|
|
436
|
+
return [] unless one_class
|
|
437
|
+
|
|
438
|
+
one_class.sphinx_indexes.collect { |index|
|
|
439
|
+
index.fields.collect { |field| field.unique_name }
|
|
440
|
+
}.flatten
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def sorted_fields_to_attributes(order_string)
|
|
444
|
+
field_names.each { |field|
|
|
445
|
+
order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
|
446
|
+
match.gsub field.to_s, field.to_s.concat("_sort")
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
order_string
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
|
|
454
|
+
# { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
|
455
|
+
#
|
|
456
|
+
def index_weights
|
|
457
|
+
weights = options[:index_weights] || {}
|
|
458
|
+
weights.keys.inject({}) do |hash, key|
|
|
459
|
+
if key.is_a?(Class)
|
|
460
|
+
name = ThinkingSphinx::Index.name_for(key)
|
|
461
|
+
hash["#{name}_core"] = weights[key]
|
|
462
|
+
hash["#{name}_delta"] = weights[key]
|
|
463
|
+
else
|
|
464
|
+
hash[key] = weights[key]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
hash
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def group_by
|
|
472
|
+
options[:group] ? options[:group].to_s : nil
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def group_function
|
|
476
|
+
options[:group] ? :attr : nil
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def internal_filters
|
|
480
|
+
filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
|
|
481
|
+
|
|
482
|
+
class_crcs = classes.collect { |klass|
|
|
483
|
+
klass.to_crc32s
|
|
484
|
+
}.flatten
|
|
485
|
+
|
|
486
|
+
unless class_crcs.empty?
|
|
487
|
+
filters << Riddle::Client::Filter.new('class_crc', class_crcs)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
filters << Riddle::Client::Filter.new(
|
|
491
|
+
'sphinx_internal_id', filter_value(options[:without_ids]), true
|
|
492
|
+
) if options[:without_ids]
|
|
493
|
+
|
|
494
|
+
filters
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def condition_filters
|
|
498
|
+
(options[:conditions] || {}).collect { |attrib, value|
|
|
499
|
+
if attributes.include?(attrib.to_sym)
|
|
500
|
+
puts <<-MSG
|
|
501
|
+
Deprecation Warning: filters on attributes should be done using the :with
|
|
502
|
+
option, not :conditions. For example:
|
|
503
|
+
:with => {:#{attrib} => #{value.inspect}}
|
|
504
|
+
MSG
|
|
505
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
506
|
+
else
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
}.compact
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def filters
|
|
513
|
+
internal_filters +
|
|
514
|
+
condition_filters +
|
|
515
|
+
(options[:with] || {}).collect { |attrib, value|
|
|
516
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
517
|
+
} +
|
|
518
|
+
(options[:without] || {}).collect { |attrib, value|
|
|
519
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
|
520
|
+
} +
|
|
521
|
+
(options[:with_all] || {}).collect { |attrib, values|
|
|
522
|
+
Array(values).collect { |value|
|
|
523
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
524
|
+
}
|
|
525
|
+
}.flatten
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# When passed a Time instance, returns the integer timestamp.
|
|
529
|
+
#
|
|
530
|
+
# If using Rails 2.1+, need to handle timezones to translate them back to
|
|
531
|
+
# UTC, as that's what datetimes will be stored as by MySQL.
|
|
532
|
+
#
|
|
533
|
+
# in_time_zone is a method that was added for the timezone support in
|
|
534
|
+
# Rails 2.1, which is why it's used for testing. I'm sure there's better
|
|
535
|
+
# ways, but this does the job.
|
|
536
|
+
#
|
|
537
|
+
def filter_value(value)
|
|
538
|
+
case value
|
|
539
|
+
when Range
|
|
540
|
+
filter_value(value.first).first..filter_value(value.last).first
|
|
541
|
+
when Array
|
|
542
|
+
value.collect { |v| filter_value(v) }.flatten
|
|
543
|
+
when Time
|
|
544
|
+
value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
|
|
545
|
+
when NilClass
|
|
546
|
+
0
|
|
547
|
+
else
|
|
548
|
+
Array(value)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def anchor
|
|
553
|
+
return {} unless options[:geo] || (options[:lat] && options[:lng])
|
|
554
|
+
|
|
555
|
+
{
|
|
556
|
+
:latitude => options[:geo] ? options[:geo].first : options[:lat],
|
|
557
|
+
:longitude => options[:geo] ? options[:geo].last : options[:lng],
|
|
558
|
+
:latitude_attribute => latitude_attr.to_s,
|
|
559
|
+
:longitude_attribute => longitude_attr.to_s
|
|
560
|
+
}
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def latitude_attr
|
|
564
|
+
options[:latitude_attr] ||
|
|
565
|
+
index_option(:latitude_attr) ||
|
|
566
|
+
attribute(:lat, :latitude)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def longitude_attr
|
|
570
|
+
options[:longitude_attr] ||
|
|
571
|
+
index_option(:longitude_attr) ||
|
|
572
|
+
attribute(:lon, :lng, :longitude)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def index_option(key)
|
|
576
|
+
return nil unless one_class
|
|
577
|
+
|
|
578
|
+
one_class.sphinx_indexes.collect { |index|
|
|
579
|
+
index.local_options[key]
|
|
580
|
+
}.compact.first
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def attribute(*keys)
|
|
584
|
+
return nil unless one_class
|
|
585
|
+
|
|
586
|
+
keys.detect { |key|
|
|
587
|
+
attributes.include?(key)
|
|
588
|
+
}
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def attributes
|
|
592
|
+
return [] unless one_class
|
|
593
|
+
|
|
594
|
+
attributes = one_class.sphinx_indexes.collect { |index|
|
|
595
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
|
596
|
+
}.flatten
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def stale_retries
|
|
600
|
+
case options[:retry_stale]
|
|
601
|
+
when TrueClass
|
|
602
|
+
3
|
|
603
|
+
when nil, FalseClass
|
|
604
|
+
0
|
|
605
|
+
else
|
|
606
|
+
options[:retry_stale].to_i
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def instances_from_class(klass, matches)
|
|
611
|
+
index_options = klass.sphinx_index_options
|
|
612
|
+
|
|
613
|
+
ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
|
|
614
|
+
instances = ids.length > 0 ? klass.find(
|
|
615
|
+
:all,
|
|
616
|
+
:joins => options[:joins],
|
|
617
|
+
:conditions => {klass.primary_key_for_sphinx.to_sym => ids},
|
|
618
|
+
:include => (options[:include] || index_options[:include]),
|
|
619
|
+
:select => (options[:select] || index_options[:select]),
|
|
620
|
+
:order => (options[:sql_order] || index_options[:sql_order])
|
|
621
|
+
) : []
|
|
622
|
+
|
|
623
|
+
# Raise an exception if we find records in Sphinx but not in the DB, so
|
|
624
|
+
# the search method can retry without them. See
|
|
625
|
+
# ThinkingSphinx::Search.retry_search_on_stale_index.
|
|
626
|
+
if options[:raise_on_stale] && instances.length < ids.length
|
|
627
|
+
stale_ids = ids - instances.map { |i| i.id }
|
|
628
|
+
raise StaleIdsException, stale_ids
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# if the user has specified an SQL order, return the collection
|
|
632
|
+
# without rearranging it into the Sphinx order
|
|
633
|
+
return instances if (options[:sql_order] || index_options[:sql_order])
|
|
634
|
+
|
|
635
|
+
ids.collect { |obj_id|
|
|
636
|
+
instances.detect do |obj|
|
|
637
|
+
obj.primary_key_for_sphinx == obj_id
|
|
638
|
+
end
|
|
639
|
+
}
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Group results by class and call #find(:all) once for each group to reduce
|
|
643
|
+
# the number of #find's in multi-model searches.
|
|
644
|
+
#
|
|
645
|
+
def instances_from_matches
|
|
646
|
+
return single_class_results if one_class
|
|
647
|
+
|
|
648
|
+
groups = results[:matches].group_by { |match|
|
|
649
|
+
match[:attributes]["class_crc"]
|
|
650
|
+
}
|
|
651
|
+
groups.each do |crc, group|
|
|
652
|
+
group.replace(
|
|
653
|
+
instances_from_class(class_from_crc(crc), group)
|
|
654
|
+
)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
results[:matches].collect do |match|
|
|
658
|
+
groups.detect { |crc, group|
|
|
659
|
+
crc == match[:attributes]["class_crc"]
|
|
660
|
+
}[1].compact.detect { |obj|
|
|
661
|
+
obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
|
|
662
|
+
}
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def single_class_results
|
|
667
|
+
instances_from_class one_class, results[:matches]
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def class_from_crc(crc)
|
|
671
|
+
config.models_by_crc[crc].constantize
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def each_with_attribute(attribute, &block)
|
|
675
|
+
populate
|
|
676
|
+
results[:matches].each_with_index do |match, index|
|
|
677
|
+
yield self[index],
|
|
678
|
+
(match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def is_scope?(method)
|
|
683
|
+
one_class && one_class.sphinx_scopes.include?(method)
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def add_scope(method, *args, &block)
|
|
687
|
+
merge_search one_class.send(method, *args, &block)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def merge_search(search)
|
|
691
|
+
search.args.each { |arg| args << arg }
|
|
692
|
+
|
|
693
|
+
search.options.keys.each do |key|
|
|
694
|
+
if HashOptions.include?(key)
|
|
695
|
+
options[key] ||= {}
|
|
696
|
+
options[key].merge! search.options[key]
|
|
697
|
+
elsif ArrayOptions.include?(key)
|
|
698
|
+
options[key] ||= []
|
|
699
|
+
options[key] += search.options[key]
|
|
700
|
+
options[key].uniq!
|
|
701
|
+
else
|
|
702
|
+
options[key] = search.options[key]
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|