thinking-sphinx 3.1.0 → 3.1.1

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -7
  4. data/HISTORY +27 -0
  5. data/README.textile +38 -218
  6. data/gemfiles/rails_3_2.gemfile +2 -3
  7. data/gemfiles/rails_4_0.gemfile +2 -3
  8. data/gemfiles/rails_4_1.gemfile +2 -3
  9. data/lib/thinking_sphinx.rb +1 -0
  10. data/lib/thinking_sphinx/active_record.rb +1 -0
  11. data/lib/thinking_sphinx/active_record/association_proxy.rb +1 -0
  12. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +5 -10
  13. data/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb +38 -0
  14. data/lib/thinking_sphinx/active_record/attribute/type.rb +19 -8
  15. data/lib/thinking_sphinx/active_record/base.rb +3 -1
  16. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +1 -1
  17. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +4 -1
  18. data/lib/thinking_sphinx/active_record/index.rb +4 -4
  19. data/lib/thinking_sphinx/active_record/property_query.rb +57 -27
  20. data/lib/thinking_sphinx/active_record/simple_many_query.rb +35 -0
  21. data/lib/thinking_sphinx/capistrano/v3.rb +11 -10
  22. data/lib/thinking_sphinx/configuration.rb +23 -6
  23. data/lib/thinking_sphinx/connection.rb +8 -9
  24. data/lib/thinking_sphinx/errors.rb +7 -2
  25. data/lib/thinking_sphinx/facet.rb +2 -2
  26. data/lib/thinking_sphinx/facet_search.rb +4 -2
  27. data/lib/thinking_sphinx/logger.rb +7 -0
  28. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -4
  29. data/lib/thinking_sphinx/middlewares/inquirer.rb +2 -2
  30. data/lib/thinking_sphinx/middlewares/sphinxql.rb +6 -2
  31. data/lib/thinking_sphinx/middlewares/stale_id_filter.rb +1 -1
  32. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +6 -1
  33. data/lib/thinking_sphinx/real_time/property.rb +2 -1
  34. data/lib/thinking_sphinx/real_time/transcriber.rb +7 -3
  35. data/lib/thinking_sphinx/search.rb +14 -4
  36. data/lib/thinking_sphinx/search/context.rb +0 -6
  37. data/lib/thinking_sphinx/test.rb +11 -2
  38. data/lib/thinking_sphinx/wildcard.rb +7 -1
  39. data/spec/acceptance/association_scoping_spec.rb +55 -15
  40. data/spec/acceptance/geosearching_spec.rb +8 -2
  41. data/spec/acceptance/real_time_updates_spec.rb +9 -0
  42. data/spec/acceptance/specifying_sql_spec.rb +31 -17
  43. data/spec/internal/app/indices/car_index.rb +5 -0
  44. data/spec/internal/app/models/car.rb +5 -0
  45. data/spec/internal/app/models/category.rb +2 -1
  46. data/spec/internal/app/models/manufacturer.rb +3 -0
  47. data/spec/internal/db/schema.rb +9 -0
  48. data/spec/thinking_sphinx/active_record/attribute/type_spec.rb +7 -0
  49. data/spec/thinking_sphinx/active_record/base_spec.rb +17 -0
  50. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -1
  51. data/spec/thinking_sphinx/configuration_spec.rb +40 -2
  52. data/spec/thinking_sphinx/errors_spec.rb +21 -0
  53. data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
  54. data/spec/thinking_sphinx/middlewares/inquirer_spec.rb +0 -4
  55. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +25 -0
  56. data/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb +2 -2
  57. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +2 -1
  58. data/spec/thinking_sphinx/search_spec.rb +56 -0
  59. data/spec/thinking_sphinx/wildcard_spec.rb +5 -0
  60. data/thinking-sphinx.gemspec +2 -2
  61. metadata +40 -32
  62. data/sketchpad.rb +0 -58
  63. data/spec/internal/log/.gitignore +0 -1
