thinking-sphinx 3.1.0 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
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