thinking-sphinx 2.0.0.rc2 → 2.0.0

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 (40) hide show
  1. data/README.textile +6 -0
  2. data/VERSION +1 -1
  3. data/features/excerpts.feature +8 -0
  4. data/features/field_sorting.feature +18 -0
  5. data/features/searching_across_models.feature +1 -1
  6. data/features/searching_by_model.feature +0 -7
  7. data/features/sphinx_scopes.feature +18 -0
  8. data/features/step_definitions/common_steps.rb +4 -0
  9. data/features/step_definitions/search_steps.rb +5 -0
  10. data/features/support/env.rb +4 -5
  11. data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
  12. data/features/thinking_sphinx/models/alpha.rb +1 -0
  13. data/features/thinking_sphinx/models/andrew.rb +17 -0
  14. data/features/thinking_sphinx/models/person.rb +2 -1
  15. data/lib/thinking_sphinx.rb +3 -0
  16. data/lib/thinking_sphinx/active_record.rb +1 -1
  17. data/lib/thinking_sphinx/active_record/scopes.rb +7 -0
  18. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +38 -8
  19. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +6 -2
  20. data/lib/thinking_sphinx/association.rb +19 -14
  21. data/lib/thinking_sphinx/attribute.rb +5 -0
  22. data/lib/thinking_sphinx/auto_version.rb +2 -0
  23. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  24. data/lib/thinking_sphinx/configuration.rb +14 -10
  25. data/lib/thinking_sphinx/context.rb +4 -2
  26. data/lib/thinking_sphinx/property.rb +1 -0
  27. data/lib/thinking_sphinx/railtie.rb +2 -2
  28. data/lib/thinking_sphinx/search.rb +74 -48
  29. data/lib/thinking_sphinx/source/sql.rb +1 -1
  30. data/lib/thinking_sphinx/tasks.rb +7 -0
  31. data/spec/thinking_sphinx/active_record/scopes_spec.rb +2 -3
  32. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +134 -0
  33. data/spec/thinking_sphinx/association_spec.rb +1 -24
  34. data/spec/thinking_sphinx/auto_version_spec.rb +8 -0
  35. data/spec/thinking_sphinx/configuration_spec.rb +11 -4
  36. data/spec/thinking_sphinx/context_spec.rb +3 -2
  37. data/spec/thinking_sphinx/search_spec.rb +67 -25
  38. data/tasks/distribution.rb +0 -6
  39. data/tasks/testing.rb +25 -15
  40. metadata +279 -67
@@ -183,3 +183,9 @@ Since I first released this library, there's been quite a few people who have su
183
183
  * Ben Hutton
184
184
  * Alfonso Jiménez
185
185
  * Szymon Nowak
186
+ * Matthew Higgins
187
+ * Anton Sozontov
188
+ * Keith Pitt
189
+ * Lee Capps
190
+ * Sam Goldstein
191
+ * Artem Orlov
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0.rc2
1
+ 2.0.0
@@ -11,3 +11,11 @@ Feature: Generate excerpts for search results
11
11
  And I am searching on comments
12
12
  And I search for "lorem"
13
13
  Then calling content on the first result excerpts object should return "de un sitio mientras que mira su diseño. El punto de usar <span class="match">Lorem</span> Ipsum es que tiene una distribución"
14
+
15
+ Scenario: Excerpt Options
16
+ Given Sphinx is running
17
+ And I am searching on comments
18
+ And I search for "lorem"
19
+ And I provide excerpt option "before_match" with value "<em>"
20
+ And I provide excerpt option "after_match" with value "</em>"
21
+ Then calling content on the first result excerpts object should return "de un sitio mientras que mira su diseño. El punto de usar <em>Lorem</em> Ipsum es que tiene una distribución"
@@ -0,0 +1,18 @@
1
+ Feature: Field Sorting
2
+ In order to sort by strings
3
+ As a developer
4
+ I want to enable sorting by existing fields
5
+
6
+ Background:
7
+ Given Sphinx is running
8
+ And I am searching on people
9
+
10
+ Scenario: Searching with ordering on a sortable field
11
+ When I order by first_name
12
+ Then I should get 20 results
13
+ And the first_name of each result should indicate order
14
+
15
+ Scenario: Sort on a case insensitive sortable field
16
+ When I order by last_name
17
+ Then the first result's "last_name" should be "abbott"
18
+
@@ -7,7 +7,7 @@ Feature: Searching across multiple model
7
7
  Given Sphinx is running