@@ -0,0 +1,38 @@
1
+ class ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeMatcher
2
+ def initialize(attribute, foreign_key)
3
+ @attribute, @foreign_key = attribute, foreign_key.to_s
4
+ end
5
+
6
+ def matches?
7
+ return false if many?
8
+
9
+ column_name_matches? || attribute_name_matches? || multi_singular_match?
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :attribute, :foreign_key
15
+
16
+ delegate :name, :multi?, :to => :attribute
17
+
18
+ def attribute_name_matches?
19
+ name == foreign_key
20
+ end
21
+
22
+ def column_name_matches?
23
+ column.__name.to_s == foreign_key
24
+ end
25
+
26
+ def column
27
+ attribute.respond_to?(:columns) ? attribute.columns.first :
28
+ attribute.column
29
+ end
30
+
31
+ def many?
32
+ attribute.respond_to?(:columns) && attribute.columns.many?
33
+ end
34
+
35
+ def multi_singular_match?
36
+ multi? && name.singularize == foreign_key
37
+ end
38
+ end
@@ -40,6 +40,20 @@ class ThinkingSphinx::ActiveRecord::Attribute::Type
40
40
  end
41
41
  end
42
42
 
43
+ def big_integer?
44
+ database_column.type == :integer && database_column.sql_type[/bigint/i]
45
+ end
46
+
47
+ def column_name
48
+ attribute.columns.first.__name.to_s
49
+ end
50
+
51
+ def database_column
52
+ @database_column ||= klass.columns.detect { |db_column|
53
+ db_column.name == column_name
54
+ }
55
+ end
56
+
43
57
  def klass
44
58
  @klass ||= associations.any? ? associations.last.klass : model
45
59
  end
@@ -57,15 +71,12 @@ class ThinkingSphinx::ActiveRecord::Attribute::Type
57
71
  end
58
72
 
59
73
  def type_from_database
60
- db_column = klass.columns.detect { |db_column|
61
- db_column.name == attribute.columns.first.__name.to_s
62
- }
74
+ raise ThinkingSphinx::MissingColumnError,
75
+ "column #{column_name} does not exist" if database_column.nil?
63
76
 
64
- if db_column.type == :integer && db_column.sql_type[/bigint/i]
65
- return :bigint
66
- end
77
+ return :bigint if big_integer?
67
78
 
68
- case db_column.type
79
+ case database_column.type
69
80
  when :datetime, :date
70
81
  :timestamp
71
82
  when :text
@@ -73,7 +84,7 @@ class ThinkingSphinx::ActiveRecord::Attribute::Type
73
84
  when :decimal
74
85
  :float
75
86
  else
76
- db_column.type
87
+ database_column.type
77
88
  end
78
89
  end
79
90
  end
@@ -50,7 +50,9 @@ module ThinkingSphinx::ActiveRecord::Base
50
50
  'You cannot search with Sphinx through ActiveRecord scopes'
51
51
  end
52
52
 
53
- merger.merge! nil, :classes => [self]
53
+ result = merger.merge! nil, :classes => [self]
54
+ result.populate if result.options[:populate]
55
+ result
54
56
  end
55
57
  end
56
58
  end
@@ -30,7 +30,7 @@ class ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks <
30
30
  end
31
31
 
32
32
  def core_indices
33
- @core_indices ||= indices.reject &:delta?
33
+ @core_indices ||= indices.select(&:delta_processor).reject(&:delta?)
34
34
  end
35
35
 
36
36
  def delta_indices
@@ -28,7 +28,10 @@ class ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks <
28
28
  end
29
29
 
30
30
  def indices
31
- @indices ||= configuration.indices_for_references reference
31
+ @indices ||= begin
32
+ all = configuration.indices_for_references(reference)
33
+ all.reject { |index| index.type == 'rt' }
34
+ end
32
35
  end
33
36
 
34
37
  def reference
@@ -12,6 +12,10 @@ class ThinkingSphinx::ActiveRecord::Index < Riddle::Configuration::Index
12
12
  end
13
13
  end
14
14
 
15
+ def attributes
16
+ sources.collect(&:attributes).flatten
17
+ end
18
+
15
19
  def delta?
16
20
  @options[:delta?]
17
21
  end
