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 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 to
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
- Improve documentation
10
- Fix bug where :associated_against would not work without an :against present
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. The default dictionary depends on your PostgreSQL setup. 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.
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
@@ -5,5 +5,5 @@ gem "pg"
5
5
  group :test do
6
6
  gem "rspec", ">=2.4"
7
7
  gem "autotest"
8
- gem "with_model", '0.1'
8
+ gem "with_model"
9
9
  end
@@ -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 associated_columns
24
+ def associations
24
25
  return [] unless @options[:associated_against]
25
- @options[:associated_against].map do |association, against|
26
- Array(against).map do |column_name, weight|
27
- Column.new(column_name, weight, @model, association)
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? ? @model.reflect_on_association(association).table_name : @model.table_name
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
- "#{self.subselect_alias}.#{self.alias}"
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
- ["pg_search", table, association, @column_name].compact.join('_')
36
- end
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 => @options[:dictionary].to_s}
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(#{":dictionary," if @options[:dictionary]} #{tsquery_sql})"
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(#{":dictionary," if @options[:dictionary]} #{@normalizer.add_normalization(search_column.to_sql)})"
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.associated_columns.map do |column|
43
- select = "string_agg(#{column.full_name}, ' ') AS #{column.alias}"
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
 
@@ -1,3 +1,3 @@
1
1
  module PgSearch
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2"
3
3
  end
@@ -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
@@ -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 (english)" do
202
- included = [model_with_pg_search.create!(:content => "jump"),
203
- model_with_pg_search.create!(:content => "jumped"),
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("jump")
207
- results.should =~ included
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 simple dictionary" do
352
+ context "with the english dictionary" do
353
353
  before do
354
354
  model_with_pg_search.class_eval do
355
- pg_search_scope :search_title, :against => :title
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 => :simple}
358
+ :tsearch => {:dictionary => :english}
361
359
  }
362
360
  end
363
361
  end
364
362
 
365
- it "returns rows that match the query exactly but not that match the query when stemmed by the default dictionary" do
366
- included = model_with_pg_search.create!(:title => "jumped")
367
- excluded = [model_with_pg_search.create!(:title => "jump"),
368
- model_with_pg_search.create!(:title => "jumping")]
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
- default_results = model_with_pg_search.search_title("jumped")
371
- default_results.should =~ [included] + excluded
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, :against => :title, :using => :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 => [:tsearch, :trigram]
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 = "tile"
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: 25
5
- prerelease: false
4
+ hash: 15
5
+ prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
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-01-20 00:00:00 -05:00
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.3.7
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