thinking-sphinx 1.5.0 → 2.0.0.rc1
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 +15 -48
- data/VERSION +1 -0
- data/features/attribute_transformation.feature +7 -7
- data/features/attribute_updates.feature +16 -18
- data/features/deleting_instances.feature +13 -16
- data/features/excerpts.feature +0 -8
- data/features/facets.feature +19 -25
- data/features/handling_edits.feature +20 -25
- data/features/searching_across_models.feature +1 -1
- data/features/searching_by_index.feature +5 -6
- data/features/searching_by_model.feature +29 -29
- data/features/sphinx_scopes.feature +0 -26
- data/features/step_definitions/common_steps.rb +6 -18
- data/features/step_definitions/scope_steps.rb +0 -4
- data/features/step_definitions/search_steps.rb +4 -9
- data/features/support/env.rb +10 -3
- data/features/thinking_sphinx/db/fixtures/alphas.rb +10 -8
- data/features/thinking_sphinx/db/fixtures/cats.rb +1 -1
- data/features/thinking_sphinx/db/fixtures/dogs.rb +1 -1
- data/features/thinking_sphinx/db/fixtures/foxes.rb +1 -1
- data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
- data/features/thinking_sphinx/db/fixtures/posts.rb +1 -5
- data/features/thinking_sphinx/db/migrations/create_posts.rb +0 -1
- data/features/thinking_sphinx/models/alpha.rb +0 -1
- data/features/thinking_sphinx/models/beta.rb +0 -5
- data/features/thinking_sphinx/models/developer.rb +1 -6
- data/features/thinking_sphinx/models/music.rb +1 -3
- data/features/thinking_sphinx/models/person.rb +1 -2
- data/features/thinking_sphinx/models/post.rb +0 -1
- data/lib/cucumber/thinking_sphinx/external_world.rb +4 -8
- data/lib/cucumber/thinking_sphinx/internal_world.rb +27 -36
- data/lib/thinking_sphinx.rb +60 -132
- data/lib/thinking_sphinx/active_record.rb +98 -124
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +13 -17
- data/lib/thinking_sphinx/active_record/delta.rb +15 -21
- data/lib/thinking_sphinx/active_record/has_many_association.rb +23 -16
- data/lib/thinking_sphinx/active_record/scopes.rb +0 -18
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +15 -63
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +0 -4
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +24 -65
- data/lib/thinking_sphinx/association.rb +11 -36
- data/lib/thinking_sphinx/attribute.rb +85 -92
- data/lib/thinking_sphinx/auto_version.rb +3 -21
- data/lib/thinking_sphinx/class_facet.rb +3 -8
- data/lib/thinking_sphinx/configuration.rb +58 -114
- data/lib/thinking_sphinx/context.rb +20 -22
- data/lib/thinking_sphinx/core/array.rb +13 -0
- data/lib/thinking_sphinx/deltas.rb +0 -2
- data/lib/thinking_sphinx/deltas/default_delta.rb +22 -18
- data/lib/thinking_sphinx/deploy/capistrano.rb +31 -30
- data/lib/thinking_sphinx/excerpter.rb +1 -2
- data/lib/thinking_sphinx/facet.rb +35 -45
- data/lib/thinking_sphinx/facet_search.rb +24 -58
- data/lib/thinking_sphinx/field.rb +0 -18
- data/lib/thinking_sphinx/index.rb +36 -38
- data/lib/thinking_sphinx/index/builder.rb +59 -74
- data/lib/thinking_sphinx/property.rb +45 -66
- data/lib/thinking_sphinx/railtie.rb +35 -0
- data/lib/thinking_sphinx/search.rb +250 -506
- data/lib/thinking_sphinx/source.rb +31 -50
- data/lib/thinking_sphinx/source/internal_properties.rb +3 -8
- data/lib/thinking_sphinx/source/sql.rb +31 -71
- data/lib/thinking_sphinx/tasks.rb +27 -48
- data/spec/thinking_sphinx/active_record/delta_spec.rb +41 -36
- data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +0 -96
- data/spec/thinking_sphinx/active_record/scopes_spec.rb +29 -29
- data/spec/thinking_sphinx/active_record_spec.rb +169 -140
- data/spec/thinking_sphinx/association_spec.rb +2 -20
- data/spec/thinking_sphinx/attribute_spec.rb +97 -101
- data/spec/thinking_sphinx/auto_version_spec.rb +11 -75
- data/spec/thinking_sphinx/configuration_spec.rb +62 -63
- data/spec/thinking_sphinx/context_spec.rb +66 -66
- data/spec/thinking_sphinx/facet_search_spec.rb +99 -99
- data/spec/thinking_sphinx/facet_spec.rb +4 -30
- data/spec/thinking_sphinx/field_spec.rb +3 -17
- data/spec/thinking_sphinx/index/builder_spec.rb +132 -169
- data/spec/thinking_sphinx/index_spec.rb +39 -45
- data/spec/thinking_sphinx/search_methods_spec.rb +33 -37
- data/spec/thinking_sphinx/search_spec.rb +269 -491
- data/spec/thinking_sphinx/source_spec.rb +48 -62
- data/spec/thinking_sphinx_spec.rb +49 -49
- data/tasks/distribution.rb +46 -0
- data/tasks/testing.rb +74 -0
- metadata +123 -199
- data/features/field_sorting.feature +0 -18
- data/features/thinking_sphinx/db/.gitignore +0 -1
- data/features/thinking_sphinx/db/fixtures/post_keywords.txt +0 -1
- data/features/thinking_sphinx/models/andrew.rb +0 -17
- data/lib/thinking-sphinx.rb +0 -1
- data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +0 -21
- data/lib/thinking_sphinx/bundled_search.rb +0 -40
- data/lib/thinking_sphinx/connection.rb +0 -71
- data/lib/thinking_sphinx/deltas/delete_job.rb +0 -16
- data/lib/thinking_sphinx/deltas/index_job.rb +0 -17
- data/lib/thinking_sphinx/rails_additions.rb +0 -181
- data/spec/fixtures/data.sql +0 -32
- data/spec/fixtures/database.yml.default +0 -3
- data/spec/fixtures/models.rb +0 -161
- data/spec/fixtures/structure.sql +0 -146
- data/spec/spec_helper.rb +0 -54
- data/spec/sphinx_helper.rb +0 -67
- data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +0 -163
- data/spec/thinking_sphinx/connection_spec.rb +0 -77
- data/spec/thinking_sphinx/rails_additions_spec.rb +0 -203
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module ThinkingSphinx
|
|
2
2
|
class Property
|
|
3
3
|
attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
def initialize(source, columns, options = {})
|
|
6
6
|
@source = source
|
|
7
7
|
@model = source.model
|
|
@@ -9,27 +9,25 @@ module ThinkingSphinx
|
|
|
9
9
|
@associations = {}
|
|
10
10
|
|
|
11
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
|
|
14
|
-
@faceted
|
|
15
|
-
@admin
|
|
16
|
-
|
|
17
|
-
@value_source = options[:value]
|
|
18
|
-
|
|
12
|
+
|
|
13
|
+
@alias = options[:as]
|
|
14
|
+
@faceted = options[:facet]
|
|
15
|
+
@admin = options[:admin]
|
|
16
|
+
|
|
19
17
|
@alias = @alias.to_sym unless @alias.blank?
|
|
20
|
-
|
|
18
|
+
|
|
21
19
|
@columns.each { |col|
|
|
22
|
-
@associations[col
|
|
20
|
+
@associations[col] = association_stack(col.__stack.clone).each { |assoc|
|
|
23
21
|
assoc.join_to(source.base)
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
end
|
|
27
|
-
|
|
25
|
+
|
|
28
26
|
# Returns the unique name of the attribute - which is either the alias of
|
|
29
27
|
# the attribute, or the name of the only column - if there is only one. If
|
|
30
28
|
# there isn't, there should be an alias. Else things probably won't work.
|
|
31
29
|
# Consider yourself warned.
|
|
32
|
-
#
|
|
30
|
+
#
|
|
33
31
|
def unique_name
|
|
34
32
|
if @columns.length == 1
|
|
35
33
|
@alias || @columns.first.__name
|
|
@@ -37,19 +35,19 @@ module ThinkingSphinx
|
|
|
37
35
|
@alias
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
|
-
|
|
38
|
+
|
|
41
39
|
def to_facet
|
|
42
40
|
return nil unless @faceted
|
|
43
|
-
|
|
44
|
-
ThinkingSphinx::Facet.new(self
|
|
41
|
+
|
|
42
|
+
ThinkingSphinx::Facet.new(self)
|
|
45
43
|
end
|
|
46
|
-
|
|
44
|
+
|
|
47
45
|
# Get the part of the GROUP BY clause related to this attribute - if one is
|
|
48
46
|
# needed. If not, all you'll get back is nil. The latter will happen if
|
|
49
47
|
# there isn't actually a real column to get data from, or if there's
|
|
50
48
|
# multiple data values (read: a has_many or has_and_belongs_to_many
|
|
51
49
|
# association).
|
|
52
|
-
#
|
|
50
|
+
#
|
|
53
51
|
def to_group_sql
|
|
54
52
|
case
|
|
55
53
|
when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
|
|
@@ -60,128 +58,109 @@ module ThinkingSphinx
|
|
|
60
58
|
}
|
|
61
59
|
end
|
|
62
60
|
end
|
|
63
|
-
|
|
61
|
+
|
|
64
62
|
def changed?(instance)
|
|
65
63
|
return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
instance.
|
|
69
|
-
!instance.send("#{col.__name.to_s}_changed?")
|
|
64
|
+
|
|
65
|
+
@columns.any? { |col|
|
|
66
|
+
instance.send("#{col.__name.to_s}_changed?")
|
|
70
67
|
}
|
|
71
68
|
end
|
|
72
|
-
|
|
69
|
+
|
|
73
70
|
def admin?
|
|
74
71
|
admin
|
|
75
72
|
end
|
|
76
|
-
|
|
73
|
+
|
|
77
74
|
def public?
|
|
78
75
|
!admin
|
|
79
76
|
end
|
|
80
|
-
|
|
81
|
-
def available?
|
|
82
|
-
columns.any? { |column| column_available?(column) }
|
|
83
|
-
end
|
|
84
|
-
|
|
77
|
+
|
|
85
78
|
private
|
|
86
|
-
|
|
79
|
+
|
|
87
80
|
# Could there be more than one value related to the parent record? If so,
|
|
88
81
|
# then this will return true. If not, false. It's that simple.
|
|
89
|
-
#
|
|
82
|
+
#
|
|
90
83
|
def is_many?
|
|
91
84
|
associations.values.flatten.any? { |assoc| assoc.is_many? }
|
|
92
85
|
end
|
|
93
|
-
|
|
86
|
+
|
|
94
87
|
# Returns true if any of the columns are string values, instead of database
|
|
95
88
|
# column references.
|
|
96
89
|
def is_string?
|
|
97
90
|
columns.all? { |col| col.is_string? }
|
|
98
91
|
end
|
|
99
|
-
|
|
92
|
+
|
|
100
93
|
def adapter
|
|
101
94
|
@adapter ||= @model.sphinx_database_adapter
|
|
102
95
|
end
|
|
103
|
-
|
|
96
|
+
|
|
104
97
|
def quote_with_table(table, column)
|
|
105
98
|
"#{quote_table_name(table)}.#{quote_column(column)}"
|
|
106
99
|
end
|
|
107
|
-
|
|
100
|
+
|
|
108
101
|
def quote_column(column)
|
|
109
102
|
@model.connection.quote_column_name(column)
|
|
110
103
|
end
|
|
111
|
-
|
|
104
|
+
|
|
112
105
|
def quote_table_name(table_name)
|
|
113
106
|
@model.connection.quote_table_name(table_name)
|
|
114
107
|
end
|
|
115
|
-
|
|
108
|
+
|
|
116
109
|
# Indication of whether the columns should be concatenated with a space
|
|
117
110
|
# between each value. True if there's either multiple sources or multiple
|
|
118
111
|
# associations.
|
|
119
|
-
#
|
|
112
|
+
#
|
|
120
113
|
def concat_ws?
|
|
121
114
|
multiple_associations? || @columns.length > 1
|
|
122
115
|
end
|
|
123
|
-
|
|
116
|
+
|
|
124
117
|
# Checks whether any column requires multiple associations (which only
|
|
125
118
|
# happens for polymorphic situations).
|
|
126
|
-
#
|
|
119
|
+
#
|
|
127
120
|
def multiple_associations?
|
|
128
|
-
associations.
|
|
121
|
+
associations.any? { |col,assocs| assocs.length > 1 }
|
|
129
122
|
end
|
|
130
|
-
|
|
123
|
+
|
|
131
124
|
# Builds a column reference tied to the appropriate associations. This
|
|
132
125
|
# dives into the associations hash and their corresponding joins to
|
|
133
126
|
# figure out how to correctly reference a column in SQL.
|
|
134
|
-
#
|
|
127
|
+
#
|
|
135
128
|
def column_with_prefix(column)
|
|
136
|
-
return nil unless column_available?(column)
|
|
137
|
-
|
|
138
129
|
if column.is_string?
|
|
139
130
|
column.__name
|
|
140
|
-
elsif column.
|
|
131
|
+
elsif associations[column].empty?
|
|
141
132
|
"#{@model.quoted_table_name}.#{quote_column(column.__name)}"
|
|
142
133
|
else
|
|
143
|
-
associations[column
|
|
134
|
+
associations[column].collect { |assoc|
|
|
144
135
|
assoc.has_column?(column.__name) ?
|
|
145
136
|
"#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
|
|
146
137
|
nil
|
|
147
138
|
}.compact
|
|
148
139
|
end
|
|
149
140
|
end
|
|
150
|
-
|
|
141
|
+
|
|
151
142
|
def columns_with_prefixes
|
|
152
143
|
@columns.collect { |column|
|
|
153
144
|
column_with_prefix column
|
|
154
|
-
}.flatten
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def column_available?(column)
|
|
158
|
-
if column.is_string?
|
|
159
|
-
true
|
|
160
|
-
elsif column.__stack.empty?
|
|
161
|
-
@model.column_names.include?(column.__name.to_s)
|
|
162
|
-
else
|
|
163
|
-
associations[column.__stack].any? { |assoc|
|
|
164
|
-
assoc.has_column?(column.__name)
|
|
165
|
-
}
|
|
166
|
-
end
|
|
145
|
+
}.flatten
|
|
167
146
|
end
|
|
168
|
-
|
|
147
|
+
|
|
169
148
|
# Gets a stack of associations for a specific path.
|
|
170
|
-
#
|
|
149
|
+
#
|
|
171
150
|
def association_stack(path, parent = nil)
|
|
172
151
|
assocs = []
|
|
173
|
-
|
|
152
|
+
|
|
174
153
|
if parent.nil?
|
|
175
154
|
assocs = @source.association(path.shift)
|
|
176
155
|
else
|
|
177
156
|
assocs = parent.children(path.shift)
|
|
178
157
|
end
|
|
179
|
-
|
|
158
|
+
|
|
180
159
|
until path.empty?
|
|
181
160
|
point = path.shift
|
|
182
161
|
assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
|
|
183
162
|
end
|
|
184
|
-
|
|
163
|
+
|
|
185
164
|
assocs
|
|
186
165
|
end
|
|
187
166
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'thinking_sphinx'
|
|
2
|
+
require 'rails'
|
|
3
|
+
|
|
4
|
+
module ThinkingSphinx
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
|
|
7
|
+
initializer "thinking_sphinx.active_record" do
|
|
8
|
+
if defined?(ActiveRecord)
|
|
9
|
+
::ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "thinking_sphinx.set_app_root" do |app|
|
|
14
|
+
ThinkingSphinx::Configuration.instance.reset # Rails has setup app now
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
config.to_prepare do
|
|
18
|
+
I18n.backend.reload!
|
|
19
|
+
I18n.backend.available_locales
|
|
20
|
+
|
|
21
|
+
# ActiveRecord::Base.to_crc32s is dependant on the subclasses being loaded
|
|
22
|
+
# consistently. When the environment is reset, subclasses/descendants will
|
|
23
|
+
# be lost but our context will not reload them for us.
|
|
24
|
+
#
|
|
25
|
+
# We reset the context which causes the subclasses/descendants to be
|
|
26
|
+
# reloaded next time the context is called.
|
|
27
|
+
#
|
|
28
|
+
ThinkingSphinx.reset_context!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
rake_tasks do
|
|
32
|
+
load File.expand_path('../tasks.rb', __FILE__)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -6,312 +6,215 @@ module ThinkingSphinx
|
|
|
6
6
|
# Most times, you will just want a specific model's results - to search and
|
|
7
7
|
# search_for_ids methods will do the job in exactly the same manner when
|
|
8
8
|
# called from a model.
|
|
9
|
-
#
|
|
10
|
-
class Search
|
|
9
|
+
#
|
|
10
|
+
class Search
|
|
11
11
|
CoreMethods = %w( == class class_eval extend frozen? id instance_eval
|
|
12
12
|
instance_of? instance_values instance_variable_defined?
|
|
13
13
|
instance_variable_get instance_variable_set instance_variables is_a?
|
|
14
|
-
kind_of? member? method methods nil? object_id respond_to?
|
|
15
|
-
|
|
14
|
+
kind_of? member? method methods nil? object_id respond_to? send should
|
|
15
|
+
type )
|
|
16
16
|
SafeMethods = %w( partition private_methods protected_methods
|
|
17
17
|
public_methods send class )
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
instance_methods.select { |method|
|
|
20
20
|
method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
|
|
21
21
|
}.each { |method|
|
|
22
22
|
undef_method method
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
HashOptions = [:conditions, :with, :without, :with_all
|
|
24
|
+
|
|
25
|
+
HashOptions = [:conditions, :with, :without, :with_all]
|
|
26
26
|
ArrayOptions = [:classes, :without_ids]
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
attr_reader :args, :options
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
# Deprecated. Use ThinkingSphinx.search
|
|
31
31
|
def self.search(*args)
|
|
32
|
-
|
|
33
|
-
ThinkingSphinx.search
|
|
32
|
+
log 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
|
|
33
|
+
ThinkingSphinx.search *args
|
|
34
34
|
end
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
# Deprecated. Use ThinkingSphinx.search_for_ids
|
|
37
37
|
def self.search_for_ids(*args)
|
|
38
|
-
|
|
39
|
-
ThinkingSphinx.search_for_ids
|
|
38
|
+
log 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
|
|
39
|
+
ThinkingSphinx.search_for_ids *args
|
|
40
40
|
end
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
# Deprecated. Use ThinkingSphinx.search_for_ids
|
|
43
43
|
def self.search_for_id(*args)
|
|
44
|
-
|
|
45
|
-
ThinkingSphinx.search_for_id
|
|
44
|
+
log 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
|
|
45
|
+
ThinkingSphinx.search_for_id *args
|
|
46
46
|
end
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
# Deprecated. Use ThinkingSphinx.count
|
|
49
49
|
def self.count(*args)
|
|
50
|
-
|
|
51
|
-
ThinkingSphinx.count
|
|
50
|
+
log 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
|
|
51
|
+
ThinkingSphinx.count *args
|
|
52
52
|
end
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
# Deprecated. Use ThinkingSphinx.facets
|
|
55
55
|
def self.facets(*args)
|
|
56
|
-
|
|
57
|
-
ThinkingSphinx.facets
|
|
56
|
+
log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
|
|
57
|
+
ThinkingSphinx.facets *args
|
|
58
58
|
end
|
|
59
|
-
|
|
60
|
-
def self.bundle_searches(enum = nil)
|
|
61
|
-
bundle = ThinkingSphinx::BundledSearch.new
|
|
62
|
-
|
|
63
|
-
if enum.nil?
|
|
64
|
-
yield bundle
|
|
65
|
-
else
|
|
66
|
-
enum.each { |item| yield bundle, item }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
bundle.searches
|
|
70
|
-
end
|
|
71
|
-
|
|
59
|
+
|
|
72
60
|
def self.matching_fields(fields, bitmask)
|
|
73
61
|
matches = []
|
|
74
62
|
bitstring = bitmask.to_s(2).rjust(32, '0').reverse
|
|
75
|
-
|
|
63
|
+
|
|
76
64
|
fields.each_with_index do |field, index|
|
|
77
65
|
matches << field if bitstring[index, 1] == '1'
|
|
78
66
|
end
|
|
79
67
|
matches
|
|
80
68
|
end
|
|
81
|
-
|
|
69
|
+
|
|
82
70
|
def initialize(*args)
|
|
83
71
|
ThinkingSphinx.context.define_indexes
|
|
84
|
-
|
|
72
|
+
|
|
85
73
|
@array = []
|
|
86
74
|
@options = args.extract_options!
|
|
87
75
|
@args = args
|
|
88
|
-
|
|
89
|
-
add_default_scope unless options[:ignore_default]
|
|
90
|
-
|
|
76
|
+
|
|
91
77
|
populate if @options[:populate]
|
|
92
78
|
end
|
|
93
|
-
|
|
94
|
-
def ==(object)
|
|
95
|
-
populate
|
|
96
|
-
super
|
|
97
|
-
end
|
|
98
|
-
|
|
79
|
+
|
|
99
80
|
def to_a
|
|
100
81
|
populate
|
|
101
82
|
@array
|
|
102
83
|
end
|
|
103
|
-
|
|
84
|
+
|
|
104
85
|
def freeze
|
|
105
86
|
populate
|
|
106
87
|
@array.freeze
|
|
107
88
|
self
|
|
108
89
|
end
|
|
109
|
-
|
|
90
|
+
|
|
110
91
|
# Indication of whether the request has been made to Sphinx for the search
|
|
111
92
|
# query.
|
|
112
|
-
#
|
|
93
|
+
#
|
|
113
94
|
# @return [Boolean] true if the results have been requested.
|
|
114
|
-
#
|
|
95
|
+
#
|
|
115
96
|
def populated?
|
|
116
97
|
!!@populated
|
|
117
98
|
end
|
|
118
|
-
|
|
119
|
-
# Indication of whether the request resulted in an error from Sphinx.
|
|
120
|
-
#
|
|
121
|
-
# @return [Boolean] true if Sphinx reports query error
|
|
122
|
-
#
|
|
123
|
-
def error?
|
|
124
|
-
!!error
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# The Sphinx-reported error, if any.
|
|
128
|
-
#
|
|
129
|
-
# @return [String, nil]
|
|
130
|
-
#
|
|
131
|
-
def error
|
|
132
|
-
populate
|
|
133
|
-
@results[:error]
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Indication of whether the request resulted in a warning from Sphinx.
|
|
137
|
-
#
|
|
138
|
-
# @return [Boolean] true if Sphinx reports query warning
|
|
139
|
-
#
|
|
140
|
-
def warning?
|
|
141
|
-
!!warning
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# The Sphinx-reported warning, if any.
|
|
145
|
-
#
|
|
146
|
-
# @return [String, nil]
|
|
147
|
-
#
|
|
148
|
-
def warning
|
|
149
|
-
populate
|
|
150
|
-
@results[:warning]
|
|
151
|
-
end
|
|
152
|
-
|
|
99
|
+
|
|
153
100
|
# The query result hash from Riddle.
|
|
154
|
-
#
|
|
101
|
+
#
|
|
155
102
|
# @return [Hash] Raw Sphinx results
|
|
156
|
-
#
|
|
103
|
+
#
|
|
157
104
|
def results
|
|
158
105
|
populate
|
|
159
106
|
@results
|
|
160
107
|
end
|
|
161
|
-
|
|
108
|
+
|
|
162
109
|
def method_missing(method, *args, &block)
|
|
163
110
|
if is_scope?(method)
|
|
164
111
|
add_scope(method, *args, &block)
|
|
165
112
|
return self
|
|
166
113
|
elsif method == :search_count
|
|
167
|
-
merge_search one_class.search(*args), self.args, options
|
|
168
114
|
return scoped_count
|
|
169
115
|
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
|
170
116
|
super
|
|
171
117
|
elsif !SafeMethods.include?(method.to_s)
|
|
172
118
|
populate
|
|
173
119
|
end
|
|
174
|
-
|
|
120
|
+
|
|
175
121
|
if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
|
|
176
122
|
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
|
|
177
123
|
else
|
|
178
124
|
@array.send(method, *args, &block)
|
|
179
125
|
end
|
|
180
126
|
end
|
|
181
|
-
|
|
127
|
+
|
|
182
128
|
# Returns true if the Search object or the underlying Array object respond
|
|
183
129
|
# to the requested method.
|
|
184
|
-
#
|
|
130
|
+
#
|
|
185
131
|
# @param [Symbol] method The method name
|
|
186
132
|
# @return [Boolean] true if either Search or Array responds to the method.
|
|
187
|
-
#
|
|
133
|
+
#
|
|
188
134
|
def respond_to?(method, include_private = false)
|
|
189
135
|
super || @array.respond_to?(method, include_private)
|
|
190
136
|
end
|
|
191
|
-
|
|
137
|
+
|
|
192
138
|
# The current page number of the result set. Defaults to 1 if no page was
|
|
193
139
|
# explicitly requested.
|
|
194
|
-
#
|
|
140
|
+
#
|
|
195
141
|
# @return [Integer]
|
|
196
|
-
#
|
|
142
|
+
#
|
|
197
143
|
def current_page
|
|
198
144
|
@options[:page].blank? ? 1 : @options[:page].to_i
|
|
199
145
|
end
|
|
200
|
-
|
|
201
|
-
def first_page?
|
|
202
|
-
current_page == 1
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Kaminari support
|
|
206
|
-
def page(page_number)
|
|
207
|
-
@options[:page] = page_number
|
|
208
|
-
self
|
|
209
|
-
end
|
|
210
|
-
|
|
146
|
+
|
|
211
147
|
# The next page number of the result set. If there are no more pages
|
|
212
148
|
# available, nil is returned.
|
|
213
|
-
#
|
|
149
|
+
#
|
|
214
150
|
# @return [Integer, nil]
|
|
215
|
-
#
|
|
151
|
+
#
|
|
216
152
|
def next_page
|
|
217
153
|
current_page >= total_pages ? nil : current_page + 1
|
|
218
154
|
end
|
|
219
|
-
|
|
220
|
-
def next_page?
|
|
221
|
-
!next_page.nil?
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def last_page?
|
|
225
|
-
next_page.nil?
|
|
226
|
-
end
|
|
227
|
-
|
|
155
|
+
|
|
228
156
|
# The previous page number of the result set. If this is the first page,
|
|
229
157
|
# then nil is returned.
|
|
230
|
-
#
|
|
158
|
+
#
|
|
231
159
|
# @return [Integer, nil]
|
|
232
|
-
#
|
|
160
|
+
#
|
|
233
161
|
def previous_page
|
|
234
162
|
current_page == 1 ? nil : current_page - 1
|
|
235
163
|
end
|
|
236
|
-
|
|
164
|
+
|
|
237
165
|
# The amount of records per set of paged results. Defaults to 20 unless a
|
|
238
166
|
# specific page size is requested.
|
|
239
|
-
#
|
|
167
|
+
#
|
|
240
168
|
# @return [Integer]
|
|
241
|
-
#
|
|
169
|
+
#
|
|
242
170
|
def per_page
|
|
243
171
|
@options[:limit] ||= @options[:per_page]
|
|
244
172
|
@options[:limit] ||= 20
|
|
245
173
|
@options[:limit].to_i
|
|
246
174
|
end
|
|
247
|
-
|
|
248
|
-
alias_method :limit_value, :per_page
|
|
249
|
-
|
|
250
|
-
# Kaminari support
|
|
251
|
-
def per(limit)
|
|
252
|
-
@options[:limit] = limit
|
|
253
|
-
self
|
|
254
|
-
end
|
|
255
|
-
|
|
175
|
+
|
|
256
176
|
# The total number of pages available if the results are paginated.
|
|
257
|
-
#
|
|
177
|
+
#
|
|
258
178
|
# @return [Integer]
|
|
259
|
-
#
|
|
179
|
+
#
|
|
260
180
|
def total_pages
|
|
261
181
|
populate
|
|
262
|
-
return 0 if @results
|
|
263
|
-
|
|
182
|
+
return 0 if @results[:total].nil?
|
|
183
|
+
|
|
264
184
|
@total_pages ||= (@results[:total] / per_page.to_f).ceil
|
|
265
185
|
end
|
|
266
|
-
# Compatibility with
|
|
186
|
+
# Compatibility with older versions of will_paginate
|
|
267
187
|
alias_method :page_count, :total_pages
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# Query time taken
|
|
271
|
-
#
|
|
272
|
-
# @return [Integer]
|
|
273
|
-
#
|
|
274
|
-
def query_time
|
|
275
|
-
populate
|
|
276
|
-
return 0 if @results[:time].nil?
|
|
277
|
-
|
|
278
|
-
@query_time ||= @results[:time]
|
|
279
|
-
end
|
|
280
|
-
|
|
188
|
+
|
|
281
189
|
# The total number of search results available.
|
|
282
|
-
#
|
|
190
|
+
#
|
|
283
191
|
# @return [Integer]
|
|
284
|
-
#
|
|
192
|
+
#
|
|
285
193
|
def total_entries
|
|
286
194
|
populate
|
|
287
|
-
return 0 if @results
|
|
288
|
-
|
|
195
|
+
return 0 if @results[:total_found].nil?
|
|
196
|
+
|
|
289
197
|
@total_entries ||= @results[:total_found]
|
|
290
198
|
end
|
|
291
|
-
|
|
292
|
-
# Compatibility with kaminari
|
|
293
|
-
alias_method :total_count, :total_entries
|
|
294
|
-
|
|
199
|
+
|
|
295
200
|
# The current page's offset, based on the number of records per page.
|
|
296
|
-
# Or explicit :offset if given.
|
|
297
|
-
#
|
|
201
|
+
# Or explicit :offset if given.
|
|
202
|
+
#
|
|
298
203
|
# @return [Integer]
|
|
299
|
-
#
|
|
204
|
+
#
|
|
300
205
|
def offset
|
|
301
206
|
@options[:offset] || ((current_page - 1) * per_page)
|
|
302
207
|
end
|
|
303
|
-
|
|
304
|
-
alias_method :offset_value, :offset
|
|
305
|
-
|
|
208
|
+
|
|
306
209
|
def indexes
|
|
307
210
|
return options[:index] if options[:index]
|
|
308
211
|
return '*' if classes.empty?
|
|
309
|
-
|
|
212
|
+
|
|
310
213
|
classes.collect { |klass|
|
|
311
214
|
klass.sphinx_index_names
|
|
312
215
|
}.flatten.uniq.join(',')
|
|
313
216
|
end
|
|
314
|
-
|
|
217
|
+
|
|
315
218
|
def each_with_groupby_and_count(&block)
|
|
316
219
|
populate
|
|
317
220
|
results[:matches].each_with_index do |match, index|
|
|
@@ -321,257 +224,141 @@ module ThinkingSphinx
|
|
|
321
224
|
end
|
|
322
225
|
end
|
|
323
226
|
alias_method :each_with_group_and_count, :each_with_groupby_and_count
|
|
324
|
-
|
|
227
|
+
|
|
325
228
|
def each_with_weighting(&block)
|
|
326
229
|
populate
|
|
327
230
|
results[:matches].each_with_index do |match, index|
|
|
328
231
|
yield self[index], match[:weight]
|
|
329
232
|
end
|
|
330
233
|
end
|
|
331
|
-
|
|
332
|
-
def each_with_match(&block)
|
|
333
|
-
populate
|
|
334
|
-
results[:matches].each_with_index do |match, index|
|
|
335
|
-
yield self[index], match
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
|
|
234
|
+
|
|
339
235
|
def excerpt_for(string, model = nil)
|
|
340
236
|
if model.nil? && one_class
|
|
341
237
|
model ||= one_class
|
|
342
238
|
end
|
|
343
|
-
|
|
239
|
+
|
|
344
240
|
populate
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
:docs => [string.to_s],
|
|
351
|
-
:words => query,
|
|
352
|
-
:index => index.split(',').first.strip
|
|
353
|
-
}.merge(options[:excerpt_options] || {})
|
|
354
|
-
).first
|
|
355
|
-
end
|
|
241
|
+
client.excerpts(
|
|
242
|
+
:docs => [string],
|
|
243
|
+
:words => results[:words].keys.join(' '),
|
|
244
|
+
:index => "#{model.source_of_sphinx_index.sphinx_name}_core"
|
|
245
|
+
).first
|
|
356
246
|
end
|
|
357
|
-
|
|
247
|
+
|
|
358
248
|
def search(*args)
|
|
359
|
-
|
|
360
|
-
merge_search ThinkingSphinx::Search.new(*args)
|
|
249
|
+
add_default_scope
|
|
250
|
+
merge_search ThinkingSphinx::Search.new(*args)
|
|
361
251
|
self
|
|
362
252
|
end
|
|
363
|
-
|
|
364
|
-
def search_for_ids(*args)
|
|
365
|
-
args << args.extract_options!.merge(
|
|
366
|
-
:ignore_default => true,
|
|
367
|
-
:ids_only => true
|
|
368
|
-
)
|
|
369
|
-
merge_search ThinkingSphinx::Search.new(*args), self.args, options
|
|
370
|
-
self
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def facets(*args)
|
|
374
|
-
options = args.extract_options!
|
|
375
|
-
merge_search self, args, options
|
|
376
|
-
args << options
|
|
377
|
-
|
|
378
|
-
ThinkingSphinx::FacetSearch.new(*args)
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def take_client
|
|
382
|
-
if options[:client]
|
|
383
|
-
prepare options[:client]
|
|
384
|
-
yield options[:client]
|
|
385
|
-
else
|
|
386
|
-
ThinkingSphinx::Connection.take do |client|
|
|
387
|
-
prepare client
|
|
388
|
-
yield client
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def append_to(client)
|
|
394
|
-
prepare client
|
|
395
|
-
client.append_query query, indexes, comment
|
|
396
|
-
client.reset
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
def populate_from_queue(results)
|
|
400
|
-
return if @populated
|
|
401
|
-
@populated = true
|
|
402
|
-
@results = results
|
|
403
|
-
|
|
404
|
-
compose_results
|
|
405
|
-
end
|
|
406
|
-
|
|
253
|
+
|
|
407
254
|
private
|
|
408
|
-
|
|
255
|
+
|
|
409
256
|
def config
|
|
410
257
|
ThinkingSphinx::Configuration.instance
|
|
411
258
|
end
|
|
412
|
-
|
|
259
|
+
|
|
413
260
|
def populate
|
|
414
261
|
return if @populated
|
|
415
262
|
@populated = true
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
log "Found #{@results[:total_found]} results", :debug,
|
|
429
|
-
"Sphinx (#{sprintf("%f", runtime)}s)"
|
|
430
|
-
|
|
431
|
-
log "Sphinx Daemon returned warning: #{warning}", :error if warning?
|
|
432
|
-
|
|
433
|
-
if error?
|
|
434
|
-
log "Sphinx Daemon returned error: #{error}", :error
|
|
435
|
-
raise SphinxError.new(error, @results) unless options[:ignore_errors]
|
|
436
|
-
end
|
|
437
|
-
rescue Errno::ECONNREFUSED => err
|
|
438
|
-
raise ThinkingSphinx::ConnectionError,
|
|
439
|
-
'Connection to Sphinx Daemon (searchd) failed.'
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
compose_results
|
|
263
|
+
|
|
264
|
+
retry_on_stale_index do
|
|
265
|
+
begin
|
|
266
|
+
log "Querying: '#{query}'"
|
|
267
|
+
runtime = Benchmark.realtime {
|
|
268
|
+
@results = client.query query, indexes, comment
|
|
269
|
+
}
|
|
270
|
+
log "Found #{@results[:total_found]} results", :debug,
|
|
271
|
+
"Sphinx (#{sprintf("%f", runtime)}s)"
|
|
272
|
+
rescue Errno::ECONNREFUSED => err
|
|
273
|
+
raise ThinkingSphinx::ConnectionError,
|
|
274
|
+
'Connection to Sphinx Daemon (searchd) failed.'
|
|
443
275
|
end
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if retries >= 0
|
|
450
|
-
retry
|
|
276
|
+
|
|
277
|
+
if options[:ids_only]
|
|
278
|
+
replace @results[:matches].collect { |match|
|
|
279
|
+
match[:attributes]["sphinx_internal_id"]
|
|
280
|
+
}
|
|
451
281
|
else
|
|
452
|
-
|
|
282
|
+
replace instances_from_matches
|
|
283
|
+
add_excerpter
|
|
284
|
+
add_sphinx_attributes
|
|
285
|
+
add_matching_fields if client.rank_mode == :fieldmask
|
|
453
286
|
end
|
|
454
287
|
end
|
|
455
288
|
end
|
|
456
|
-
|
|
457
|
-
def compose_results
|
|
458
|
-
if options[:ids_only]
|
|
459
|
-
compose_ids_results
|
|
460
|
-
elsif options[:attributes_only]
|
|
461
|
-
compose_attributes_results
|
|
462
|
-
elsif options[:only]
|
|
463
|
-
compose_only_results
|
|
464
|
-
else
|
|
465
|
-
replace instances_from_matches
|
|
466
|
-
add_excerpter
|
|
467
|
-
add_sphinx_attributes
|
|
468
|
-
add_matching_fields if options[:rank_mode] == :fieldmask
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
def compose_ids_results
|
|
473
|
-
replace @results[:matches].collect { |match|
|
|
474
|
-
match[:attributes]['sphinx_internal_id']
|
|
475
|
-
}
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
def compose_attributes_results
|
|
479
|
-
replace @results[:matches].collect { |match|
|
|
480
|
-
attributes = {}
|
|
481
|
-
match[:attributes].each do |name, value|
|
|
482
|
-
attributes[name.to_sym] = match[:attributes][name]
|
|
483
|
-
end
|
|
484
|
-
attributes
|
|
485
|
-
}
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
def compose_only_results
|
|
489
|
-
replace @results[:matches].collect { |match|
|
|
490
|
-
case only = options[:only]
|
|
491
|
-
when String, Symbol
|
|
492
|
-
match[:attributes][only.to_s]
|
|
493
|
-
when Array
|
|
494
|
-
only.inject({}) do |hash, attribute|
|
|
495
|
-
hash[attribute.to_sym] = match[:attributes][attribute.to_s]
|
|
496
|
-
hash
|
|
497
|
-
end
|
|
498
|
-
else
|
|
499
|
-
raise "Unexpected object for :only argument. String or Array is expected, #{only.class} was received."
|
|
500
|
-
end
|
|
501
|
-
}
|
|
502
|
-
end
|
|
503
|
-
|
|
289
|
+
|
|
504
290
|
def add_excerpter
|
|
505
291
|
each do |object|
|
|
506
|
-
next if object.
|
|
507
|
-
|
|
508
|
-
|
|
292
|
+
next if object.respond_to?(:excerpts)
|
|
293
|
+
|
|
294
|
+
excerpter = ThinkingSphinx::Excerpter.new self, object
|
|
295
|
+
block = lambda { excerpter }
|
|
296
|
+
|
|
297
|
+
object.singleton_class.instance_eval do
|
|
298
|
+
define_method(:excerpts, &block)
|
|
299
|
+
end
|
|
509
300
|
end
|
|
510
301
|
end
|
|
511
|
-
|
|
302
|
+
|
|
512
303
|
def add_sphinx_attributes
|
|
513
304
|
each do |object|
|
|
514
|
-
next if object.nil?
|
|
515
|
-
|
|
305
|
+
next if object.nil? || object.respond_to?(:sphinx_attributes)
|
|
306
|
+
|
|
516
307
|
match = match_hash object
|
|
517
308
|
next if match.nil?
|
|
518
|
-
|
|
519
|
-
object.
|
|
309
|
+
|
|
310
|
+
object.singleton_class.instance_eval do
|
|
311
|
+
define_method(:sphinx_attributes) { match[:attributes] }
|
|
312
|
+
end
|
|
520
313
|
end
|
|
521
314
|
end
|
|
522
|
-
|
|
315
|
+
|
|
523
316
|
def add_matching_fields
|
|
524
317
|
each do |object|
|
|
525
|
-
next if object.nil?
|
|
526
|
-
|
|
318
|
+
next if object.nil? || object.respond_to?(:matching_fields)
|
|
319
|
+
|
|
527
320
|
match = match_hash object
|
|
528
321
|
next if match.nil?
|
|
529
|
-
|
|
322
|
+
fields = ThinkingSphinx::Search.matching_fields(
|
|
530
323
|
@results[:fields], match[:weight]
|
|
531
324
|
)
|
|
325
|
+
|
|
326
|
+
object.singleton_class.instance_eval do
|
|
327
|
+
define_method(:matching_fields) { fields }
|
|
328
|
+
end
|
|
532
329
|
end
|
|
533
330
|
end
|
|
534
|
-
|
|
331
|
+
|
|
535
332
|
def match_hash(object)
|
|
536
333
|
@results[:matches].detect { |match|
|
|
537
|
-
class_crc = object.class.name
|
|
538
|
-
class_crc = object.class.to_crc32 if Riddle.loaded_version.to_i < 2
|
|
539
|
-
|
|
540
334
|
match[:attributes]['sphinx_internal_id'] == object.
|
|
541
335
|
primary_key_for_sphinx &&
|
|
542
|
-
match[:attributes][
|
|
336
|
+
match[:attributes]['class_crc'] == object.class.to_crc32
|
|
543
337
|
}
|
|
544
338
|
end
|
|
545
|
-
|
|
339
|
+
|
|
546
340
|
def self.log(message, method = :debug, identifier = 'Sphinx')
|
|
547
341
|
return if ::ActiveRecord::Base.logger.nil?
|
|
548
|
-
|
|
549
|
-
info =
|
|
550
|
-
|
|
551
|
-
identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
|
|
552
|
-
info << " \e[#{identifier_color}m#{identifier}\e[0m "
|
|
553
|
-
info << "\e[#{message_color}m#{message}\e[0m"
|
|
554
|
-
else
|
|
555
|
-
info = "#{identifier} #{message}"
|
|
556
|
-
end
|
|
557
|
-
|
|
342
|
+
identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
|
|
343
|
+
info = " \e[#{identifier_color}m#{identifier}\e[0m "
|
|
344
|
+
info << "\e[#{message_color}m#{message}\e[0m"
|
|
558
345
|
::ActiveRecord::Base.logger.send method, info
|
|
559
346
|
end
|
|
560
|
-
|
|
347
|
+
|
|
561
348
|
def log(*args)
|
|
562
349
|
self.class.log(*args)
|
|
563
350
|
end
|
|
564
|
-
|
|
565
|
-
def
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
351
|
+
|
|
352
|
+
def client
|
|
353
|
+
client = config.client
|
|
354
|
+
|
|
355
|
+
index_options = one_class ?
|
|
356
|
+
one_class.sphinx_indexes.first.local_options : {}
|
|
357
|
+
|
|
571
358
|
[
|
|
572
359
|
:max_matches, :group_by, :group_function, :group_clause,
|
|
573
360
|
:group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
|
|
574
|
-
:rank_mode, :
|
|
361
|
+
:rank_mode, :max_query_time, :field_weights
|
|
575
362
|
].each do |key|
|
|
576
363
|
value = options[key] || index_options[key]
|
|
577
364
|
client.send("#{key}=", value) if value
|
|
@@ -579,7 +366,7 @@ module ThinkingSphinx
|
|
|
579
366
|
|
|
580
367
|
# treated non-standard as :select is already used for AR queries
|
|
581
368
|
client.select = options[:sphinx_select] || '*'
|
|
582
|
-
|
|
369
|
+
|
|
583
370
|
client.limit = per_page
|
|
584
371
|
client.offset = offset
|
|
585
372
|
client.match_mode = match_mode
|
|
@@ -590,69 +377,72 @@ module ThinkingSphinx
|
|
|
590
377
|
client.group_function = group_function if group_function
|
|
591
378
|
client.index_weights = index_weights
|
|
592
379
|
client.anchor = anchor
|
|
593
|
-
|
|
380
|
+
|
|
594
381
|
client
|
|
595
382
|
end
|
|
596
|
-
|
|
383
|
+
|
|
597
384
|
def retry_on_stale_index(&block)
|
|
598
385
|
stale_ids = []
|
|
599
386
|
retries = stale_retries
|
|
600
|
-
|
|
387
|
+
|
|
601
388
|
begin
|
|
602
389
|
options[:raise_on_stale] = retries > 0
|
|
603
390
|
block.call
|
|
604
|
-
|
|
391
|
+
|
|
605
392
|
# If ThinkingSphinx::Search#instances_from_matches found records in
|
|
606
393
|
# Sphinx but not in the DB and the :raise_on_stale option is set, this
|
|
607
394
|
# exception is raised. We retry a limited number of times, excluding the
|
|
608
395
|
# stale ids from the search.
|
|
609
396
|
rescue StaleIdsException => err
|
|
610
397
|
retries -= 1
|
|
611
|
-
|
|
398
|
+
|
|
612
399
|
# For logging
|
|
613
400
|
stale_ids |= err.ids
|
|
614
401
|
# ID exclusion
|
|
615
402
|
options[:without_ids] = Array(options[:without_ids]) | err.ids
|
|
616
|
-
|
|
403
|
+
|
|
617
404
|
log 'Sphinx Stale Ids (%s %s left): %s' % [
|
|
618
405
|
retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
|
|
619
406
|
]
|
|
620
407
|
retry
|
|
621
408
|
end
|
|
622
409
|
end
|
|
623
|
-
|
|
410
|
+
|
|
624
411
|
def classes
|
|
625
412
|
@classes ||= options[:classes] || []
|
|
626
413
|
end
|
|
627
|
-
|
|
414
|
+
|
|
628
415
|
def one_class
|
|
629
416
|
@one_class ||= classes.length != 1 ? nil : classes.first
|
|
630
417
|
end
|
|
631
|
-
|
|
418
|
+
|
|
632
419
|
def query
|
|
633
420
|
@query ||= begin
|
|
634
421
|
q = @args.join(' ') << conditions_as_query
|
|
635
422
|
(options[:star] ? star_query(q) : q).strip
|
|
636
423
|
end
|
|
637
424
|
end
|
|
638
|
-
|
|
425
|
+
|
|
639
426
|
def conditions_as_query
|
|
640
427
|
return '' if @options[:conditions].blank?
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
428
|
+
|
|
429
|
+
# Soon to be deprecated.
|
|
430
|
+
keys = @options[:conditions].keys.reject { |key|
|
|
431
|
+
attributes.include?(key.to_sym)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
' ' + keys.collect { |key|
|
|
435
|
+
"@#{key} #{options[:conditions][key]}"
|
|
645
436
|
}.join(' ')
|
|
646
437
|
end
|
|
647
|
-
|
|
438
|
+
|
|
648
439
|
def star_query(query)
|
|
649
|
-
token = options[:star].is_a?(Regexp) ? options[:star] :
|
|
440
|
+
token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
|
|
650
441
|
|
|
651
442
|
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
|
652
443
|
pre, proper, post = $`, $&, $'
|
|
653
444
|
# E.g. "@foo", "/2", "~3", but not as part of a token
|
|
654
|
-
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
|
655
|
-
pre.match(%r{(\W|^)@\([^\)]*$})
|
|
445
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
|
656
446
|
# E.g. "foo bar", with quotes
|
|
657
447
|
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
|
658
448
|
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
|
@@ -663,25 +453,15 @@ module ThinkingSphinx
|
|
|
663
453
|
end
|
|
664
454
|
end
|
|
665
455
|
end
|
|
666
|
-
|
|
667
|
-
if Regexp.instance_methods.include?(:encoding)
|
|
668
|
-
DefaultStarToken = Regexp.new('\p{Word}+')
|
|
669
|
-
else
|
|
670
|
-
DefaultStarToken = Regexp.new('\w+', nil, 'u')
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
def default_star_token
|
|
674
|
-
DefaultStarToken
|
|
675
|
-
end
|
|
676
|
-
|
|
456
|
+
|
|
677
457
|
def comment
|
|
678
458
|
options[:comment] || ''
|
|
679
459
|
end
|
|
680
|
-
|
|
460
|
+
|
|
681
461
|
def match_mode
|
|
682
462
|
options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
|
|
683
463
|
end
|
|
684
|
-
|
|
464
|
+
|
|
685
465
|
def sort_mode
|
|
686
466
|
@sort_mode ||= case options[:sort_mode]
|
|
687
467
|
when :asc
|
|
@@ -701,11 +481,11 @@ module ThinkingSphinx
|
|
|
701
481
|
options[:sort_mode]
|
|
702
482
|
end
|
|
703
483
|
end
|
|
704
|
-
|
|
484
|
+
|
|
705
485
|
def sort_by
|
|
706
486
|
case @sort_by = (options[:sort_by] || options[:order])
|
|
707
487
|
when String
|
|
708
|
-
sorted_fields_to_attributes(@sort_by
|
|
488
|
+
sorted_fields_to_attributes(@sort_by)
|
|
709
489
|
when Symbol
|
|
710
490
|
field_names.include?(@sort_by) ?
|
|
711
491
|
@sort_by.to_s.concat('_sort') : @sort_by.to_s
|
|
@@ -713,28 +493,28 @@ module ThinkingSphinx
|
|
|
713
493
|
''
|
|
714
494
|
end
|
|
715
495
|
end
|
|
716
|
-
|
|
496
|
+
|
|
717
497
|
def field_names
|
|
718
498
|
return [] unless one_class
|
|
719
|
-
|
|
499
|
+
|
|
720
500
|
one_class.sphinx_indexes.collect { |index|
|
|
721
501
|
index.fields.collect { |field| field.unique_name }
|
|
722
502
|
}.flatten
|
|
723
503
|
end
|
|
724
|
-
|
|
504
|
+
|
|
725
505
|
def sorted_fields_to_attributes(order_string)
|
|
726
506
|
field_names.each { |field|
|
|
727
507
|
order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
|
728
508
|
match.gsub field.to_s, field.to_s.concat("_sort")
|
|
729
509
|
}
|
|
730
510
|
}
|
|
731
|
-
|
|
511
|
+
|
|
732
512
|
order_string
|
|
733
513
|
end
|
|
734
|
-
|
|
514
|
+
|
|
735
515
|
# Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
|
|
736
516
|
# { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
|
737
|
-
#
|
|
517
|
+
#
|
|
738
518
|
def index_weights
|
|
739
519
|
weights = options[:index_weights] || {}
|
|
740
520
|
weights.keys.inject({}) do |hash, key|
|
|
@@ -745,39 +525,55 @@ module ThinkingSphinx
|
|
|
745
525
|
else
|
|
746
526
|
hash[key] = weights[key]
|
|
747
527
|
end
|
|
748
|
-
|
|
528
|
+
|
|
749
529
|
hash
|
|
750
530
|
end
|
|
751
531
|
end
|
|
752
|
-
|
|
532
|
+
|
|
753
533
|
def group_by
|
|
754
534
|
options[:group] ? options[:group].to_s : nil
|
|
755
535
|
end
|
|
756
|
-
|
|
536
|
+
|
|
757
537
|
def group_function
|
|
758
538
|
options[:group] ? :attr : nil
|
|
759
539
|
end
|
|
760
|
-
|
|
540
|
+
|
|
761
541
|
def internal_filters
|
|
762
542
|
filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
|
|
763
|
-
|
|
543
|
+
|
|
764
544
|
class_crcs = classes.collect { |klass|
|
|
765
545
|
klass.to_crc32s
|
|
766
546
|
}.flatten
|
|
767
|
-
|
|
547
|
+
|
|
768
548
|
unless class_crcs.empty?
|
|
769
549
|
filters << Riddle::Client::Filter.new('class_crc', class_crcs)
|
|
770
550
|
end
|
|
771
|
-
|
|
551
|
+
|
|
772
552
|
filters << Riddle::Client::Filter.new(
|
|
773
553
|
'sphinx_internal_id', filter_value(options[:without_ids]), true
|
|
774
|
-
)
|
|
775
|
-
|
|
554
|
+
) if options[:without_ids]
|
|
555
|
+
|
|
776
556
|
filters
|
|
777
557
|
end
|
|
778
|
-
|
|
558
|
+
|
|
559
|
+
def condition_filters
|
|
560
|
+
(options[:conditions] || {}).collect { |attrib, value|
|
|
561
|
+
if attributes.include?(attrib.to_sym)
|
|
562
|
+
puts <<-MSG
|
|
563
|
+
Deprecation Warning: filters on attributes should be done using the :with
|
|
564
|
+
option, not :conditions. For example:
|
|
565
|
+
:with => {:#{attrib} => #{value.inspect}}
|
|
566
|
+
MSG
|
|
567
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
568
|
+
else
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
}.compact
|
|
572
|
+
end
|
|
573
|
+
|
|
779
574
|
def filters
|
|
780
575
|
internal_filters +
|
|
576
|
+
condition_filters +
|
|
781
577
|
(options[:with] || {}).collect { |attrib, value|
|
|
782
578
|
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
783
579
|
} +
|
|
@@ -788,15 +584,18 @@ module ThinkingSphinx
|
|
|
788
584
|
Array(values).collect { |value|
|
|
789
585
|
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
790
586
|
}
|
|
791
|
-
}.flatten +
|
|
792
|
-
(options[:without_any] || {}).collect { |attrib, values|
|
|
793
|
-
Array(values).collect { |value|
|
|
794
|
-
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
|
795
|
-
}
|
|
796
587
|
}.flatten
|
|
797
588
|
end
|
|
798
|
-
|
|
589
|
+
|
|
799
590
|
# When passed a Time instance, returns the integer timestamp.
|
|
591
|
+
#
|
|
592
|
+
# If using Rails 2.1+, need to handle timezones to translate them back to
|
|
593
|
+
# UTC, as that's what datetimes will be stored as by MySQL.
|
|
594
|
+
#
|
|
595
|
+
# in_time_zone is a method that was added for the timezone support in
|
|
596
|
+
# Rails 2.1, which is why it's used for testing. I'm sure there's better
|
|
597
|
+
# ways, but this does the job.
|
|
598
|
+
#
|
|
800
599
|
def filter_value(value)
|
|
801
600
|
case value
|
|
802
601
|
when Range
|
|
@@ -804,19 +603,17 @@ module ThinkingSphinx
|
|
|
804
603
|
when Array
|
|
805
604
|
value.collect { |v| filter_value(v) }.flatten
|
|
806
605
|
when Time
|
|
807
|
-
[value.to_i]
|
|
808
|
-
when Date
|
|
809
|
-
[Time.utc(value.year, value.month, value.day).to_i]
|
|
606
|
+
value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
|
|
810
607
|
when NilClass
|
|
811
608
|
0
|
|
812
609
|
else
|
|
813
610
|
Array(value)
|
|
814
611
|
end
|
|
815
612
|
end
|
|
816
|
-
|
|
613
|
+
|
|
817
614
|
def anchor
|
|
818
615
|
return {} unless options[:geo] || (options[:lat] && options[:lng])
|
|
819
|
-
|
|
616
|
+
|
|
820
617
|
{
|
|
821
618
|
:latitude => options[:geo] ? options[:geo].first : options[:lat],
|
|
822
619
|
:longitude => options[:geo] ? options[:geo].last : options[:lng],
|
|
@@ -824,43 +621,43 @@ module ThinkingSphinx
|
|
|
824
621
|
:longitude_attribute => longitude_attr.to_s
|
|
825
622
|
}
|
|
826
623
|
end
|
|
827
|
-
|
|
624
|
+
|
|
828
625
|
def latitude_attr
|
|
829
626
|
options[:latitude_attr] ||
|
|
830
627
|
index_option(:latitude_attr) ||
|
|
831
628
|
attribute(:lat, :latitude)
|
|
832
629
|
end
|
|
833
|
-
|
|
630
|
+
|
|
834
631
|
def longitude_attr
|
|
835
632
|
options[:longitude_attr] ||
|
|
836
633
|
index_option(:longitude_attr) ||
|
|
837
634
|
attribute(:lon, :lng, :longitude)
|
|
838
635
|
end
|
|
839
|
-
|
|
636
|
+
|
|
840
637
|
def index_option(key)
|
|
841
638
|
return nil unless one_class
|
|
842
|
-
|
|
639
|
+
|
|
843
640
|
one_class.sphinx_indexes.collect { |index|
|
|
844
641
|
index.local_options[key]
|
|
845
642
|
}.compact.first
|
|
846
643
|
end
|
|
847
|
-
|
|
644
|
+
|
|
848
645
|
def attribute(*keys)
|
|
849
646
|
return nil unless one_class
|
|
850
|
-
|
|
647
|
+
|
|
851
648
|
keys.detect { |key|
|
|
852
649
|
attributes.include?(key)
|
|
853
650
|
}
|
|
854
651
|
end
|
|
855
|
-
|
|
652
|
+
|
|
856
653
|
def attributes
|
|
857
654
|
return [] unless one_class
|
|
858
|
-
|
|
655
|
+
|
|
859
656
|
attributes = one_class.sphinx_indexes.collect { |index|
|
|
860
657
|
index.attributes.collect { |attrib| attrib.unique_name }
|
|
861
658
|
}.flatten
|
|
862
659
|
end
|
|
863
|
-
|
|
660
|
+
|
|
864
661
|
def stale_retries
|
|
865
662
|
case options[:retry_stale]
|
|
866
663
|
when TrueClass
|
|
@@ -871,50 +668,7 @@ module ThinkingSphinx
|
|
|
871
668
|
options[:retry_stale].to_i
|
|
872
669
|
end
|
|
873
670
|
end
|
|
874
|
-
|
|
875
|
-
def hard_retries
|
|
876
|
-
options[:hard_retry_count] || config.hard_retry_count
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
def include_for_class(klass)
|
|
880
|
-
includes = options[:include] || klass.sphinx_index_options[:include]
|
|
881
|
-
|
|
882
|
-
case includes
|
|
883
|
-
when NilClass
|
|
884
|
-
nil
|
|
885
|
-
when Array
|
|
886
|
-
include_from_array includes, klass
|
|
887
|
-
when Symbol
|
|
888
|
-
klass.reflections[includes].nil? ? nil : includes
|
|
889
|
-
when Hash
|
|
890
|
-
include_from_hash includes, klass
|
|
891
|
-
else
|
|
892
|
-
includes
|
|
893
|
-
end
|
|
894
|
-
end
|
|
895
|
-
|
|
896
|
-
def include_from_array(array, klass)
|
|
897
|
-
scoped_array = []
|
|
898
|
-
array.each do |value|
|
|
899
|
-
case value
|
|
900
|
-
when Hash
|
|
901
|
-
scoped_hash = include_from_hash(value, klass)
|
|
902
|
-
scoped_array << scoped_hash unless scoped_hash.nil?
|
|
903
|
-
else
|
|
904
|
-
scoped_array << value unless klass.reflections[value].nil?
|
|
905
|
-
end
|
|
906
|
-
end
|
|
907
|
-
scoped_array.empty? ? nil : scoped_array
|
|
908
|
-
end
|
|
909
|
-
|
|
910
|
-
def include_from_hash(hash, klass)
|
|
911
|
-
scoped_hash = {}
|
|
912
|
-
hash.keys.each do |key|
|
|
913
|
-
scoped_hash[key] = hash[key] unless klass.reflections[key].nil?
|
|
914
|
-
end
|
|
915
|
-
scoped_hash.empty? ? nil : scoped_hash
|
|
916
|
-
end
|
|
917
|
-
|
|
671
|
+
|
|
918
672
|
def instances_from_class(klass, matches)
|
|
919
673
|
index_options = klass.sphinx_index_options
|
|
920
674
|
|
|
@@ -923,13 +677,13 @@ module ThinkingSphinx
|
|
|
923
677
|
:all,
|
|
924
678
|
:joins => options[:joins],
|
|
925
679
|
:conditions => {klass.primary_key_for_sphinx.to_sym => ids},
|
|
926
|
-
:include =>
|
|
680
|
+
:include => (options[:include] || index_options[:include]),
|
|
927
681
|
:select => (options[:select] || index_options[:select]),
|
|
928
682
|
:order => (options[:sql_order] || index_options[:sql_order])
|
|
929
683
|
) : []
|
|
930
684
|
|
|
931
685
|
# Raise an exception if we find records in Sphinx but not in the DB, so
|
|
932
|
-
# the search method can retry without them. See
|
|
686
|
+
# the search method can retry without them. See
|
|
933
687
|
# ThinkingSphinx::Search.retry_search_on_stale_index.
|
|
934
688
|
if options[:raise_on_stale] && instances.length < ids.length
|
|
935
689
|
stale_ids = ids - instances.map { |i| i.id }
|
|
@@ -946,43 +700,39 @@ module ThinkingSphinx
|
|
|
946
700
|
end
|
|
947
701
|
}
|
|
948
702
|
end
|
|
949
|
-
|
|
703
|
+
|
|
950
704
|
# Group results by class and call #find(:all) once for each group to reduce
|
|
951
705
|
# the number of #find's in multi-model searches.
|
|
952
|
-
#
|
|
706
|
+
#
|
|
953
707
|
def instances_from_matches
|
|
954
708
|
return single_class_results if one_class
|
|
955
|
-
|
|
709
|
+
|
|
956
710
|
groups = results[:matches].group_by { |match|
|
|
957
|
-
match[:attributes][
|
|
711
|
+
match[:attributes]["class_crc"]
|
|
958
712
|
}
|
|
959
713
|
groups.each do |crc, group|
|
|
960
714
|
group.replace(
|
|
961
715
|
instances_from_class(class_from_crc(crc), group)
|
|
962
716
|
)
|
|
963
717
|
end
|
|
964
|
-
|
|
718
|
+
|
|
965
719
|
results[:matches].collect do |match|
|
|
966
720
|
groups.detect { |crc, group|
|
|
967
|
-
crc == match[:attributes][
|
|
721
|
+
crc == match[:attributes]["class_crc"]
|
|
968
722
|
}[1].compact.detect { |obj|
|
|
969
723
|
obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
|
|
970
724
|
}
|
|
971
725
|
end
|
|
972
726
|
end
|
|
973
|
-
|
|
727
|
+
|
|
974
728
|
def single_class_results
|
|
975
729
|
instances_from_class one_class, results[:matches]
|
|
976
730
|
end
|
|
977
|
-
|
|
731
|
+
|
|
978
732
|
def class_from_crc(crc)
|
|
979
|
-
|
|
980
|
-
config.models_by_crc[crc].constantize
|
|
981
|
-
else
|
|
982
|
-
crc.constantize
|
|
983
|
-
end
|
|
733
|
+
config.models_by_crc[crc].constantize
|
|
984
734
|
end
|
|
985
|
-
|
|
735
|
+
|
|
986
736
|
def each_with_attribute(attribute, &block)
|
|
987
737
|
populate
|
|
988
738
|
results[:matches].each_with_index do |match, index|
|
|
@@ -990,25 +740,23 @@ module ThinkingSphinx
|
|
|
990
740
|
(match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
|
|
991
741
|
end
|
|
992
742
|
end
|
|
993
|
-
|
|
743
|
+
|
|
994
744
|
def is_scope?(method)
|
|
995
745
|
one_class && one_class.sphinx_scopes.include?(method)
|
|
996
746
|
end
|
|
997
|
-
|
|
747
|
+
|
|
998
748
|
# Adds the default_sphinx_scope if set.
|
|
999
749
|
def add_default_scope
|
|
1000
|
-
|
|
1001
|
-
add_scope(one_class.get_default_sphinx_scope.to_sym)
|
|
750
|
+
add_scope(one_class.get_default_sphinx_scope) if one_class && one_class.has_default_sphinx_scope?
|
|
1002
751
|
end
|
|
1003
|
-
|
|
752
|
+
|
|
1004
753
|
def add_scope(method, *args, &block)
|
|
1005
|
-
method
|
|
1006
|
-
merge_search one_class.send(method, *args, &block), self.args, options
|
|
754
|
+
merge_search one_class.send(method, *args, &block)
|
|
1007
755
|
end
|
|
1008
|
-
|
|
1009
|
-
def merge_search(search
|
|
756
|
+
|
|
757
|
+
def merge_search(search)
|
|
1010
758
|
search.args.each { |arg| args << arg }
|
|
1011
|
-
|
|
759
|
+
|
|
1012
760
|
search.options.keys.each do |key|
|
|
1013
761
|
if HashOptions.include?(key)
|
|
1014
762
|
options[key] ||= {}
|
|
@@ -1022,20 +770,16 @@ module ThinkingSphinx
|
|
|
1022
770
|
end
|
|
1023
771
|
end
|
|
1024
772
|
end
|
|
1025
|
-
|
|
773
|
+
|
|
1026
774
|
def scoped_count
|
|
1027
|
-
return self.total_entries if
|
|
1028
|
-
|
|
775
|
+
return self.total_entries if @options[:ids_only]
|
|
776
|
+
|
|
1029
777
|
@options[:ids_only] = true
|
|
1030
778
|
results_count = self.total_entries
|
|
1031
779
|
@options[:ids_only] = false
|
|
1032
780
|
@populated = false
|
|
1033
|
-
|
|
781
|
+
|
|
1034
782
|
results_count
|
|
1035
783
|
end
|
|
1036
|
-
|
|
1037
|
-
def crc_attribute
|
|
1038
|
-
Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
|
|
1039
|
-
end
|
|
1040
784
|
end
|
|
1041
785
|
end
|