@@ -40,10 +44,6 @@ class ThinkingSphinx::ActiveRecord::Index < Riddle::Configuration::Index
40
44
  adapter_for(model)
41
45
  end
42
46
 
43
- def attributes
44
- sources.collect(&:attributes).flatten
45
- end
46
-
47
47
  def fields
48
48
  sources.collect(&:fields).flatten
49
49
  end
@@ -4,26 +4,28 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
4
4
  end
5
5
 
6
6
  def to_s
7
- identifier = [type, property.name].compact.join(' ')
8
-
9
- "#{identifier} from #{source_type}; #{queries.join('; ')}"
10
- end
11
-
12
- private
7
+ if unsafe_habtm_column?
8
+ raise <<-MESSAGE
9
+ Source queries cannot be used with HABTM joins if they use anything beyond the
10
+ primary key.
11
+ MESSAGE
12
+ end
13
13
 
14
- def queries
15
- queries = []
16
- if column.string?
17
- queries << column.__name.strip.gsub(/\n/, "\\\n")
14
+ if safe_habtm_column?
15
+ ThinkingSphinx::ActiveRecord::SimpleManyQuery.new(
16
+ property, source, type
17
+ ).to_s
18
18
  else
19
- queries << to_sql
20
- queries << range_sql if ranged?
19
+ "#{identifier} from #{source_type}; #{queries.join('; ')}"
21
20
  end
22
- queries
23
21
  end
24
22
 
23
+ private
24
+
25
25
  attr_reader :property, :source, :type
26
26
 
27
+ delegate :unscoped, :to => :base_association_class, :prefix => true
28
+
27
29
  def base_association
28
30
  reflections.first
29
31
  end
@@ -31,7 +33,6 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
31
33
  def base_association_class
32
34
  base_association.klass
33
35
  end
34
- delegate :unscoped, :to => :base_association_class, :prefix => true
35
36
 
36
37
  def column
37
38
  @column ||= property.columns.first
@@ -43,17 +44,8 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
43
44
  [reflection.through_reflection, reflection.source_reflection]
44
45
  end
45
46
 
46
- def reflections
47
- @reflections ||= begin
48
- base = source.model
49
-
50
- column.__stack.collect { |key|
51
- reflection = base.reflections[key]
52
- base = reflection.klass
53
-
54
- extend_reflection reflection
55
- }.flatten
56
- end
47
+ def identifier
48
+ [type, property.name].compact.join(' ')
57
49
  end
58
50
 
59
51
  def joins
@@ -68,10 +60,25 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
68
60
  end
69
61
  end
70
62
 
63
+ def macros
64
+ reflections.collect &:macro
65
+ end
66
+
71
67
  def offset
72
68
  "* #{ThinkingSphinx::Configuration.instance.indices.count} + #{source.offset}"
73
69
  end
74
70
 
71
+ def queries
72
+ queries = []
73
+ if column.string?
74
+ queries << column.__name.strip.gsub(/\n/, "\\\n")
75
+ else
76
+ queries << to_sql
77
+ queries << range_sql if ranged?
78
+ end
79
+ queries
80
+ end
81
+
75
82
  def quoted_foreign_key
76
83
  quote_with_table(base_association_class.table_name, base_association.foreign_key)
77
84
  end
@@ -98,6 +105,23 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
98
105
  ).to_sql
99
106
  end
100
107
 
108
+ def reflections
109
+ @reflections ||= begin
110
+ base = source.model
111
+
112
+ column.__stack.collect { |key|
113
+ reflection = base.reflections[key]
114
+ base = reflection.klass
115
+
116
+ extend_reflection reflection
117
+ }.flatten
118
+ end
119
+ end
120
+
121
+ def safe_habtm_column?
122
+ macros == [:has_and_belongs_to_many] && column.__name == :id
123
+ end
124
+
101
125
  def source_type
102
126
  property.source_type.to_s.dasherize
103
127
  end
@@ -105,12 +129,18 @@ class ThinkingSphinx::ActiveRecord::PropertyQuery
105
129
  def to_sql
106
130
  raise "Could not determine SQL for MVA" if reflections.empty?