8
8
  When I search for James
9
9
  And I am retrieving the result count
10
- Then I should get a value of 3
10
+ Then I should get a value of 6
11
11
 
12
12
  Scenario: Confirming existance of a document id in a given index
13
13
  Given Sphinx is running
@@ -97,13 +97,6 @@ Feature: Searching on a single model
97
97
  Then I should get 10 results
98
98
  And the value of each result should indicate order
99
99
 
100
- Scenario: Searching with ordering on a sortable field
101
- Given Sphinx is running
102
- And I am searching on people
103
- And I order by first_name
104
- Then I should get 20 results
105
- And the first_name of each result should indicate order
106
-
107
100
  Scenario: Intepreting Sphinx Internal Identifiers
108
101
  Given Sphinx is running
109
102
  And I am searching on people
@@ -48,3 +48,21 @@ Feature: Sphinx Scopes
48
48
  And I am retrieving the scoped result count for "Byrne"
49
49
  Then I should get a value of 1
50
50
 
51
+ Scenario: Default Scope
52
+ Given Sphinx is running
53
+ And I am searching on andrews
54
+ Then I should get 7 results
55
+
56
+ Scenario: Default Scope and additional query terms
57
+ Given Sphinx is running
58
+ And I am searching on andrews
59
+ When I search for "Byrne"
60
+ Then I should get 1 result
61
+
62
+ Scenario: Explicit scope plus search over a default scope
63
+ Given Sphinx is running
64
+ And I am searching on andrews
65
+ When I use the locked_last_name scope
66
+ And I search for "Cecil"
67
+ Then I should get 1 result
68
+
@@ -155,6 +155,10 @@ Then /^the (\w+) of each result should indicate order$/ do |attribute|
155
155
  end
156
156
  end
157
157
 
158
+ Then /^the first result's "([^"]*)" should be "([^"]*)"$/ do |attribute, value|
159
+ results.first.send(attribute.to_sym).should == value
160
+ end
161
+
158
162
  Then /^I can iterate by result and (\w+)$/ do |attribute|
159
163
  iteration = lambda { |result, attr_value|
160
164
  result.should be_kind_of(@model)
@@ -87,3 +87,8 @@ end
87
87
  Then /^the first result should have a (\w+\s?\w*) of (\d+)$/ do |attribute, value|
88
88
  results.first.sphinx_attributes[attribute.gsub(/\s+/, '_')].should == value.to_i
89
89
  end
90
+
91
+ Given /^I provide excerpt option "([a-z_]*)" with value "([^"]*)"$/ do |k, v|
92
+ @options[:excerpt_options] ||= {}
93
+ @options[:excerpt_options][k.to_sym] = v
94
+ end
@@ -1,16 +1,15 @@
1
1
  require 'rubygems'
2
- require 'cucumber'
3
- require 'rspec'
4
2
  require 'fileutils'
5
- require 'ginger'
6
- require 'will_paginate'
7
- require 'active_record'
3
+ require 'bundler'
4
+
5
+ Bundler.require :default, :development
8
6
 
9
7
  $:.unshift File.dirname(__FILE__) + '/../../lib'
10
8
  Dir[File.join(File.dirname(__FILE__), '../../vendor/*/lib')].each do |path|
11
9
  $:.unshift path
12
10
  end
13
11
 
12
+ require 'active_record'
14
13
  require 'cucumber/thinking_sphinx/internal_world'
15
14
 
16
15
  world = Cucumber::ThinkingSphinx::InternalWorld.new
@@ -18,7 +18,7 @@ Person.create :gender => "male", :first_name => "Peter", :middle_initial => "C",
18
18
  Person.create :gender => "female", :first_name => "Hollie", :middle_initial => "C", :last_name => "Hunter", :street_address => "34 Cornish Street", :city => "Kensington", :state => "VIC", :postcode => "3031", :email => "Hollie.C.Hunter@mailinator.com", :birthday => "1954/2/16 00:00:00"
