thinking-sphinx 1.5.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/README.textile +15 -48
  2. data/VERSION +1 -0
  3. data/features/attribute_transformation.feature +7 -7
  4. data/features/attribute_updates.feature +16 -18
  5. data/features/deleting_instances.feature +13 -16
  6. data/features/excerpts.feature +0 -8
  7. data/features/facets.feature +19 -25
  8. data/features/handling_edits.feature +20 -25
  9. data/features/searching_across_models.feature +1 -1
  10. data/features/searching_by_index.feature +5 -6
  11. data/features/searching_by_model.feature +29 -29
  12. data/features/sphinx_scopes.feature +0 -26
  13. data/features/step_definitions/common_steps.rb +6 -18
  14. data/features/step_definitions/scope_steps.rb +0 -4
  15. data/features/step_definitions/search_steps.rb +4 -9
  16. data/features/support/env.rb +10 -3
  17. data/features/thinking_sphinx/db/fixtures/alphas.rb +10 -8
  18. data/features/thinking_sphinx/db/fixtures/cats.rb +1 -1
  19. data/features/thinking_sphinx/db/fixtures/dogs.rb +1 -1
  20. data/features/thinking_sphinx/db/fixtures/foxes.rb +1 -1
  21. data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
  22. data/features/thinking_sphinx/db/fixtures/posts.rb +1 -5
  23. data/features/thinking_sphinx/db/migrations/create_posts.rb +0 -1
  24. data/features/thinking_sphinx/models/alpha.rb +0 -1
  25. data/features/thinking_sphinx/models/beta.rb +0 -5
  26. data/features/thinking_sphinx/models/developer.rb +1 -6
  27. data/features/thinking_sphinx/models/music.rb +1 -3
  28. data/features/thinking_sphinx/models/person.rb +1 -2
  29. data/features/thinking_sphinx/models/post.rb +0 -1
  30. data/lib/cucumber/thinking_sphinx/external_world.rb +4 -8
  31. data/lib/cucumber/thinking_sphinx/internal_world.rb +27 -36
  32. data/lib/thinking_sphinx.rb +60 -132
  33. data/lib/thinking_sphinx/active_record.rb +98 -124
  34. data/lib/thinking_sphinx/active_record/attribute_updates.rb +13 -17
  35. data/lib/thinking_sphinx/active_record/delta.rb +15 -21
  36. data/lib/thinking_sphinx/active_record/has_many_association.rb +23 -16
  37. data/lib/thinking_sphinx/active_record/scopes.rb +0 -18
  38. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +15 -63
  39. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +0 -4
  40. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +24 -65
  41. data/lib/thinking_sphinx/association.rb +11 -36
  42. data/lib/thinking_sphinx/attribute.rb +85 -92
  43. data/lib/thinking_sphinx/auto_version.rb +3 -21
  44. data/lib/thinking_sphinx/class_facet.rb +3 -8
  45. data/lib/thinking_sphinx/configuration.rb +58 -114
  46. data/lib/thinking_sphinx/context.rb +20 -22
  47. data/lib/thinking_sphinx/core/array.rb +13 -0
  48. data/lib/thinking_sphinx/deltas.rb +0 -2
  49. data/lib/thinking_sphinx/deltas/default_delta.rb +22 -18
  50. data/lib/thinking_sphinx/deploy/capistrano.rb +31 -30
  51. data/lib/thinking_sphinx/excerpter.rb +1 -2
  52. data/lib/thinking_sphinx/facet.rb +35 -45
  53. data/lib/thinking_sphinx/facet_search.rb +24 -58
  54. data/lib/thinking_sphinx/field.rb +0 -18
  55. data/lib/thinking_sphinx/index.rb +36 -38
  56. data/lib/thinking_sphinx/index/builder.rb +59 -74
  57. data/lib/thinking_sphinx/property.rb +45 -66
  58. data/lib/thinking_sphinx/railtie.rb +35 -0
  59. data/lib/thinking_sphinx/search.rb +250 -506
  60. data/lib/thinking_sphinx/source.rb +31 -50
  61. data/lib/thinking_sphinx/source/internal_properties.rb +3 -8
  62. data/lib/thinking_sphinx/source/sql.rb +31 -71
  63. data/lib/thinking_sphinx/tasks.rb +27 -48
  64. data/spec/thinking_sphinx/active_record/delta_spec.rb +41 -36
  65. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +0 -96
  66. data/spec/thinking_sphinx/active_record/scopes_spec.rb +29 -29
  67. data/spec/thinking_sphinx/active_record_spec.rb +169 -140
  68. data/spec/thinking_sphinx/association_spec.rb +2 -20
  69. data/spec/thinking_sphinx/attribute_spec.rb +97 -101
  70. data/spec/thinking_sphinx/auto_version_spec.rb +11 -75
  71. data/spec/thinking_sphinx/configuration_spec.rb +62 -63
  72. data/spec/thinking_sphinx/context_spec.rb +66 -66
  73. data/spec/thinking_sphinx/facet_search_spec.rb +99 -99
  74. data/spec/thinking_sphinx/facet_spec.rb +4 -30
  75. data/spec/thinking_sphinx/field_spec.rb +3 -17
  76. data/spec/thinking_sphinx/index/builder_spec.rb +132 -169
  77. data/spec/thinking_sphinx/index_spec.rb +39 -45
  78. data/spec/thinking_sphinx/search_methods_spec.rb +33 -37
  79. data/spec/thinking_sphinx/search_spec.rb +269 -491
  80. data/spec/thinking_sphinx/source_spec.rb +48 -62
  81. data/spec/thinking_sphinx_spec.rb +49 -49
  82. data/tasks/distribution.rb +46 -0
  83. data/tasks/testing.rb +74 -0
  84. metadata +123 -199
  85. data/features/field_sorting.feature +0 -18
  86. data/features/thinking_sphinx/db/.gitignore +0 -1
  87. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +0 -1
  88. data/features/thinking_sphinx/models/andrew.rb +0 -17
  89. data/lib/thinking-sphinx.rb +0 -1
  90. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +0 -21
  91. data/lib/thinking_sphinx/bundled_search.rb +0 -40
  92. data/lib/thinking_sphinx/connection.rb +0 -71
  93. data/lib/thinking_sphinx/deltas/delete_job.rb +0 -16
  94. data/lib/thinking_sphinx/deltas/index_job.rb +0 -17
  95. data/lib/thinking_sphinx/rails_additions.rb +0 -181
  96. data/spec/fixtures/data.sql +0 -32
  97. data/spec/fixtures/database.yml.default +0 -3
  98. data/spec/fixtures/models.rb +0 -161
  99. data/spec/fixtures/structure.sql +0 -146
  100. data/spec/spec_helper.rb +0 -54
  101. data/spec/sphinx_helper.rb +0 -67
  102. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +0 -163
  103. data/spec/thinking_sphinx/connection_spec.rb +0 -77
  104. data/spec/thinking_sphinx/rails_additions_spec.rb +0 -203
