pg_search 0.1.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +21 -7
- data/README.rdoc +15 -1
- data/Rakefile +2 -0
- data/gemfiles/Gemfile.common +1 -1
- data/lib/pg_search/configuration.rb +9 -5
- data/lib/pg_search/configuration/association.rb +34 -0
- data/lib/pg_search/configuration/column.rb +6 -7
- data/lib/pg_search/features/tsearch.rb +8 -4
- data/lib/pg_search/scope_options.rb +2 -4
- data/lib/pg_search/version.rb +1 -1
- data/spec/associations_spec.rb +54 -0
- data/spec/pg_search_spec.rb +27 -28
- metadata +7 -11
data/CHANGELOG
CHANGED
@@ -1,18 +1,32 @@
|
|
1
|
+
### 0.2
|
2
|
+
|
3
|
+
* Set dictionary to :simple by default for :tsearch. Before it was unset,
|
4
|
+
which would fall back to PostgreSQL's default dictionary, usually
|
5
|
+
"english".
|
6
|
+
|
7
|
+
* Fix a bug with search strings containing a colon ":"
|
8
|
+
|
9
|
+
* Improve performance of :associated_against by only doing one INNER JOIN per
|
10
|
+
association
|
11
|
+
|
1
12
|
### 0.1.1
|
2
13
|
|
3
|
-
Fix a bug with dmetaphone searches containing " w " (which dmetaphone maps
|
4
|
-
an empty string)
|
14
|
+
* Fix a bug with dmetaphone searches containing " w " (which dmetaphone maps
|
15
|
+
to an empty string)
|
5
16
|
|
6
17
|
### 0.1
|
7
18
|
|
8
|
-
Change API to {:ignoring => :accents} from {:normalizing => :diacritics}
|
9
|
-
|
10
|
-
|
19
|
+
* Change API to {:ignoring => :accents} from {:normalizing => :diacritics}
|
20
|
+
|
21
|
+
* Improve documentation
|
22
|
+
|
23
|
+
* Fix bug where :associated_against would not work without an :against
|
24
|
+
present
|
11
25
|
|
12
26
|
### 0.0.2
|
13
27
|
|
14
|
-
Fix gem ownership.
|
28
|
+
* Fix gem ownership.
|
15
29
|
|
16
30
|
### 0.0.1
|
17
31
|
|
18
|
-
Initial release.
|
32
|
+
* Initial release.
|
data/README.rdoc
CHANGED
@@ -6,6 +6,8 @@
|
|
6
6
|
|
7
7
|
PgSearch builds named scopes that take advantage of PostgreSQL's full text search
|
8
8
|
|
9
|
+
Read the blog post introducing PgSearch at http://bit.ly/pg_search
|
10
|
+
|
9
11
|
== INSTALL
|
10
12
|
|
11
13
|
gem install pg_search
|
@@ -203,7 +205,7 @@ PostgreSQL's full text search matches on whole words by default. If you want to
|
|
203
205
|
|
204
206
|
===== :dictionary
|
205
207
|
|
206
|
-
PostgreSQL full text search also support multiple dictionaries for stemming.
|
208
|
+
PostgreSQL full text search also support multiple dictionaries for stemming. You can learn more about how dictionaries work by reading the {PostgreSQL documention}[http://www.postgresql.org/docs/current/static/textsearch-dictionaries.html]. If you use one of the language dictionaries, such as "english", then variants of words (e.g. "jumping" and "jumped") will match each other. If you don't want stemming, you should pick the "simple" dictionary which does not do any stemming. If you don't specify a dictionary, the "simple" dictionary will be used.
|
207
209
|
|
208
210
|
class BoringTweet < ActiveRecord::Base
|
209
211
|
include PgSearch
|
@@ -306,6 +308,18 @@ Ignoring accents uses the {unaccent contrib package}[http://www.postgresql.org/d
|
|
306
308
|
* Postgresql
|
307
309
|
* Postgresql contrib modules for certain features
|
308
310
|
|
311
|
+
== ATTRIBUTIONS
|
312
|
+
|
313
|
+
PgSearch would not have been possible without inspiration from
|
314
|
+
{texticle}[https://github.com/tenderlove/texticle]. Thanks to
|
315
|
+
{Aaron Patterson}[http://tenderlovemaking.com/]!
|
316
|
+
|
317
|
+
== CONTRIBUTIONS AND FEEDBACK
|
318
|
+
|
319
|
+
Welcomed! Feel free to join and contribute to our {public Pivotal Tracker project}[https://www.pivotaltracker.com/projects/228645] where we manage new feature ideas and bugs.
|
320
|
+
|
321
|
+
We also have a {Google Group}[http://groups.google.com/group/casecommons-dev] for discussing pg_search and other Case Commons open source projects.
|
322
|
+
|
309
323
|
== LICENSE
|
310
324
|
|
311
325
|
MIT
|
data/Rakefile
CHANGED
@@ -4,6 +4,7 @@ Bundler::GemHelper.install_tasks
|
|
4
4
|
task :default => :spec
|
5
5
|
|
6
6
|
environments = %w[rails2 rails3]
|
7
|
+
major, minor, revision = RUBY_VERSION.split(".").map{|str| str.to_i }
|
7
8
|
|
8
9
|
in_environment = lambda do |environment, command|
|
9
10
|
sh %Q{export BUNDLE_GEMFILE="gemfiles/#{environment}/Gemfile"; bundle update && bundle exec #{command}}
|
@@ -11,6 +12,7 @@ end
|
|
11
12
|
|
12
13
|
in_all_environments = lambda do |command|
|
13
14
|
environments.each do |environment|
|
15
|
+
next if environment == "rails2" && major == 1 && minor > 8
|
14
16
|
puts "\n---#{environment}---\n"
|
15
17
|
in_environment.call(environment, command)
|
16
18
|
end
|
data/gemfiles/Gemfile.common
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "pg_search/configuration/association"
|
1
2
|
require "pg_search/configuration/column"
|
2
3
|
|
3
4
|
module PgSearch
|
@@ -20,15 +21,18 @@ module PgSearch
|
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
+
def associations
|
24
25
|
return [] unless @options[:associated_against]
|
25
|
-
@options[:associated_against].map do |association,
|
26
|
-
|
27
|
-
|
28
|
-
end
|
26
|
+
@options[:associated_against].map do |association, column_names|
|
27
|
+
association = Association.new(@model, association, column_names)
|
28
|
+
association
|
29
29
|
end.flatten
|
30
30
|
end
|
31
31
|
|
32
|
+
def associated_columns
|
33
|
+
associations.map(&:columns).flatten
|
34
|
+
end
|
35
|
+
|
32
36
|
def query
|
33
37
|
@options[:query].to_s
|
34
38
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "digest"
|
2
|
+
|
3
|
+
module PgSearch
|
4
|
+
class Configuration
|
5
|
+
class Association
|
6
|
+
attr_reader :columns
|
7
|
+
|
8
|
+
def initialize(model, name, column_names)
|
9
|
+
@model = model
|
10
|
+
@name = name
|
11
|
+
@columns = Array(column_names).map do |column_name, weight|
|
12
|
+
Column.new(column_name, weight, @model, self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def table_name
|
17
|
+
@model.reflect_on_association(@name).table_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def join(primary_key)
|
21
|
+
selects = columns.map do |column|
|
22
|
+
"string_agg(#{column.full_name}, ' ') AS #{column.alias}"
|
23
|
+
end.join(", ")
|
24
|
+
relation = @model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
|
25
|
+
"LEFT OUTER JOIN (#{relation.to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def subselect_alias
|
29
|
+
subselect_name = ["pg_search", table_name, @name, "subselect"].compact.join('_')
|
30
|
+
"pg_search_#{Digest::SHA2.hexdigest(subselect_name)}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
1
3
|
module PgSearch
|
2
4
|
class Configuration
|
3
5
|
class Column
|
@@ -11,7 +13,7 @@ module PgSearch
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def table
|
14
|
-
foreign? ? @
|
16
|
+
foreign? ? @association.table_name : @model.table_name
|
15
17
|
end
|
16
18
|
|
17
19
|
def full_name
|
@@ -20,7 +22,7 @@ module PgSearch
|
|
20
22
|
|
21
23
|
def to_sql
|
22
24
|
name = if foreign?
|
23
|
-
"#{
|
25
|
+
"#{@association.subselect_alias}.#{self.alias}"
|
24
26
|
else
|
25
27
|
full_name
|
26
28
|
end
|
@@ -32,11 +34,8 @@ module PgSearch
|
|
32
34
|
end
|
33
35
|
|
34
36
|
def alias
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def subselect_alias
|
39
|
-
"#{self.alias}_subselect"
|
37
|
+
name = [association.subselect_alias, @column_name].compact.join('_')
|
38
|
+
"pg_search_#{Digest::SHA2.hexdigest(name)}"
|
40
39
|
end
|
41
40
|
end
|
42
41
|
end
|
@@ -24,7 +24,7 @@ module PgSearch
|
|
24
24
|
private
|
25
25
|
|
26
26
|
def interpolations
|
27
|
-
{:query => @query.to_s, :dictionary =>
|
27
|
+
{:query => @query.to_s, :dictionary => dictionary.to_s}
|
28
28
|
end
|
29
29
|
|
30
30
|
def document
|
@@ -35,7 +35,7 @@ module PgSearch
|
|
35
35
|
return "''" if @query.blank?
|
36
36
|
|
37
37
|
@query.split(" ").compact.map do |term|
|
38
|
-
sanitized_term = term.gsub(/['
|
38
|
+
sanitized_term = term.gsub(/['?\-\\:]/, " ")
|
39
39
|
|
40
40
|
term_sql = @normalizer.add_normalization(connection.quote(sanitized_term))
|
41
41
|
|
@@ -45,13 +45,13 @@ module PgSearch
|
|
45
45
|
# Add tsearch prefix operator if we're using a prefix search.
|
46
46
|
tsquery_sql = "#{tsquery_sql} || #{connection.quote(':*')}" if @options[:prefix]
|
47
47
|
|
48
|
-
"to_tsquery(
|
48
|
+
"to_tsquery(:dictionary, #{tsquery_sql})"
|
49
49
|
end.join(" && ")
|
50
50
|
end
|
51
51
|
|
52
52
|
def tsdocument
|
53
53
|
@columns.map do |search_column|
|
54
|
-
tsvector = "to_tsvector(
|
54
|
+
tsvector = "to_tsvector(:dictionary, #{@normalizer.add_normalization(search_column.to_sql)})"
|
55
55
|
search_column.weight.nil? ? tsvector : "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
|
56
56
|
end.join(" || ")
|
57
57
|
end
|
@@ -59,6 +59,10 @@ module PgSearch
|
|
59
59
|
def tsearch_rank
|
60
60
|
["ts_rank((#{tsdocument}), (#{tsquery}))", interpolations]
|
61
61
|
end
|
62
|
+
|
63
|
+
def dictionary
|
64
|
+
@options[:dictionary] || :simple
|
65
|
+
end
|
62
66
|
end
|
63
67
|
end
|
64
68
|
end
|
@@ -39,10 +39,8 @@ module PgSearch
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def joins
|
42
|
-
@config.
|
43
|
-
|
44
|
-
relation = model.joins(column.association).select("#{primary_key} AS id, #{select}").group(primary_key)
|
45
|
-
"LEFT OUTER JOIN (#{relation.to_sql}) #{column.subselect_alias} ON #{column.subselect_alias}.id = #{primary_key}"
|
42
|
+
@config.associations.map do |association|
|
43
|
+
association.join(primary_key)
|
46
44
|
end.join(' ')
|
47
45
|
end
|
48
46
|
|
data/lib/pg_search/version.rb
CHANGED
data/spec/associations_spec.rb
CHANGED
@@ -237,6 +237,60 @@ describe PgSearch do
|
|
237
237
|
end
|
238
238
|
end
|
239
239
|
end
|
240
|
+
|
241
|
+
context "against multiple attributes on one association" do
|
242
|
+
with_model :associated_model do
|
243
|
+
table do |t|
|
244
|
+
t.string 'title'
|
245
|
+
t.text 'author'
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
with_model :model_with_association do
|
250
|
+
table do |t|
|
251
|
+
t.belongs_to 'another_model'
|
252
|
+
end
|
253
|
+
|
254
|
+
model do
|
255
|
+
include PgSearch
|
256
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
257
|
+
|
258
|
+
pg_search_scope :with_associated, :associated_against => {:another_model => [:title, :author]}
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should only do one join" do
|
263
|
+
included = [
|
264
|
+
ModelWithAssociation.create!(
|
265
|
+
:another_model => AssociatedModel.create!(
|
266
|
+
:title => "foo",
|
267
|
+
:author => "bar"
|
268
|
+
)
|
269
|
+
),
|
270
|
+
ModelWithAssociation.create!(
|
271
|
+
:another_model => AssociatedModel.create!(
|
272
|
+
:title => "foo bar",
|
273
|
+
:author => "baz"
|
274
|
+
)
|
275
|
+
)
|
276
|
+
]
|
277
|
+
excluded = [
|
278
|
+
ModelWithAssociation.create!(
|
279
|
+
:another_model => AssociatedModel.create!(
|
280
|
+
:title => "foo",
|
281
|
+
:author => "baz"
|
282
|
+
)
|
283
|
+
)
|
284
|
+
]
|
285
|
+
|
286
|
+
results = ModelWithAssociation.with_associated('foo bar')
|
287
|
+
|
288
|
+
results.to_sql.scan("INNER JOIN").length.should == 1
|
289
|
+
included.each { |object| results.should include(object) }
|
290
|
+
excluded.each { |object| results.should_not include(object) }
|
291
|
+
end
|
292
|
+
|
293
|
+
end
|
240
294
|
end
|
241
295
|
else
|
242
296
|
context "without Arel support" do
|
data/spec/pg_search_spec.rb
CHANGED
@@ -198,13 +198,13 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
198
198
|
results.should_not include(excluded)
|
199
199
|
end
|
200
200
|
|
201
|
-
it "returns rows that match the query when stemmed by the default dictionary
|
202
|
-
included =
|
203
|
-
|
201
|
+
it "returns rows that match the query exactly and not those that match the query when stemmed by the default english dictionary" do
|
202
|
+
included = model_with_pg_search.create!(:content => "jumped")
|
203
|
+
excluded = [model_with_pg_search.create!(:content => "jump"),
|
204
204
|
model_with_pg_search.create!(:content => "jumping")]
|
205
205
|
|
206
|
-
results = model_with_pg_search.search_content("
|
207
|
-
results.should
|
206
|
+
results = model_with_pg_search.search_content("jumped")
|
207
|
+
results.should == [included]
|
208
208
|
end
|
209
209
|
|
210
210
|
it "returns rows that match sorted by rank" do
|
@@ -241,9 +241,9 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
241
241
|
end
|
242
242
|
|
243
243
|
it "returns rows that match a query with characters that are invalid in a tsquery expression" do
|
244
|
-
included = model_with_pg_search.create!(:content => "(Foo.) Bar?, \\")
|
244
|
+
included = model_with_pg_search.create!(:content => "(:Foo.) Bar?, \\")
|
245
245
|
|
246
|
-
results = model_with_pg_search.search_content("foo bar .,?() \\")
|
246
|
+
results = model_with_pg_search.search_content("foo :bar .,?() \\")
|
247
247
|
results.should == [included]
|
248
248
|
end
|
249
249
|
|
@@ -349,32 +349,24 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
349
349
|
end
|
350
350
|
end
|
351
351
|
|
352
|
-
context "with the
|
352
|
+
context "with the english dictionary" do
|
353
353
|
before do
|
354
354
|
model_with_pg_search.class_eval do
|
355
|
-
pg_search_scope :
|
356
|
-
|
357
|
-
pg_search_scope :search_title_with_simple,
|
358
|
-
:against => :title,
|
355
|
+
pg_search_scope :search_content_with_english,
|
356
|
+
:against => :content,
|
359
357
|
:using => {
|
360
|
-
:tsearch => {:dictionary => :
|
358
|
+
:tsearch => {:dictionary => :english}
|
361
359
|
}
|
362
360
|
end
|
363
361
|
end
|
364
362
|
|
365
|
-
it "returns rows that match the query
|
366
|
-
included = model_with_pg_search.create!(:
|
367
|
-
|
368
|
-
model_with_pg_search.create!(:
|
363
|
+
it "returns rows that match the query when stemmed by the english dictionary" do
|
364
|
+
included = [model_with_pg_search.create!(:content => "jump"),
|
365
|
+
model_with_pg_search.create!(:content => "jumped"),
|
366
|
+
model_with_pg_search.create!(:content => "jumping")]
|
369
367
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
simple_results = model_with_pg_search.search_title_with_simple("jumped")
|
374
|
-
simple_results.should == [included]
|
375
|
-
excluded.each do |result|
|
376
|
-
simple_results.should_not include(result)
|
377
|
-
end
|
368
|
+
results = model_with_pg_search.search_content_with_english("jump")
|
369
|
+
results.should =~ included
|
378
370
|
end
|
379
371
|
end
|
380
372
|
|
@@ -472,13 +464,20 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
472
464
|
context "using multiple features" do
|
473
465
|
before do
|
474
466
|
model_with_pg_search.class_eval do
|
475
|
-
pg_search_scope :with_tsearch,
|
467
|
+
pg_search_scope :with_tsearch,
|
468
|
+
:against => :title,
|
469
|
+
:using => [
|
470
|
+
[:tsearch, {:prefix => true}]
|
471
|
+
]
|
476
472
|
|
477
473
|
pg_search_scope :with_trigram, :against => :title, :using => :trigram
|
478
474
|
|
479
475
|
pg_search_scope :with_tsearch_and_trigram_using_array,
|
480
476
|
:against => :title,
|
481
|
-
:using => [
|
477
|
+
:using => [
|
478
|
+
[:tsearch, {:prefix => true}],
|
479
|
+
:trigram
|
480
|
+
]
|
482
481
|
|
483
482
|
end
|
484
483
|
end
|
@@ -493,7 +492,7 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
493
492
|
model_with_pg_search.with_tsearch_and_trigram_using_array(trigram_query).should == [record]
|
494
493
|
|
495
494
|
# matches tsearch only
|
496
|
-
tsearch_query = "
|
495
|
+
tsearch_query = "til"
|
497
496
|
model_with_pg_search.with_tsearch(tsearch_query).should include(record)
|
498
497
|
model_with_pg_search.with_trigram(tsearch_query).should_not include(record)
|
499
498
|
model_with_pg_search.with_tsearch_and_trigram_using_array(tsearch_query).should == [record]
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
|
10
|
-
version: 0.1.1
|
8
|
+
- 2
|
9
|
+
version: "0.2"
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- Case Commons, LLC
|
@@ -15,8 +14,7 @@ autorequire:
|
|
15
14
|
bindir: bin
|
16
15
|
cert_chain: []
|
17
16
|
|
18
|
-
date: 2011-
|
19
|
-
default_executable:
|
17
|
+
date: 2011-05-11 00:00:00 Z
|
20
18
|
dependencies: []
|
21
19
|
|
22
20
|
description: PgSearch builds ActiveRecord named scopes that take advantage of PostgreSQL's full text search
|
@@ -40,11 +38,10 @@ files:
|
|
40
38
|
- TODO
|
41
39
|
- gemfiles/Gemfile.common
|
42
40
|
- gemfiles/rails2/Gemfile
|
43
|
-
- gemfiles/rails2/Gemfile.lock
|
44
41
|
- gemfiles/rails3/Gemfile
|
45
|
-
- gemfiles/rails3/Gemfile.lock
|
46
42
|
- lib/pg_search.rb
|
47
43
|
- lib/pg_search/configuration.rb
|
44
|
+
- lib/pg_search/configuration/association.rb
|
48
45
|
- lib/pg_search/configuration/column.rb
|
49
46
|
- lib/pg_search/features.rb
|
50
47
|
- lib/pg_search/features/dmetaphone.rb
|
@@ -63,7 +60,6 @@ files:
|
|
63
60
|
- spec/spec_helper.rb
|
64
61
|
- sql/dmetaphone.sql
|
65
62
|
- sql/uninstall_dmetaphone.sql
|
66
|
-
has_rdoc: true
|
67
63
|
homepage: https://github.com/Casecommons/pg_search
|
68
64
|
licenses: []
|
69
65
|
|
@@ -93,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
89
|
requirements: []
|
94
90
|
|
95
91
|
rubyforge_project:
|
96
|
-
rubygems_version: 1.
|
92
|
+
rubygems_version: 1.8.1
|
97
93
|
signing_key:
|
98
94
|
specification_version: 3
|
99
95
|
summary: PgSearch builds ActiveRecord named scopes that take advantage of PostgreSQL's full text search
|