19
19
  Person.create :gender => "male", :first_name => "Jonathan", :middle_initial => "C", :last_name => "Turner", :street_address => "2 Kopkes Road", :city => "Carngham", :state => "VIC", :postcode => "3351", :email => "Jonathan.C.Turner@trashymail.com", :birthday => "1963/8/26 00:00:00"
20
20
  Person.create :gender => "female", :first_name => "Kate", :middle_initial => "S", :last_name => "Doyle", :street_address => "42 Gregory Way", :city => "Mungalup", :state => "WA", :postcode => "6225", :email => "Kate.S.Doyle@mailinator.com", :birthday => "1974/1/5 00:00:00"
21
- Person.create :gender => "male", :first_name => "Harley", :middle_initial => "M", :last_name => "Abbott", :street_address => "39 Faulkner Street", :city => "Tilbuster", :state => "NSW", :postcode => "2350", :email => "Harley.M.Abbott@trashymail.com", :birthday => "1953/10/4 00:00:00"
21
+ Person.create :gender => "male", :first_name => "Harley", :middle_initial => "M", :last_name => "abbott", :street_address => "39 Faulkner Street", :city => "Tilbuster", :state => "NSW", :postcode => "2350", :email => "Harley.M.Abbott@trashymail.com", :birthday => "1953/10/4 00:00:00"
22
22
  Person.create :gender => "male", :first_name => "Morgan", :middle_initial => "E", :last_name => "Iqbal", :street_address => "64 Carlisle Street", :city => "Dysart", :state => "VIC", :postcode => "3660", :email => "Morgan.E.Iqbal@spambob.com", :birthday => "1954/7/6 00:00:00"
23
23
  Person.create :gender => "female", :first_name => "Phoebe", :middle_initial => "T", :last_name => "Wells", :street_address => "10 Mnimbah Road", :city => "Eccleston", :state => "NSW", :postcode => "2311", :email => "Phoebe.T.Wells@trashymail.com", :birthday => "1949/5/27 00:00:00"
24
24
  Person.create :gender => "male", :first_name => "Finley", :middle_initial => "I", :last_name => "Martin", :street_address => "15 Thomas Lane", :city => "Epping", :state => "VIC", :postcode => "3076", :email => "Finley.I.Martin@dodgit.com", :birthday => "1983/3/12 00:00:00"
@@ -14,6 +14,7 @@ class Alpha < ActiveRecord::Base
14
14
 
15
15
  has value, created_at, created_on
16
16
  has cost, :facet => true
17
+ has active
17
18
 
18
19
  set_property :field_weights => {'alternative_name' => 10}
19
20
 
@@ -0,0 +1,17 @@
1
+ require "#{File.dirname(__FILE__)}/person"
2
+
3
+ class Andrew < ActiveRecord::Base
4
+ set_table_name 'people'
5
+
6
+ define_index do
7
+ indexes first_name, last_name, street_address
8
+ end
9
+
10
+ sphinx_scope(:locked_first_name) {
11
+ {:conditions => {:first_name => 'Andrew'}}
12
+ }
13
+ sphinx_scope(:locked_last_name) {
14
+ {:conditions => {:last_name => 'Byrne'}}
15
+ }
16
+ default_sphinx_scope :locked_first_name
17
+ end
@@ -1,6 +1,7 @@
1
1
  class Person < ActiveRecord::Base
2
2
  define_index do
3
- indexes first_name, last_name, :sortable => true
3
+ indexes first_name, :sortable => true
4
+ indexes last_name, :sortable => :insensitive
4
5
 
5
6
  has [first_name, middle_initial, last_name], :as => :name_sort
6
7
  has birthday
@@ -9,6 +9,7 @@ require 'thinking_sphinx/property'
9
9
  require 'thinking_sphinx/active_record'
10
10
  require 'thinking_sphinx/association'
11
11
  require 'thinking_sphinx/attribute'
12
+ require 'thinking_sphinx/bundled_search'
12
13
  require 'thinking_sphinx/configuration'
13
14
  require 'thinking_sphinx/context'
14
15
  require 'thinking_sphinx/excerpter'
@@ -30,6 +31,8 @@ require 'thinking_sphinx/adapters/postgresql_adapter'
30
31
  require 'thinking_sphinx/railtie' if defined?(Rails)
31
32
 
32
33
  module ThinkingSphinx
