thinking-sphinx 2.0.5 → 2.0.6

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