107
131
 
108
- relation = base_association_class_unscoped.select("#{quoted_foreign_key} #{offset} AS #{quote_column('id')}, #{quoted_primary_key} AS #{quote_column(property.name)}"
109
- )
132
+ relation = base_association_class_unscoped.select("#{quoted_foreign_key} #{offset} AS #{quote_column('id')}, #{quoted_primary_key} AS #{quote_column(property.name)}")
110
133
  relation = relation.joins(joins) if joins.present?
111
134
  relation = relation.where("#{quoted_foreign_key} BETWEEN $start AND $end") if ranged?
135
+ relation = relation.where("#{quoted_foreign_key} IS NOT NULL")
112
136
  relation = relation.order("#{quoted_foreign_key} ASC") if type.nil?
113
137
 
114
138
  relation.to_sql
115
139
  end
140
+
141
+ def unsafe_habtm_column?
142
+ macros.include?(:has_and_belongs_to_many) && (
143
+ macros.length > 1 || column.__name != :id
144
+ )
145
+ end
116
146
  end
@@ -0,0 +1,35 @@
1
+ class ThinkingSphinx::ActiveRecord::SimpleManyQuery <
2
+ ThinkingSphinx::ActiveRecord::PropertyQuery
3
+
4
+ def to_s
5
+ "#{identifier} from #{source_type}; #{queries.join('; ')}"
6
+ end
7
+
8
+ private
9
+
10
+ def reflection
11
+ @reflection ||= source.model.reflections[column.__stack.first]
12
+ end
13
+
14
+ def quoted_foreign_key
15
+ quote_with_table reflection.join_table, reflection.foreign_key
16
+ end
17
+
18
+ def quoted_primary_key
19
+ quote_with_table reflection.join_table, reflection.association_foreign_key
20
+ end
21
+
22
+ def range_sql
23
+ "SELECT MIN(#{quoted_foreign_key}), MAX(#{quoted_foreign_key}) FROM #{quote_column reflection.join_table}"
24
+ end
25
+
26
+ def to_sql
27
+ selects = [
28
+ "#{quoted_foreign_key} #{offset} AS #{quote_column('id')}",
29
+ "#{quoted_primary_key} AS #{quote_column(property.name)}"
30
+ ]
31
+ sql = "SELECT #{selects.join(', ')} FROM #{quote_column reflection.join_table}"
32
+ sql += " WHERE (#{quoted_foreign_key} BETWEEN $start AND $end)" if ranged?
33
+ sql
34
+ end
35
+ end
@@ -1,6 +1,7 @@
1
1
  namespace :load do
2
2
  task :defaults do
3
3
  set :thinking_sphinx_roles, :db
4
+ set :thinking_sphinx_rails_env, -> { fetch(:stage) }
4
5
  end
5
6
  end
6
7
 
@@ -12,7 +13,7 @@ if you alter the structure of your indexes.
12
13
  task :rebuild do
13
14
  on roles fetch(:thinking_sphinx_roles) do
14
15
  within current_path do
15
- with rails_env: fetch(:stage) do
16
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
16
17
  execute :rake, "ts:rebuild"
17
18
  end
18
19
  end
@@ -21,9 +22,9 @@ if you alter the structure of your indexes.
21
22
 
22
23
  desc 'Stop Sphinx, clear Sphinx index files, generate configuration file, start Sphinx, repopulate all data.'
23
24
  task :regenerate do
24
- on roles fetch(:thinking_sphinx_options) do
25
+ on roles fetch(:thinking_sphinx_roles) do
25
26
  within current_path do
26
- with rails_env: fetch(:stage) do
27
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
27
28
  execute :rake, 'ts:regenerate'
28
29
  end
29
30
  end
@@ -34,7 +35,7 @@ if you alter the structure of your indexes.
34
35
  task :index do
35
36
  on roles fetch(:thinking_sphinx_roles) do
36
37
  within current_path do
37
- with rails_env: fetch(:stage) do
38
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
38
39
  execute :rake, 'ts:index'
39
40
  end
40
41
  end