34
+ mattr_accessor :database_adapter
35
+
33
36
  # A ConnectionError will get thrown when a connection to Sphinx can't be
34
37
  # made.
35
38
  class ConnectionError < StandardError
@@ -349,7 +349,7 @@ module ThinkingSphinx
349
349
  # @return [Integer] Unique record id for the purposes of Sphinx.
350
350
  #
351
351
  def primary_key_for_sphinx
352
- @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
352
+ read_attribute(self.class.primary_key_for_sphinx)
353
353
  end
354
354
 
355
355
  def sphinx_document_id
@@ -53,6 +53,13 @@ module ThinkingSphinx
53
53
 
54
54
  ThinkingSphinx::Search.new(options)
55
55
  end
56
+
57
+ define_method("#{method}_without_default".to_sym) do |*args|
58
+ options = {:classes => classes_option, :ignore_default => true}
59
+ options.merge! block.call(*args)
60
+
61
+ ThinkingSphinx::Search.new(options)
62
+ end
56
63
  end
57
64
  end
58
65
 
@@ -10,24 +10,50 @@ module ThinkingSphinx
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
+ else
20
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{adapter}"
21
+ end
22
+ end
23
+
24
+ def self.adapter_for_model(model)
25
+ case ThinkingSphinx.database_adapter
26
+ when String
27
+ ThinkingSphinx.database_adapter.to_sym
28
+ when NilClass
29
+ standard_adapter_for_model model
30
+ when Proc
31
+ ThinkingSphinx.database_adapter.call model
32
+ else
33
+ ThinkingSphinx.database_adapter
34
+ end
35
+ end
36
+
37
+ def self.standard_adapter_for_model(model)
13
38
  case model.connection.class.name
14
39
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
15
40
  "ActiveRecord::ConnectionAdapters::MysqlplusAdapter",
16
41
  "ActiveRecord::ConnectionAdapters::Mysql2Adapter",
17
42
  "ActiveRecord::ConnectionAdapters::NullDBAdapter"
18
- ThinkingSphinx::MysqlAdapter.new model
43
+ :mysql
19
44
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
20
- ThinkingSphinx::PostgreSQLAdapter.new model
45
+ :postgresql
21
46
  when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
22
- if model.connection.config[:adapter] == "jdbcmysql"
23
- ThinkingSphinx::MysqlAdapter.new model
24
- elsif model.connection.config[:adapter] == "jdbcpostgresql"
25
- ThinkingSphinx::PostgreSQLAdapter.new model
47
+ case model.connection.config[:adapter]
48
+ when "jdbcmysql"
49
+ :mysql
50
+ when "jdbcpostgresql"
51
+ :postgresql
26
52
  else
27
- raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
53
+ model.connection.config[:adapter]
28
54
  end
29
55
  else
30
- raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
56
+ model.connection.class.name
31
57
  end
32
58
  end
33
59
 
@@ -39,6 +65,10 @@ module ThinkingSphinx
39
65
  /bigint/i
40
66
  end
41
67
 
68
+ def downcase(clause)
69
+ "LOWER(#{clause})"
70
+ end
71
+
42
72
  protected
43
73
 
44
74
  def connection
@@ -68,7 +68,7 @@ module ThinkingSphinx
68
68
  end
69
69
 
70
70
  def utc_query_pre
71
- 'SET TIME ZONE UTC'
71
+ "SET TIME ZONE 'UTC'"
72
72
  end
73
73
 
74
74
  private
@@ -119,6 +119,10 @@ module ThinkingSphinx
119
119
  DECLARE j int;
120
120
  DECLARE word_array bytea;
121
121
  BEGIN
122
+ IF COALESCE(word, '') = '' THEN
123
+ return 0;
124
+ END IF;
125
+
122
126
  i = 0;
123
127
  tmp = 4294967295;
124
128
  word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
@@ -139,7 +143,7 @@ module ThinkingSphinx
139
143
  END LOOP;