@@ -3,7 +3,7 @@ module ThinkingSphinx
3
3
  # This module contains all the delta-related code for models. There isn't
4
4
  # really anything you need to call manually in here - except perhaps
5
5
  # index_delta, but not sure what reason why.
6
- #
6
+ #
7
7
  module Delta
8
8
  # Code for after_commit callback is written by Eli Miller:
9
9
  # http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
@@ -16,42 +16,36 @@ module ThinkingSphinx
16
16
  # if running in the test environment.
17
17
  #
18
18
  def index_delta(instance = nil)
19
- delta_objects.each { |obj| obj.index(self, instance) }
19
+ delta_object.index(self, instance)
20
20
  end
21
-
22
- def delta_objects
23
- self.sphinx_indexes.collect(&:delta_object).compact
21
+
22
+ def delta_object
23
+ self.sphinx_indexes.first.delta_object
24
24
  end
25
25
  end
26
-
26
+
27
27
  def toggled_delta?
28
- self.class.delta_objects.any? { |obj| obj.toggled(self) }
28
+ self.class.delta_object.toggled(self)
29
29
  end
30
-
30
+
31
31
  private
32
-
32
+
33
33
  # Set the delta value for the model to be true.
34
34
  def toggle_delta
35
- self.class.delta_objects.each { |obj|
36
- obj.toggle(self)
37
- } if should_toggle_delta?
35
+ self.class.delta_object.toggle(self) if should_toggle_delta?
38
36
  end
39
-
37
+
40
38
  # Build the delta index for the related model. This won't be called
41
39
  # if running in the test environment.
42
- #
40
+ #
43
41
  def index_delta
44
- self.class.index_delta(self) if self.class.delta_objects.any? { |obj|
45
- obj.toggled(self)
46
- }
42
+ self.class.index_delta(self) if self.class.delta_object.toggled(self)
47
43
  end
48
-
44
+
49
45
  def should_toggle_delta?
50
- return fire_delta? if respond_to?(:fire_delta?)
51
-
52
46
  self.new_record? || indexed_data_changed?
53
47
  end
54
-
48
+
55
49
  def indexed_data_changed?
