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.
- data/README.textile +6 -0
- data/VERSION +1 -1
- data/features/excerpts.feature +8 -0
- data/features/field_sorting.feature +18 -0
- data/features/searching_across_models.feature +1 -1
- data/features/searching_by_model.feature +0 -7
- data/features/sphinx_scopes.feature +18 -0
- data/features/step_definitions/common_steps.rb +4 -0
- data/features/step_definitions/search_steps.rb +5 -0
- data/features/support/env.rb +4 -5
- data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
- data/features/thinking_sphinx/models/alpha.rb +1 -0
- data/features/thinking_sphinx/models/andrew.rb +17 -0
- data/features/thinking_sphinx/models/person.rb +2 -1
- data/lib/thinking_sphinx.rb +3 -0
- data/lib/thinking_sphinx/active_record.rb +1 -1
- data/lib/thinking_sphinx/active_record/scopes.rb +7 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +38 -8
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +6 -2
- data/lib/thinking_sphinx/association.rb +19 -14
- data/lib/thinking_sphinx/attribute.rb +5 -0
- data/lib/thinking_sphinx/auto_version.rb +2 -0
- data/lib/thinking_sphinx/bundled_search.rb +44 -0
- data/lib/thinking_sphinx/configuration.rb +14 -10
- data/lib/thinking_sphinx/context.rb +4 -2
- data/lib/thinking_sphinx/property.rb +1 -0
- data/lib/thinking_sphinx/railtie.rb +2 -2
- data/lib/thinking_sphinx/search.rb +74 -48
- data/lib/thinking_sphinx/source/sql.rb +1 -1
- data/lib/thinking_sphinx/tasks.rb +7 -0
- data/spec/thinking_sphinx/active_record/scopes_spec.rb +2 -3
- data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +134 -0
- data/spec/thinking_sphinx/association_spec.rb +1 -24
- data/spec/thinking_sphinx/auto_version_spec.rb +8 -0
- data/spec/thinking_sphinx/configuration_spec.rb +11 -4
- data/spec/thinking_sphinx/context_spec.rb +3 -2
- data/spec/thinking_sphinx/search_spec.rb +67 -25
- data/tasks/distribution.rb +0 -6
- data/tasks/testing.rb +25 -15
- metadata +279 -67
data/README.textile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0.0
|
1
|
+
2.0.0
|
data/features/excerpts.feature
CHANGED
@@ -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
|
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
|
data/features/support/env.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require 'cucumber'
|
3
|
-
require 'rspec'
|
4
2
|
require 'fileutils'
|
5
|
-
require '
|
6
|
-
|
7
|
-
require
|
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 => "
|
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"
|
@@ -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,
|
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
|
data/lib/thinking_sphinx.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
43
|
+
:mysql
|
19
44
|
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
20
|
-
|
45
|
+
:postgresql
|
21
46
|
when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
47
|
+
case model.connection.config[:adapter]
|
48
|
+
when "jdbcmysql"
|
49
|
+
:mysql
|
50
|
+
when "jdbcpostgresql"
|
51
|
+
:postgresql
|
26
52
|
else
|
27
|
-
|
53
|
+
model.connection.config[:adapter]
|
28
54
|
end
|
29
55
|
else
|
30
|
-
|
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
|
-
|
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
|
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 { |
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
@@ -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
|