140
144
  return (tmp # 4294967295);
141
145
  END
142
- $$ IMMUTABLE STRICT LANGUAGE plpgsql;
146
+ $$ IMMUTABLE LANGUAGE plpgsql;
143
147
  SQL
144
148
  execute function, true
145
149
  end
@@ -45,13 +45,10 @@ module ThinkingSphinx
45
45
 
46
46
  # association is polymorphic - create associations for each
47
47
  # non-polymorphic reflection.
48
- polymorphic_classes(ref).collect { |klass|
49
- Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
50
- ref.macro,
51
- "#{ref.name}_#{klass.name}".to_sym,
52
- casted_options(klass, ref),
53
- ref.active_record
54
- )
48
+ polymorphic_classes(ref).collect { |poly_class|
49
+ reflection = depolymorphic_reflection(ref, poly_class)
50
+ klass.reflections[reflection.name] = reflection
51
+ Association.new parent, reflection
55
52
  }
56
53
  end
57
54
 
@@ -66,14 +63,13 @@ module ThinkingSphinx
66
63
  )
67
64
  end
68
65
 
69
- # Returns the association's join SQL statements - and it replaces
70
- # ::ts_join_alias:: with the aliased table name so the generated reflection
71
- # join conditions avoid column name collisions.
72
- #
73
- def to_sql
74
- @join.to_sql.gsub(/::ts_join_alias::/,
66
+ def arel_join
67
+ arel_join = @join.with_join_class(Arel::OuterJoin)
68
+ arel_join.options[:conditions].gsub!(/::ts_join_alias::/,
75
69
  "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
76
- )
70
+ ) if arel_join.options[:conditions].is_a?(String)
71
+
72
+ arel_join
77
73
  end
78
74
 
79
75
  # Returns true if the association - or a parent - is a has_many or
@@ -121,6 +117,15 @@ module ThinkingSphinx
121
117
 
122
118
  private
123
119
 
120
+ def self.depolymorphic_reflection(reflection, klass)
121
+ ::ActiveRecord::Reflection::AssociationReflection.new(
122
+ reflection.macro,
123
+ "#{reflection.name}_#{klass.name}".to_sym,
124
+ casted_options(klass, reflection),
125
+ reflection.active_record
126
+ )
127
+ end
128
+
124
129
  # Returns all the objects that could be currently instantiated from a
125
130
  # polymorphic association. This is pretty damn fast if there's an index on
126
131
  # the foreign type column - but if there isn't, it can take a while if you
@@ -111,6 +111,7 @@ module ThinkingSphinx
111
111
  clause = adapter.crc(clause) if @crc
112
112
  clause = adapter.concatenate(clause, separator) if concat_ws?
113
113
  clause = adapter.group_concatenate(clause, separator) if is_many?
114
+ clause = adapter.downcase(clause) if insensitive?
114
115
 
115
116
  "#{clause} AS #{quote_column(unique_name)}"
116
117
  end
@@ -380,5 +381,9 @@ block:
380
381
  value
381
382
  end
382
383
  end
384
+
385
+ def insensitive?
386
+ @sortable == :insensitive
387
+ end
383
388
  end
384
389
  end
@@ -5,6 +5,8 @@ module ThinkingSphinx
5
5
  case version
6
6
  when '0.9.8', '0.9.9'
7
7
  require "riddle/#{version}"
8
+ when '1.10-beta'
9
+ require 'riddle/1.10'
8
10
  else
9
11
  STDERR.puts %Q{
10
12
  Sphinx cannot be found on your system. You may need to configure the following
@@ -0,0 +1,44 @@
1
+ module ThinkingSphinx
2
+ class BundledSearch
3
+ attr_reader :client
4
+
5
+ def initialize
6
+ @searches = []
7
+ end
8
+
9
+ def search(*args)
10
+ @searches << ThinkingSphinx.search(*args)
11
+ @searches.last.append_to client
12
+ end
13
+
14
+ def search_for_ids(*args)
15
+ @searches << ThinkingSphinx.search_for_ids(*args)
16
+ @searches.last.append_to client
17
+ end
18
+
19
+ def searches
20
+ populate
21
+ @searches
22
+ end
23
+
24
+ private
25
+
26
+ def client
27
+ @client ||= ThinkingSphinx::Configuration.instance.client
28
+ end
29
+
30
+ def populated?
31
+ @populated
32
+ end
33
+
34
+ def populate
35
+ return if populated?
36
+
37
+ @populated = true
38
+
39
+ client.run.each_with_index do |results, index|
40
+ searches[index].populate_from_queue results
41
+ end
42
+ end
43
+ end
44
+ end