@@ -43,9 +44,9 @@ if you alter the structure of your indexes.
43
44
 
44
45
  desc 'Generate Sphinx indexes into the shared path.'
45
46
  task :generate do
46
- on roles fetch(:thinking_sphinx_options) do
47
+ on roles fetch(:thinking_sphinx_roles) do
47
48
  within current_path do
48
- with rails_env: fetch(:stage) do
49
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
49
50
  execute :rake, 'ts:generate'
50
51
  end
51
52
  end
@@ -56,7 +57,7 @@ if you alter the structure of your indexes.
56
57
  task :restart do
57
58
  on roles fetch(:thinking_sphinx_roles) do
58
59
  within current_path do
59
- with rails_env: fetch(:stage) do
60
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
60
61
  %w(stop configure start).each do |task|
61
62
  execute :rake, "ts:#{task}"
62
63
  end
@@ -69,7 +70,7 @@ if you alter the structure of your indexes.
69
70
  task :start do
70
71
  on roles fetch(:thinking_sphinx_roles) do
71
72
  within current_path do
72
- with rails_env: fetch(:stage) do
73
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
73
74
  execute :rake, 'ts:start'
74
75
  end
75
76
  end
@@ -81,7 +82,7 @@ if you alter the structure of your indexes.
81
82
  task :configure do
82
83
  on roles fetch(:thinking_sphinx_roles) do
83
84
  within current_path do
84
- with rails_env: fetch(:stage) do
85
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
85
86
  execute :rake, 'ts:configure'
86
87
  end
87
88
  end
@@ -92,7 +93,7 @@ if you alter the structure of your indexes.
92
93
  task :stop do
93
94
  on roles fetch(:thinking_sphinx_roles) do
94
95
  within current_path do
95
- with rails_env: fetch(:stage) do
96
+ with rails_env: fetch(:thinking_sphinx_rails_env) do
96
97
  execute :rake, 'ts:stop'
97
98
  end
98
99
  end
@@ -74,7 +74,9 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
74
74
  end
75
75
  end
76
76
 
77
- ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
77
+ if settings['distributed_indices'].nil? || settings['distributed_indices']
78
+ ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
79
+ end
78
80
 
79
81
  @preloaded_indices = true
80
82
  end
@@ -117,13 +119,18 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
117
119
  end
118
120
 
119
121
  def log_root
120
- framework_root.join('log').realpath
122
+ real_path 'log'
121
123
  end
122
124
 
123
125
  def framework_root
124
126
  Pathname.new(framework.root)
125
127
  end
126
128
 
129
+ def real_path(*arguments)
130
+ path = framework_root.join(*arguments)
131
+ path.exist? ? path.realpath : path
132
+ end
133
+
127
134
  def settings_to_hash
128
135
  contents = YAML.load(ERB.new(File.read(settings_file)).result)
129
136
  contents && contents[environment] || {}
@@ -142,7 +149,12 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
142
149
  @indices_location = settings['indices_location'] || framework_root.join(
143
150
  'db', 'sphinx', environment
144
151
  ).to_s
145
- @version = settings['version'] || '2.0.6'
152
+ @version = settings['version'] || '2.1.4'
153
+
154
+ if settings['common_sphinx_configuration']
155
+ common.common_sphinx_configuration = true
156
+ indexer.common_sphinx_configuration = true
157
+ end
146
158
 
147
159
  configure_searchd
148
160
 
@@ -152,12 +164,17 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
152
164
  end
153
165
 
154
166
  def tmp_path
155
- path = framework_root.join('tmp')
156
- File.exists?(path) ? path.realpath : path
167
+ real_path 'tmp'
168
+ end
169
+
170
+ def sphinx_sections
171
+ sections = [indexer, searchd]
172
+ sections.unshift common if settings['common_sphinx_configuration']
173
+ sections
157
174
  end
158
175
 
159
176
  def apply_sphinx_settings!
160
- [indexer, searchd].each do |object|
177
+ sphinx_sections.each do |object|
161
178
  settings.each do |key, value|
162
179
  next unless object.class.settings.include?(key.to_sym)
163
180