56
50
  sphinx_indexes.any? { |index|
57
51
  index.fields.any? { |field| field.changed?(self) } ||
@@ -2,23 +2,26 @@ module ThinkingSphinx
2
2
  module ActiveRecord
3
3
  module HasManyAssociation
4
4
  def search(*args)
5
- @reflection.klass.search(*association_args(args))
6
- end
7
-
8
- def facets(*args)
9
- @reflection.klass.facets(*association_args(args))
10
- end
11
-
12
- private
13
-
14
- def association_args(args)
15
- options = args.extract_options!
5
+ options = args.extract_options!
16
6
  options[:with] ||= {}
17
7
  options[:with].merge! default_filter
18
-
19
- args + [options]
8
+
9
+ args << options
10
+ @reflection.klass.search(*args)
11
+ end
12
+
13
+ def method_missing(method, *args, &block)
14
+ if responds_to_scope(method)
15
+ @reflection.klass.
16
+ search(:with => default_filter).
17
+ send(method, *args, &block)
18
+ else
19
+ super
20
+ end
20
21
  end
21
-
22
+
23
+ private
24
+
22
25
  def attribute_for_foreign_key
23
26
  foreign_key = @reflection.primary_key_name
24
27
  stack = [@reflection.options[:through]].compact
@@ -27,8 +30,7 @@ module ThinkingSphinx
27
30
  (@reflection.klass.sphinx_indexes || []).each do |index|
28
31
  attribute = index.attributes.detect { |attrib|
29
32
  attrib.columns.length == 1 &&
30
- attrib.columns.first.__name == foreign_key.to_sym ||
31
- attrib.alias == foreign_key.to_sym
33
+ attrib.columns.first.__name == foreign_key.to_sym
32
34
  }
33
35
  return attribute unless attribute.nil?
34
36
  end
@@ -39,6 +41,11 @@ module ThinkingSphinx
39
41
  def default_filter
40
42
  {attribute_for_foreign_key.unique_name => @owner.id}
41
43
  end
44
+
45
+ def responds_to_scope(scope)
46
+ @reflection.klass.respond_to?(:sphinx_scopes) &&
47
+ @reflection.klass.sphinx_scopes.include?(scope)
48
+ end
42
49
  end
43
50
  end
44
51
  end
@@ -17,7 +17,6 @@ module ThinkingSphinx
17
17
  # The scope is automatically applied when the search method is called. It
18
18
  # will only be applied if it is an existing sphinx_scope.
19
19
  def default_sphinx_scope(sphinx_scope_name)
20
- add_sphinx_scopes_support_to_has_many_associations
21
20
  @default_sphinx_scope = sphinx_scope_name
22
21
  end
23
22
 
@@ -44,8 +43,6 @@ module ThinkingSphinx
44
43
  # @articles = Article.latest_first.search 'pancakes'
45
44
  #
46
45
  def sphinx_scope(method, &block)
47
- add_sphinx_scopes_support_to_has_many_associations
48
-
49
46
  @sphinx_scopes ||= []
50
47
  @sphinx_scopes << method
51
48
 
@@ -56,13 +53,6 @@ module ThinkingSphinx
56
53
 
57
54
  ThinkingSphinx::Search.new(options)
58
55
  end
59
-
60
- define_method("#{method}_without_default".to_sym) do |*args|
61
- options = {:classes => classes_option, :ignore_default => true}
62
- options.merge! block.call(*args)
63
-
64
- ThinkingSphinx::Search.new(options)
65
- end
66
56
  end
67
57
  end
68
58
 
@@ -79,14 +69,6 @@ module ThinkingSphinx
79
69
 
80
70
  sphinx_scopes.clear
81
71
  end
82
-
83
- def add_sphinx_scopes_support_to_has_many_associations
84
- scope_mixin = ::ThinkingSphinx::ActiveRecord::HasManyAssociationWithScopes
85
-
86
- ::ActiveRecord::Associations::HasManyAssociation.send(:include, scope_mixin)
87
- ::ActiveRecord::Associations::HasManyThroughAssociation.send(:include, scope_mixin)
88
- end
89
-
90
72
  end
91
73
  end
92
74
  end
@@ -3,90 +3,42 @@ module ThinkingSphinx
3
3
  def initialize(model)
4
4
  @model = model
5
5
  end
6
-
6
+
7
7
  def setup
8
8
  # Deliberately blank - subclasses should do something though. Well, if
9
9
  # they need to.
10
10
  end
11
-
11
+
12
12
  def self.detect(model)
13
- adapter = adapter_for_model model
14
- case adapter
15
- when :mysql
16
- ThinkingSphinx::MysqlAdapter.new model
17
- when :postgresql
18
- ThinkingSphinx::PostgreSQLAdapter.new model
19
- when Class
20
- adapter.new model
21
- else
22
- raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{adapter}"
23
- end
24
- end
25
-
26
- def self.adapter_for_model(model)
27
- case ThinkingSphinx.database_adapter
28
- when String
29
- ThinkingSphinx.database_adapter.to_sym
30
- when NilClass
31
- standard_adapter_for_model model
32
- when Proc
33
- ThinkingSphinx.database_adapter.call model
34
- else
35
- ThinkingSphinx.database_adapter
36
- end
37
- end
38
-
39
- def self.standard_adapter_for_model(model)
40
13
  case model.connection.class.name
41
14
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
42
- "ActiveRecord::ConnectionAdapters::MysqlplusAdapter",
43
- "ActiveRecord::ConnectionAdapters::Mysql2Adapter",
44
- "ActiveRecord::ConnectionAdapters::NullDBAdapter"
45
- :mysql
15
+ "ActiveRecord::ConnectionAdapters::MysqlplusAdapter"
16
+ ThinkingSphinx::MysqlAdapter.new model
46
17
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
47
- :postgresql
18
+ ThinkingSphinx::PostgreSQLAdapter.new model
48
19
  when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
49
- case model.connection.config[:adapter]
50
- when "jdbcmysql"
51
- :mysql
52
- when "jdbcpostgresql"
53
- :postgresql
54
- when "jdbc"
55
- match = /^jdbc:(mysql|postgresql):\/\//.match(model.connection.config[:url])
56
- if match
57
- match[1].to_sym
58
- else
59
- model.connection.config[:adapter]
60
- end
20
+ if model.connection.config[:adapter] == "jdbcmysql"
21
+ ThinkingSphinx::MysqlAdapter.new model
22
+ elsif model.connection.config[:adapter] == "jdbcpostgresql"
23
+ ThinkingSphinx::PostgreSQLAdapter.new model
61
24
  else
62
- model.connection.config[:adapter].to_sym
25
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
63
26
  end
64
27
  else
65
- model.connection.class.name
28
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
66
29
  end
67
30
  end
68
-
31
+
69
32
  def quote_with_table(column)
70
33
  "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
71
34
  end
72
-
35
+
73
36
  def bigint_pattern
74
37
  /bigint/i
75
38
  end
76
-
77
- def downcase(clause)
78
- "LOWER(#{clause})"
79
- end
80
-
81
- def case(expression, pairs, default)
82
- "CASE #{expression} " +
83
- pairs.keys.inject('') { |string, key|
84
- string + "WHEN '#{key}' THEN #{pairs[key]} "
85
- } + "ELSE #{default} END"
86
- end
87
-
39
+
88
40
  protected
89
-
41
+
90
42
  def connection
91
43
  @connection ||= @model.connection
92
44
  end
@@ -28,10 +28,6 @@ module ThinkingSphinx
28
28
  "CAST(#{clause} AS UNSIGNED)"
29
29
  end
30
30
 
31
- def cast_to_int(clause)
32
- "CAST(#{clause} AS SIGNED)"
33
- end
34
-
35
31
  def convert_nulls(clause, default = '')
36
32
  default = "'#{default}'" if default.is_a?(String)
37
33
 
@@ -4,49 +4,37 @@ module ThinkingSphinx
4
4
  create_array_accum_function
5
5
  create_crc32_function
6
6
  end
7
-
7
+
8
8
  def sphinx_identifier
9
9
  "pgsql"
10
10
  end
11
-
11
+
12
12
  def concatenate(clause, separator = ' ')
13
13
  if clause[/^COALESCE/]
14
14
  clause.split('), ').join(") || '#{separator}' || ")
15
15
  else
16
16
  clause.split(', ').collect { |field|
17
- "CAST(COALESCE(#{field}::varchar, '') as varchar)"
17
+ "CAST(COALESCE(#{field}, '') as varchar)"
18
18
  }.join(" || '#{separator}' || ")
19
19
  end
20
20
  end
21
-
21
+
22
22
  def group_concatenate(clause, separator = ' ')
23
- if server_version >= 80400
24
- "array_to_string(array_agg(COALESCE(#{clause}, '0')), '#{separator}')"
25
- else
26
- "array_to_string(array_accum(COALESCE(#{clause}, '0')), '#{separator}')"
27
- end
23
+ "array_to_string(array_accum(COALESCE(#{clause}, '0')), '#{separator}')"
28
24
  end
29
-
25
+
30
26
  def cast_to_string(clause)
31
27
  clause
32
28
  end
33
-
29
+
34
30
  def cast_to_datetime(clause)
35
- if ThinkingSphinx::Configuration.instance.use_64_bit
36
- "cast(floor(extract(epoch from #{clause})) as bigint)"
37
- else
38
- "cast(floor(extract(epoch from #{clause})) as int)"
39
- end
31
+ "cast(extract(epoch from #{clause}) as int)"
40
32
  end
41
-
33
+
42
34
  def cast_to_unsigned(clause)
43
35
  clause
44
36
  end
45
-
46
- def cast_to_int(clause)
47
- "#{clause}::INT8"
48
- end
49
-
37
+
50
38
  def convert_nulls(clause, default = '')
51
39
  default = case default
52
40
  when String
@@ -58,44 +46,34 @@ module ThinkingSphinx
58
46
  else
59
47
  default
60
48
  end
61
-
49
+
62
50
  "COALESCE(#{clause}, #{default})"
63
51
  end
64
-
52
+
65
53
  def boolean(value)
66
54
  value ? 'TRUE' : 'FALSE'
67
55
  end
68
-
56
+
69
57
  def crc(clause, blank_to_null = false)
70
58
  clause = "NULLIF(#{clause},'')" if blank_to_null
71
59
  "crc32(#{clause})"
72
60
  end
73
-
61
+
74
62
  def utf8_query_pre
75
63
  nil
76
64
  end
77
-
65
+
78
66
  def time_difference(diff)
79
67
  "current_timestamp - interval '#{diff} seconds'"
80
68
  end
81
-
69
+
82
70
  def utc_query_pre
83
- "SET TIME ZONE 'UTC'"
71
+ 'SET TIME ZONE UTC'
84
72
  end
85
-
73
+
86
74
  private
87
-
75
+
88
76
  def execute(command, output_error = false)
89
- if RUBY_PLATFORM == 'java'
90
- connection.transaction do
91
- execute_command command, output_error
92
- end
93
- else
94
- execute_command command, output_error
95
- end
96
- end
97
-
98
- def execute_command(command, output_error = false)
99
77
  connection.execute "begin"
100
78
  connection.execute "savepoint ts"
101
79
  begin
@@ -107,11 +85,9 @@ module ThinkingSphinx
107
85
  connection.execute "release savepoint ts"
108
86
  connection.execute "commit"
109
87
  end
110
-
88
+
111
89
  def create_array_accum_function
112
- if server_version >= 80311
113
- return
114
- elsif server_version > 80200
90
+ if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
115
91
  execute <<-SQL
116
92
  CREATE AGGREGATE array_accum (anyelement)
117
93
  (
@@ -132,7 +108,7 @@ module ThinkingSphinx
132
108
  SQL
133
109
  end
134
110
  end
135
-
111
+
136
112
  def create_crc32_function
137
113
  execute "CREATE LANGUAGE 'plpgsql';"
138
114
  function = <<-SQL
@@ -141,16 +117,10 @@ module ThinkingSphinx
141
117
  DECLARE tmp bigint;
142
118
  DECLARE i int;
143
119
  DECLARE j int;
144
- DECLARE byte_length int;
145
120
  DECLARE word_array bytea;
146
121
  BEGIN
147
- IF COALESCE(word, '') = '' THEN
148
- return 0;
149
- END IF;
150
-
151
122
  i = 0;
152
123
  tmp = 4294967295;
153
- byte_length = bit_length(word) / 8;
154
124
  word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
155
125
  LOOP
156
126
  tmp = (tmp # get_byte(word_array, i))::bigint;
@@ -163,26 +133,15 @@ module ThinkingSphinx
163
133
  EXIT;
164
134
  END IF;
165
135
  END LOOP;
166
- IF i >= byte_length THEN
136
+ IF i >= char_length(word) THEN
167
137
  EXIT;
168
138
  END IF;
169
139
  END LOOP;
170
140
  return (tmp # 4294967295);
171
141
  END
172
- $$ IMMUTABLE LANGUAGE plpgsql;
142
+ $$ IMMUTABLE STRICT LANGUAGE plpgsql;
173
143
  SQL
174
144
  execute function, true
175
145
  end
176
-
177
- def server_version
178
- if RUBY_PLATFORM == 'java'
179
- (connection.raw_connection.connection.server_major_version * 10000) +
180
- (connection.raw_connection.connection.server_minor_version * 100)
181
- elsif connection.raw_connection.respond_to?(:server_version)
182
- connection.raw_connection.server_version
183
- else
184
- 0
185
- end
186
- end
187
146
  end
188
147
  end