pg_search 0.2.2 → 0.3
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/.gitignore +1 -2
- data/.rspec +1 -0
- data/.travis.yml +2 -0
- data/CHANGELOG +8 -5
- data/Gemfile +10 -5
- data/README.rdoc +151 -44
- data/Rakefile +9 -24
- data/lib/pg_search.rb +45 -21
- data/lib/pg_search/configuration.rb +13 -1
- data/lib/pg_search/configuration/association.rb +5 -6
- data/lib/pg_search/configuration/column.rb +1 -2
- data/lib/pg_search/document.rb +21 -0
- data/lib/pg_search/features/tsearch.rb +19 -10
- data/lib/pg_search/multisearch.rb +46 -0
- data/lib/pg_search/multisearchable.rb +28 -0
- data/lib/pg_search/railtie.rb +0 -4
- data/lib/pg_search/scope.rb +1 -1
- data/lib/pg_search/scope_options.rb +13 -17
- data/lib/pg_search/tasks.rb +37 -0
- data/lib/pg_search/version.rb +1 -1
- data/pg_search.gemspec +3 -0
- data/spec/associations_spec.rb +209 -230
- data/spec/pg_search/document_spec.rb +49 -0
- data/spec/pg_search/multisearch_spec.rb +66 -0
- data/spec/pg_search/multisearchable_spec.rb +108 -0
- data/spec/pg_search_spec.rb +148 -42
- data/spec/spec_helper.rb +6 -2
- metadata +83 -31
- checksums.yaml +0 -7
- data/TODO +0 -10
- data/gemfiles/Gemfile.common +0 -9
- data/gemfiles/rails2/Gemfile +0 -4
- data/gemfiles/rails3/Gemfile +0 -4
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe PgSearch::Document do
|
4
|
+
with_table "pg_search_documents", {}, &DOCUMENTS_SCHEMA
|
5
|
+
|
6
|
+
with_model :Searchable do
|
7
|
+
table
|
8
|
+
model do
|
9
|
+
include PgSearch
|
10
|
+
multisearchable({})
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it { should be_an(ActiveRecord::Base) }
|
15
|
+
|
16
|
+
describe "callbacks" do
|
17
|
+
describe "before_validation" do
|
18
|
+
subject { document }
|
19
|
+
let(:document) { PgSearch::Document.new(:searchable => searchable) }
|
20
|
+
let(:searchable) { Searchable.new }
|
21
|
+
|
22
|
+
before do
|
23
|
+
# Redefine the options for multisearchable
|
24
|
+
Searchable.multisearchable(multisearchable_options)
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when searching against a single column" do
|
28
|
+
let(:multisearchable_options) { {:against => :some_content} }
|
29
|
+
let(:text) { "foo bar" }
|
30
|
+
before do
|
31
|
+
searchable.stub!(:some_content => text)
|
32
|
+
document.valid?
|
33
|
+
end
|
34
|
+
|
35
|
+
its(:content) { should == text }
|
36
|
+
end
|
37
|
+
|
38
|
+
context "when searching against multiple columns" do
|
39
|
+
let(:multisearchable_options) { {:against => [:attr1, :attr2]} }
|
40
|
+
before do
|
41
|
+
searchable.stub!(:attr1 => "1", :attr2 => "2")
|
42
|
+
document.valid?
|
43
|
+
end
|
44
|
+
|
45
|
+
its(:content) { should == "1 2" }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe PgSearch::Multisearch do
|
4
|
+
with_table "pg_search_documents", {}, &DOCUMENTS_SCHEMA
|
5
|
+
|
6
|
+
with_model :MultisearchableModel do
|
7
|
+
table do |t|
|
8
|
+
t.string :title
|
9
|
+
t.text :content
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
model do
|
13
|
+
include PgSearch
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".rebuild" do
|
18
|
+
it "should fetch the proper columns from the model" do
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe ".rebuild_sql" do
|
23
|
+
context "with one attribute" do
|
24
|
+
it "should generate the proper SQL code" do
|
25
|
+
model = MultisearchableModel
|
26
|
+
connection = model.connection
|
27
|
+
|
28
|
+
model.multisearchable :against => :title
|
29
|
+
|
30
|
+
expected_sql = <<-SQL
|
31
|
+
INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content)
|
32
|
+
SELECT #{connection.quote(model.name)} AS searchable_type,
|
33
|
+
#{model.quoted_table_name}.id AS searchable_id,
|
34
|
+
(
|
35
|
+
coalesce(#{model.quoted_table_name}.title, '')
|
36
|
+
) AS content
|
37
|
+
FROM #{model.quoted_table_name}
|
38
|
+
SQL
|
39
|
+
|
40
|
+
PgSearch::Multisearch.rebuild_sql(MultisearchableModel).should == expected_sql
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "with multiple attributes" do
|
45
|
+
it "should generate the proper SQL code" do
|
46
|
+
model = MultisearchableModel
|
47
|
+
connection = model.connection
|
48
|
+
|
49
|
+
model.multisearchable :against => [:title, :content]
|
50
|
+
|
51
|
+
expected_sql = <<-SQL
|
52
|
+
INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content)
|
53
|
+
SELECT #{connection.quote(model.name)} AS searchable_type,
|
54
|
+
#{model.quoted_table_name}.id AS searchable_id,
|
55
|
+
(
|
56
|
+
coalesce(#{model.quoted_table_name}.title, '') || ' ' || coalesce(#{model.quoted_table_name}.content, '')
|
57
|
+
) AS content
|
58
|
+
FROM #{model.quoted_table_name}
|
59
|
+
SQL
|
60
|
+
|
61
|
+
PgSearch::Multisearch.rebuild_sql(MultisearchableModel).should == expected_sql
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe PgSearch::Multisearchable do
|
4
|
+
with_table "pg_search_documents", {}, &DOCUMENTS_SCHEMA
|
5
|
+
|
6
|
+
before { PgSearch.stub(:multisearch_enabled?) { true } }
|
7
|
+
|
8
|
+
describe "a model that is multisearchable" do
|
9
|
+
subject { ModelThatIsMultisearchable }
|
10
|
+
|
11
|
+
with_model :ModelThatIsMultisearchable do
|
12
|
+
table do |t|
|
13
|
+
end
|
14
|
+
model do
|
15
|
+
include PgSearch
|
16
|
+
multisearchable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "callbacks" do
|
21
|
+
describe "after_create" do
|
22
|
+
let(:record) { ModelThatIsMultisearchable.new }
|
23
|
+
|
24
|
+
describe "saving the record" do
|
25
|
+
subject do
|
26
|
+
lambda { record.save! }
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with multisearch enabled" do
|
30
|
+
before { PgSearch.stub(:multisearch_enabled?) { true } }
|
31
|
+
it { should change(PgSearch::Document, :count).by(1) }
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with multisearch disabled" do
|
35
|
+
before { PgSearch.stub(:multisearch_enabled?) { false } }
|
36
|
+
it { should_not change(PgSearch::Document, :count) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "the document" do
|
41
|
+
subject { document }
|
42
|
+
before { record.save! }
|
43
|
+
let(:document) { PgSearch::Document.last }
|
44
|
+
|
45
|
+
its(:searchable) { should == record }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "after_update" do
|
50
|
+
let!(:record) { ModelThatIsMultisearchable.create! }
|
51
|
+
|
52
|
+
context "when the document is present" do
|
53
|
+
describe "saving the record" do
|
54
|
+
subject do
|
55
|
+
lambda { record.save! }
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with multisearch enabled" do
|
59
|
+
before { PgSearch.stub(:multisearch_enabled?) { true } }
|
60
|
+
|
61
|
+
before { record.pg_search_document.should_receive(:save) }
|
62
|
+
it { should_not change(PgSearch::Document, :count) }
|
63
|
+
end
|
64
|
+
|
65
|
+
context "with multisearch disabled" do
|
66
|
+
before { PgSearch.stub(:multisearch_enabled?) { false } }
|
67
|
+
|
68
|
+
before { record.pg_search_document.should_not_receive(:save) }
|
69
|
+
it { should_not change(PgSearch::Document, :count) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when the document is missing" do
|
75
|
+
before { record.pg_search_document = nil }
|
76
|
+
|
77
|
+
describe "saving the record" do
|
78
|
+
subject do
|
79
|
+
lambda { record.save! }
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with multisearch enabled" do
|
83
|
+
before { PgSearch.stub(:multisearch_enabled?) { true } }
|
84
|
+
it { should change(PgSearch::Document, :count).by(1) }
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with multisearch disabled" do
|
88
|
+
before { PgSearch.stub(:multisearch_enabled?) { false } }
|
89
|
+
it { should_not change(PgSearch::Document, :count) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "after_destroy" do
|
96
|
+
it "should remove its document" do
|
97
|
+
record = ModelThatIsMultisearchable.create!
|
98
|
+
document = record.pg_search_document
|
99
|
+
|
100
|
+
lambda { record.destroy }.should change(PgSearch::Document, :count).by(-1)
|
101
|
+
lambda {
|
102
|
+
PgSearch::Document.find(document.id)
|
103
|
+
}.should raise_error(ActiveRecord::RecordNotFound)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/spec/pg_search_spec.rb
CHANGED
@@ -369,6 +369,48 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
369
369
|
results.should =~ included
|
370
370
|
end
|
371
371
|
end
|
372
|
+
|
373
|
+
describe "ranking" do
|
374
|
+
before do
|
375
|
+
["Strip Down", "Down", "Down and Out", "Won't Let You Down"].each do |name|
|
376
|
+
ModelWithPgSearch.create! :content => name
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context "with a normalization specified" do
|
381
|
+
before do
|
382
|
+
ModelWithPgSearch.class_eval do
|
383
|
+
pg_search_scope :search_content_with_normalization,
|
384
|
+
:against => :content,
|
385
|
+
:using => {
|
386
|
+
:tsearch => {:normalization => 2}
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
it "ranks the results for documents with less text higher" do
|
391
|
+
results = ModelWithPgSearch.search_content_with_normalization("down")
|
392
|
+
|
393
|
+
results.map(&:content).should == ["Down", "Strip Down", "Down and Out", "Won't Let You Down"]
|
394
|
+
results.first.rank.should be > results.last.rank
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
context "with no normalization" do
|
399
|
+
before do
|
400
|
+
ModelWithPgSearch.class_eval do
|
401
|
+
pg_search_scope :search_content_without_normalization,
|
402
|
+
:against => :content,
|
403
|
+
:using => :tsearch
|
404
|
+
end
|
405
|
+
end
|
406
|
+
it "ranks the results equally" do
|
407
|
+
results = ModelWithPgSearch.search_content_without_normalization("down")
|
408
|
+
|
409
|
+
results.map(&:content).should == ["Strip Down", "Down", "Down and Out", "Won't Let You Down"]
|
410
|
+
results.first.rank.should == results.last.rank
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
372
414
|
|
373
415
|
context "against columns ranked with arrays" do
|
374
416
|
before do
|
@@ -420,6 +462,33 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
420
462
|
results.should == [winner, loser]
|
421
463
|
end
|
422
464
|
end
|
465
|
+
|
466
|
+
context "searching any_word option" do
|
467
|
+
before do
|
468
|
+
ModelWithPgSearch.class_eval do
|
469
|
+
pg_search_scope :search_title_with_any_word,
|
470
|
+
:against => :title,
|
471
|
+
:using => {
|
472
|
+
:tsearch => {:any_word => true}
|
473
|
+
}
|
474
|
+
|
475
|
+
pg_search_scope :search_title_with_all_words,
|
476
|
+
:against => :title
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
it "returns all results containing any word in their title" do
|
481
|
+
numbers = %w(one two three four).map{|number| ModelWithPgSearch.create!(:title => number)}
|
482
|
+
|
483
|
+
results = ModelWithPgSearch.search_title_with_any_word("one two three four")
|
484
|
+
|
485
|
+
results.map(&:title).should == %w(one two three four)
|
486
|
+
|
487
|
+
results = ModelWithPgSearch.search_title_with_all_words("one two three four")
|
488
|
+
|
489
|
+
results.map(&:title).should == []
|
490
|
+
end
|
491
|
+
end
|
423
492
|
end
|
424
493
|
|
425
494
|
context "using dmetaphone" do
|
@@ -523,48 +592,6 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
523
592
|
end
|
524
593
|
end
|
525
594
|
|
526
|
-
context "using a tsvector column" do
|
527
|
-
with_model :ModelWithPgSearchUsingTsVectorColumn do
|
528
|
-
table do |t|
|
529
|
-
t.text 'content'
|
530
|
-
t.column 'content_tsvector', :tsvector
|
531
|
-
end
|
532
|
-
|
533
|
-
model { include PgSearch }
|
534
|
-
end
|
535
|
-
|
536
|
-
let!(:expected) { ModelWithPgSearchUsingTsVectorColumn.create!(:content => 'tiling is grouty') }
|
537
|
-
let!(:unexpected) { ModelWithPgSearchUsingTsVectorColumn.create!(:content => 'longcat is looooooooong') }
|
538
|
-
|
539
|
-
before do
|
540
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
541
|
-
UPDATE #{ModelWithPgSearchUsingTsVectorColumn.table_name}
|
542
|
-
SET content_tsvector = to_tsvector('english'::regconfig, "#{ModelWithPgSearchUsingTsVectorColumn.table_name}"."content")
|
543
|
-
SQL
|
544
|
-
|
545
|
-
ModelWithPgSearchUsingTsVectorColumn.class_eval do
|
546
|
-
pg_search_scope :search_by_content_with_tsvector,
|
547
|
-
:against => :content,
|
548
|
-
:using => {
|
549
|
-
:tsearch => {
|
550
|
-
:tsvector_column => 'content_tsvector',
|
551
|
-
:dictionary => 'english'
|
552
|
-
}
|
553
|
-
}
|
554
|
-
end
|
555
|
-
end
|
556
|
-
|
557
|
-
if defined?(ActiveRecord::Relation)
|
558
|
-
it "should not use to_tsvector in the query" do
|
559
|
-
ModelWithPgSearchUsingTsVectorColumn.search_by_content_with_tsvector("tiles").to_sql.should_not =~ /to_tsvector/
|
560
|
-
end
|
561
|
-
end
|
562
|
-
|
563
|
-
it "should find the expected result" do
|
564
|
-
ModelWithPgSearchUsingTsVectorColumn.search_by_content_with_tsvector("tiles").map(&:id).should == [expected.id]
|
565
|
-
end
|
566
|
-
end
|
567
|
-
|
568
595
|
context "ignoring accents" do
|
569
596
|
before do
|
570
597
|
ModelWithPgSearch.class_eval do
|
@@ -645,4 +672,83 @@ describe "an ActiveRecord model which includes PgSearch" do
|
|
645
672
|
end
|
646
673
|
end
|
647
674
|
end
|
675
|
+
|
676
|
+
describe ".multisearchable" do
|
677
|
+
it "should include the Multisearchable module" do
|
678
|
+
ModelWithPgSearch.should_receive(:include).with(PgSearch::Multisearchable)
|
679
|
+
ModelWithPgSearch.multisearchable
|
680
|
+
end
|
681
|
+
|
682
|
+
it "should set pg_search_multisearchable_options on the class" do
|
683
|
+
options = double(:options)
|
684
|
+
ModelWithPgSearch.multisearchable(options)
|
685
|
+
ModelWithPgSearch.pg_search_multisearchable_options.should == options
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
describe ".multisearch" do
|
690
|
+
subject { PgSearch.multisearch(query) }
|
691
|
+
let(:query) { double(:query) }
|
692
|
+
let(:relation) { double(:relation) }
|
693
|
+
before do
|
694
|
+
PgSearch::Document.should_receive(:search).with(query).and_return(relation)
|
695
|
+
end
|
696
|
+
|
697
|
+
it { should == relation }
|
698
|
+
end
|
699
|
+
|
700
|
+
describe ".disable_multisearch" do
|
701
|
+
it "should temporarily disable multisearch" do
|
702
|
+
@multisearch_enabled_before = PgSearch.multisearch_enabled?
|
703
|
+
PgSearch.disable_multisearch do
|
704
|
+
@multisearch_enabled_inside = PgSearch.multisearch_enabled?
|
705
|
+
end
|
706
|
+
@multisearch_enabled_after = PgSearch.multisearch_enabled?
|
707
|
+
|
708
|
+
@multisearch_enabled_before.should be(true)
|
709
|
+
@multisearch_enabled_inside.should be(false)
|
710
|
+
@multisearch_enabled_after.should be(true)
|
711
|
+
end
|
712
|
+
|
713
|
+
it "should reenable multisearch after an error" do
|
714
|
+
@multisearch_enabled_before = PgSearch.multisearch_enabled?
|
715
|
+
begin
|
716
|
+
PgSearch.disable_multisearch do
|
717
|
+
@multisearch_enabled_inside = PgSearch.multisearch_enabled?
|
718
|
+
raise
|
719
|
+
end
|
720
|
+
rescue
|
721
|
+
end
|
722
|
+
|
723
|
+
@multisearch_enabled_after = PgSearch.multisearch_enabled?
|
724
|
+
|
725
|
+
@multisearch_enabled_before.should be(true)
|
726
|
+
@multisearch_enabled_inside.should be(false)
|
727
|
+
@multisearch_enabled_after.should be(true)
|
728
|
+
end
|
729
|
+
|
730
|
+
it "should not disable multisearch on other threads" do
|
731
|
+
values = Queue.new
|
732
|
+
sync = Queue.new
|
733
|
+
Thread.new do
|
734
|
+
values.push PgSearch.multisearch_enabled?
|
735
|
+
sync.pop # wait
|
736
|
+
values.push PgSearch.multisearch_enabled?
|
737
|
+
sync.pop # wait
|
738
|
+
values.push PgSearch.multisearch_enabled?
|
739
|
+
end
|
740
|
+
|
741
|
+
@multisearch_enabled_before = values.pop
|
742
|
+
PgSearch.disable_multisearch do
|
743
|
+
sync.push :go
|
744
|
+
@multisearch_enabled_inside = values.pop
|
745
|
+
end
|
746
|
+
sync.push :go
|
747
|
+
@multisearch_enabled_after = values.pop
|
748
|
+
|
749
|
+
@multisearch_enabled_before.should be(true)
|
750
|
+
@multisearch_enabled_inside.should be(true)
|
751
|
+
@multisearch_enabled_after.should be(true)
|
752
|
+
end
|
753
|
+
end
|
648
754
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,6 +4,7 @@ require "pg_search"
|
|
4
4
|
begin
|
5
5
|
ActiveRecord::Base.establish_connection(:adapter => 'postgresql',
|
6
6
|
:database => 'pg_search_test',
|
7
|
+
:username => ('postgres' if ENV["TRAVIS"]),
|
7
8
|
:min_messages => 'warning')
|
8
9
|
connection = ActiveRecord::Base.connection
|
9
10
|
connection.execute("SELECT 1")
|
@@ -45,8 +46,11 @@ RSpec.configure do |config|
|
|
45
46
|
config.extend WithModel
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
-
|
49
|
+
RSpec::Matchers::OperatorMatcher.register(ActiveRecord::Relation, '=~', RSpec::Matchers::MatchArray)
|
50
|
+
|
51
|
+
DOCUMENTS_SCHEMA = lambda do |t|
|
52
|
+
t.belongs_to :searchable, :polymorphic => true
|
53
|
+
t.text :content
|
50
54
|
end
|
51
55
|
|
52
56
|
require 'irb'
|