thinking-sphinx 2.0.0.rc2 → 2.0.0

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