pg_search 0.0.2
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/.autotest +5 -0
- data/.gitignore +7 -0
- data/.rvmrc +1 -0
- data/CHANGELOG +7 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.rdoc +290 -0
- data/Rakefile +35 -0
- data/TODO +12 -0
- data/gemfiles/Gemfile.common +9 -0
- data/gemfiles/rails2/Gemfile +4 -0
- data/gemfiles/rails3/Gemfile +4 -0
- data/lib/pg_search.rb +32 -0
- data/lib/pg_search/configuration.rb +73 -0
- data/lib/pg_search/configuration/column.rb +43 -0
- data/lib/pg_search/features.rb +7 -0
- data/lib/pg_search/features/dmetaphone.rb +28 -0
- data/lib/pg_search/features/trigram.rb +29 -0
- data/lib/pg_search/features/tsearch.rb +64 -0
- data/lib/pg_search/normalizer.rb +13 -0
- data/lib/pg_search/railtie.rb +11 -0
- data/lib/pg_search/scope.rb +31 -0
- data/lib/pg_search/scope_options.rb +75 -0
- data/lib/pg_search/tasks.rb +37 -0
- data/lib/pg_search/version.rb +3 -0
- data/pg_search.gemspec +19 -0
- data/script/setup-contrib +12 -0
- data/spec/associations_spec.rb +225 -0
- data/spec/pg_search_spec.rb +596 -0
- data/spec/spec_helper.rb +92 -0
- data/sql/dmetaphone.sql +4 -0
- data/sql/uninstall_dmetaphone.sql +1 -0
- metadata +103 -0
| @@ -0,0 +1,12 @@ | |
| 1 | 
            +
            #!/bin/sh
         | 
| 2 | 
            +
            POSTGRESQL_VERSION=`pg_config --version | awk '{print $2}'`
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            cd /tmp
         | 
| 5 | 
            +
            test -e /tmp/postgresql-$POSTGRESQL_VERSION.tar.bz2 || wget http://ftp9.us.postgresql.org/pub/mirrors/postgresql/source/v$POSTGRESQL_VERSION/postgresql-$POSTGRESQL_VERSION.tar.bz2
         | 
| 6 | 
            +
            test -d /tmp/postgresql-$POSTGRESQL_VERSION || tar zxvf postgresql-$POSTGRESQL_VERSION.tar.bz2
         | 
| 7 | 
            +
            cd postgresql-$POSTGRESQL_VERSION && eval ./configure `pg_config --configure` && make
         | 
| 8 | 
            +
            cd contrib/unaccent && make && make install
         | 
| 9 | 
            +
            cd ..
         | 
| 10 | 
            +
            cd contrib/pg_trgm && make && make install
         | 
| 11 | 
            +
            cd ..
         | 
| 12 | 
            +
            cd contrib/fuzzystrmatch && make && make install
         | 
| @@ -0,0 +1,225 @@ | |
| 1 | 
            +
            require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe PgSearch do
         | 
| 4 | 
            +
              context "joining to another table" do
         | 
| 5 | 
            +
                if defined?(ActiveRecord::Relation)
         | 
| 6 | 
            +
                  context "with Arel support" do
         | 
| 7 | 
            +
                    context "through a belongs_to association" do
         | 
| 8 | 
            +
                      with_model :associated_model do
         | 
| 9 | 
            +
                        table do |t|
         | 
| 10 | 
            +
                          t.string 'title'
         | 
| 11 | 
            +
                        end
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      with_model :model_with_belongs_to do
         | 
| 15 | 
            +
                        table do |t|
         | 
| 16 | 
            +
                          t.string 'title'
         | 
| 17 | 
            +
                          t.belongs_to 'another_model'
         | 
| 18 | 
            +
                        end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                        model do
         | 
| 21 | 
            +
                          include PgSearch
         | 
| 22 | 
            +
                          belongs_to :another_model, :class_name => 'AssociatedModel'
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                          pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
                      end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      it "returns rows that match the query in either its own columns or the columns of the associated model" do
         | 
| 29 | 
            +
                        associated = associated_model.create!(:title => 'abcdef')
         | 
| 30 | 
            +
                        included = [
         | 
| 31 | 
            +
                          model_with_belongs_to.create!(:title => 'ghijkl', :another_model => associated),
         | 
| 32 | 
            +
                          model_with_belongs_to.create!(:title => 'abcdef')
         | 
| 33 | 
            +
                        ]
         | 
| 34 | 
            +
                        excluded = model_with_belongs_to.create!(:title => 'mnopqr',
         | 
| 35 | 
            +
                                                                 :another_model => associated_model.create!(:title => 'stuvwx'))
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                        results = model_with_belongs_to.with_associated('abcdef')
         | 
| 38 | 
            +
                        results.map(&:title).should =~ included.map(&:title)
         | 
| 39 | 
            +
                        results.should_not include(excluded)
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    context "through a has_many association" do
         | 
| 44 | 
            +
                      with_model :associated_model_with_has_many do
         | 
| 45 | 
            +
                        table do |t|
         | 
| 46 | 
            +
                          t.string 'title'
         | 
| 47 | 
            +
                          t.belongs_to 'model_with_has_many'
         | 
| 48 | 
            +
                        end
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      with_model :model_with_has_many do
         | 
| 52 | 
            +
                        table do |t|
         | 
| 53 | 
            +
                          t.string 'title'
         | 
| 54 | 
            +
                        end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                        model do
         | 
| 57 | 
            +
                          include PgSearch
         | 
| 58 | 
            +
                          has_many :other_models, :class_name => 'AssociatedModelWithHasMany', :foreign_key => 'model_with_has_many_id'
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                          pg_search_scope :with_associated, :against => [:title], :associated_against => {:other_models => :title}
         | 
| 61 | 
            +
                        end
         | 
| 62 | 
            +
                      end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                      it "returns rows that match the query in either its own columns or the columns of the associated model" do
         | 
| 65 | 
            +
                        included = [
         | 
| 66 | 
            +
                          model_with_has_many.create!(:title => 'abcdef', :other_models => [
         | 
| 67 | 
            +
                                                      associated_model_with_has_many.create!(:title => 'foo'),
         | 
| 68 | 
            +
                                                      associated_model_with_has_many.create!(:title => 'bar')
         | 
| 69 | 
            +
                        ]),
         | 
| 70 | 
            +
                          model_with_has_many.create!(:title => 'ghijkl', :other_models => [
         | 
| 71 | 
            +
                                                      associated_model_with_has_many.create!(:title => 'foo bar'),
         | 
| 72 | 
            +
                                                      associated_model_with_has_many.create!(:title => 'mnopqr')
         | 
| 73 | 
            +
                        ]),
         | 
| 74 | 
            +
                          model_with_has_many.create!(:title => 'foo bar')
         | 
| 75 | 
            +
                        ]
         | 
| 76 | 
            +
                        excluded = model_with_has_many.create!(:title => 'stuvwx', :other_models => [
         | 
| 77 | 
            +
                                                               associated_model_with_has_many.create!(:title => 'abcdef')
         | 
| 78 | 
            +
                        ])
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                        results = model_with_has_many.with_associated('foo bar')
         | 
| 81 | 
            +
                        results.map(&:title).should =~ included.map(&:title)
         | 
| 82 | 
            +
                        results.should_not include(excluded)
         | 
| 83 | 
            +
                      end
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                    context "across multiple associations" do
         | 
| 87 | 
            +
                      context "on different tables" do
         | 
| 88 | 
            +
                        with_model :first_associated_model do
         | 
| 89 | 
            +
                          table do |t|
         | 
| 90 | 
            +
                            t.string 'title'
         | 
| 91 | 
            +
                            t.belongs_to 'model_with_many_associations'
         | 
| 92 | 
            +
                          end
         | 
| 93 | 
            +
                          model {}
         | 
| 94 | 
            +
                        end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                        with_model :second_associated_model do
         | 
| 97 | 
            +
                          table do |t|
         | 
| 98 | 
            +
                            t.string 'title'
         | 
| 99 | 
            +
                          end
         | 
| 100 | 
            +
                          model {}
         | 
| 101 | 
            +
                        end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                        with_model :model_with_many_associations do
         | 
| 104 | 
            +
                          table do |t|
         | 
| 105 | 
            +
                            t.string 'title'
         | 
| 106 | 
            +
                            t.belongs_to 'model_of_second_type'
         | 
| 107 | 
            +
                          end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                          model do
         | 
| 110 | 
            +
                            include PgSearch
         | 
| 111 | 
            +
                            has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'model_with_many_associations_id'
         | 
| 112 | 
            +
                            belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                            pg_search_scope :with_associated, :against => :title,
         | 
| 115 | 
            +
                              :associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
         | 
| 116 | 
            +
                          end
         | 
| 117 | 
            +
                        end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                        it "returns rows that match the query in either its own columns or the columns of the associated model" do
         | 
| 120 | 
            +
                          matching_second = second_associated_model.create!(:title => "foo bar")
         | 
| 121 | 
            +
                          unmatching_second = second_associated_model.create!(:title => "uiop")
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                          included = [
         | 
| 124 | 
            +
                            ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
         | 
| 125 | 
            +
                                                              first_associated_model.create!(:title => 'foo'),
         | 
| 126 | 
            +
                                                              first_associated_model.create!(:title => 'bar')
         | 
| 127 | 
            +
                          ]),
         | 
| 128 | 
            +
                            ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
         | 
| 129 | 
            +
                                                              first_associated_model.create!(:title => 'foo bar'),
         | 
| 130 | 
            +
                                                              first_associated_model.create!(:title => 'mnopqr')
         | 
| 131 | 
            +
                          ]),
         | 
| 132 | 
            +
                            ModelWithManyAssociations.create!(:title => 'foo bar'),
         | 
| 133 | 
            +
                            ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
         | 
| 134 | 
            +
                          ]
         | 
| 135 | 
            +
                          excluded = [
         | 
| 136 | 
            +
                            ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
         | 
| 137 | 
            +
                                                              first_associated_model.create!(:title => 'abcdef')
         | 
| 138 | 
            +
                          ]),
         | 
| 139 | 
            +
                            ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
         | 
| 140 | 
            +
                          ]
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                          results = ModelWithManyAssociations.with_associated('foo bar')
         | 
| 143 | 
            +
                          results.map(&:title).should =~ included.map(&:title)
         | 
| 144 | 
            +
                          excluded.each { |object| results.should_not include(object) }
         | 
| 145 | 
            +
                        end
         | 
| 146 | 
            +
                      end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                      context "on the same table" do
         | 
| 149 | 
            +
                        with_model :doubly_associated_model do
         | 
| 150 | 
            +
                          table do |t|
         | 
| 151 | 
            +
                            t.string 'title'
         | 
| 152 | 
            +
                            t.belongs_to 'model_with_double_association'
         | 
| 153 | 
            +
                            t.belongs_to 'model_with_double_association_again'
         | 
| 154 | 
            +
                          end
         | 
| 155 | 
            +
                          model {}
         | 
| 156 | 
            +
                        end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                        with_model :model_with_double_association do
         | 
| 159 | 
            +
                          table do |t|
         | 
| 160 | 
            +
                            t.string 'title'
         | 
| 161 | 
            +
                          end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                          model do
         | 
| 164 | 
            +
                            include PgSearch
         | 
| 165 | 
            +
                            has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_id'
         | 
| 166 | 
            +
                            has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_again_id'
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                            pg_search_scope :with_associated, :against => :title,
         | 
| 169 | 
            +
                              :associated_against => {:things => :title, :thingamabobs => :title}
         | 
| 170 | 
            +
                          end
         | 
| 171 | 
            +
                        end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                        it "returns rows that match the query in either its own columns or the columns of the associated model" do
         | 
| 174 | 
            +
                          included = [
         | 
| 175 | 
            +
                            ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
         | 
| 176 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => 'foo'),
         | 
| 177 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => 'bar')
         | 
| 178 | 
            +
                          ]),
         | 
| 179 | 
            +
                            ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
         | 
| 180 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => 'foo bar'),
         | 
| 181 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => 'mnopqr')
         | 
| 182 | 
            +
                          ]),
         | 
| 183 | 
            +
                            ModelWithDoubleAssociation.create!(:title => 'foo bar'),
         | 
| 184 | 
            +
                            ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
         | 
| 185 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => "foo bar")
         | 
| 186 | 
            +
                          ])
         | 
| 187 | 
            +
                          ]
         | 
| 188 | 
            +
                          excluded = [
         | 
| 189 | 
            +
                            ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
         | 
| 190 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => 'abcdef')
         | 
| 191 | 
            +
                          ]),
         | 
| 192 | 
            +
                            ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
         | 
| 193 | 
            +
                                                                  DoublyAssociatedModel.create!(:title => "uiop")
         | 
| 194 | 
            +
                          ])
         | 
| 195 | 
            +
                          ]
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                          results = ModelWithDoubleAssociation.with_associated('foo bar')
         | 
| 198 | 
            +
                          results.map(&:title).should =~ included.map(&:title)
         | 
| 199 | 
            +
                          excluded.each { |object| results.should_not include(object) }
         | 
| 200 | 
            +
                        end
         | 
| 201 | 
            +
                      end
         | 
| 202 | 
            +
                    end
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
                else
         | 
| 205 | 
            +
                  context "without Arel support" do
         | 
| 206 | 
            +
                    with_model :model do
         | 
| 207 | 
            +
                      table do |t|
         | 
| 208 | 
            +
                        t.string 'title'
         | 
| 209 | 
            +
                      end
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                      model do
         | 
| 212 | 
            +
                        include PgSearch
         | 
| 213 | 
            +
                        pg_search_scope :with_joins, :against => :title, :joins => :another_model
         | 
| 214 | 
            +
                      end
         | 
| 215 | 
            +
                    end
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                    it "should raise an error" do
         | 
| 218 | 
            +
                      lambda {
         | 
| 219 | 
            +
                        Model.with_joins('foo')
         | 
| 220 | 
            +
                      }.should raise_error(ArgumentError, /joins/)
         | 
| 221 | 
            +
                    end
         | 
| 222 | 
            +
                  end
         | 
| 223 | 
            +
                end
         | 
| 224 | 
            +
              end
         | 
| 225 | 
            +
            end
         | 
| @@ -0,0 +1,596 @@ | |
| 1 | 
            +
            require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe "an ActiveRecord model which includes PgSearch" do
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              with_model :model_with_pg_search do
         | 
| 6 | 
            +
                table do |t|
         | 
| 7 | 
            +
                  t.string 'title'
         | 
| 8 | 
            +
                  t.text 'content'
         | 
| 9 | 
            +
                  t.integer 'importance'
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                model do
         | 
| 13 | 
            +
                  include PgSearch
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              describe ".pg_search_scope" do
         | 
| 18 | 
            +
                it "builds a scope" do
         | 
| 19 | 
            +
                  model_with_pg_search.class_eval do
         | 
| 20 | 
            +
                    pg_search_scope "matching_query", :against => []
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  lambda {
         | 
| 24 | 
            +
                    model_with_pg_search.scoped({}).matching_query("foo").scoped({})
         | 
| 25 | 
            +
                  }.should_not raise_error
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                context "when passed a lambda" do
         | 
| 29 | 
            +
                  it "builds a dynamic scope" do
         | 
| 30 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 31 | 
            +
                      pg_search_scope :search_title_or_content, lambda { |query, pick_content|
         | 
| 32 | 
            +
                        {
         | 
| 33 | 
            +
                          :query => query.gsub("-remove-", ""),
         | 
| 34 | 
            +
                          :against => pick_content ? :content : :title
         | 
| 35 | 
            +
                        }
         | 
| 36 | 
            +
                      }
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    included = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
         | 
| 40 | 
            +
                    excluded = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    model_with_pg_search.search_title_or_content('fo-remove-o', false).should == [included]
         | 
| 43 | 
            +
                    model_with_pg_search.search_title_or_content('b-remove-ar', true).should == [included]
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                context "when an unknown option is passed in" do
         | 
| 48 | 
            +
                  it "raises an exception when invoked" do
         | 
| 49 | 
            +
                    lambda {
         | 
| 50 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 51 | 
            +
                        pg_search_scope :with_unknown_option, :against => :content, :foo => :bar
         | 
| 52 | 
            +
                      end
         | 
| 53 | 
            +
                      model_with_pg_search.with_unknown_option("foo")
         | 
| 54 | 
            +
                    }.should raise_error(ArgumentError, /foo/)
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  context "dynamically" do
         | 
| 58 | 
            +
                    it "raises an exception when invoked" do
         | 
| 59 | 
            +
                      lambda {
         | 
| 60 | 
            +
                        model_with_pg_search.class_eval do
         | 
| 61 | 
            +
                          pg_search_scope :with_unknown_option, lambda { |*| {:against => :content, :foo => :bar} }
         | 
| 62 | 
            +
                        end
         | 
| 63 | 
            +
                        model_with_pg_search.with_unknown_option("foo")
         | 
| 64 | 
            +
                      }.should raise_error(ArgumentError, /foo/)
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                context "when an unknown :using is passed" do
         | 
| 70 | 
            +
                  it "raises an exception when invoked" do
         | 
| 71 | 
            +
                    lambda {
         | 
| 72 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 73 | 
            +
                        pg_search_scope :with_unknown_using, :against => :content, :using => :foo
         | 
| 74 | 
            +
                      end
         | 
| 75 | 
            +
                      model_with_pg_search.with_unknown_using("foo")
         | 
| 76 | 
            +
                    }.should raise_error(ArgumentError, /foo/)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  context "dynamically" do
         | 
| 80 | 
            +
                    it "raises an exception when invoked" do
         | 
| 81 | 
            +
                      lambda {
         | 
| 82 | 
            +
                        model_with_pg_search.class_eval do
         | 
| 83 | 
            +
                          pg_search_scope :with_unknown_using, lambda { |*| {:against => :content, :using => :foo} }
         | 
| 84 | 
            +
                        end
         | 
| 85 | 
            +
                        model_with_pg_search.with_unknown_using("foo")
         | 
| 86 | 
            +
                      }.should raise_error(ArgumentError, /foo/)
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                context "when an unknown :normalizing is passed" do
         | 
| 92 | 
            +
                  it "raises an exception when invoked" do
         | 
| 93 | 
            +
                    lambda {
         | 
| 94 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 95 | 
            +
                        pg_search_scope :with_unknown_normalizing, :against => :content, :normalizing => :foo
         | 
| 96 | 
            +
                      end
         | 
| 97 | 
            +
                      model_with_pg_search.with_unknown_normalizing("foo")
         | 
| 98 | 
            +
                    }.should raise_error(ArgumentError, /normalizing.*foo/)
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  context "dynamically" do
         | 
| 102 | 
            +
                    it "raises an exception when invoked" do
         | 
| 103 | 
            +
                      lambda {
         | 
| 104 | 
            +
                        model_with_pg_search.class_eval do
         | 
| 105 | 
            +
                          pg_search_scope :with_unknown_normalizing, lambda { |*| {:against => :content, :normalizing => :foo} }
         | 
| 106 | 
            +
                        end
         | 
| 107 | 
            +
                        model_with_pg_search.with_unknown_normalizing("foo")
         | 
| 108 | 
            +
                      }.should raise_error(ArgumentError, /normalizing.*foo/)
         | 
| 109 | 
            +
                    end
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  context "when :against is not passed in" do
         | 
| 113 | 
            +
                    it "raises an exception when invoked" do
         | 
| 114 | 
            +
                      lambda {
         | 
| 115 | 
            +
                        model_with_pg_search.class_eval do
         | 
| 116 | 
            +
                          pg_search_scope :with_unknown_normalizing, {}
         | 
| 117 | 
            +
                        end
         | 
| 118 | 
            +
                        model_with_pg_search.with_unknown_normalizing("foo")
         | 
| 119 | 
            +
                      }.should raise_error(ArgumentError, /against/)
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                    context "dynamically" do
         | 
| 122 | 
            +
                      it "raises an exception when invoked" do
         | 
| 123 | 
            +
                        lambda {
         | 
| 124 | 
            +
                          model_with_pg_search.class_eval do
         | 
| 125 | 
            +
                            pg_search_scope :with_unknown_normalizing, lambda { |*| {} }
         | 
| 126 | 
            +
                          end
         | 
| 127 | 
            +
                          model_with_pg_search.with_unknown_normalizing("foo")
         | 
| 128 | 
            +
                        }.should raise_error(ArgumentError, /against/)
         | 
| 129 | 
            +
                      end
         | 
| 130 | 
            +
                    end
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
              end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
              describe "a search scope" do
         | 
| 136 | 
            +
                context "against a single column" do
         | 
| 137 | 
            +
                  before do
         | 
| 138 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 139 | 
            +
                      pg_search_scope :search_content, :against => :content
         | 
| 140 | 
            +
                    end
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  it "returns an empty array when a blank query is passed in" do
         | 
| 144 | 
            +
                    model_with_pg_search.create!(:content => 'foo')
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                    results = model_with_pg_search.search_content('')
         | 
| 147 | 
            +
                    results.should == []
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  it "returns rows where the column contains the term in the query" do
         | 
| 151 | 
            +
                    included = model_with_pg_search.create!(:content => 'foo')
         | 
| 152 | 
            +
                    excluded = model_with_pg_search.create!(:content => 'bar')
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                    results = model_with_pg_search.search_content('foo')
         | 
| 155 | 
            +
                    results.should include(included)
         | 
| 156 | 
            +
                    results.should_not include(excluded)
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  it "returns rows where the column contains all the terms in the query in any order" do
         | 
| 160 | 
            +
                    included = [model_with_pg_search.create!(:content => 'foo bar'),
         | 
| 161 | 
            +
                                model_with_pg_search.create!(:content => 'bar foo')]
         | 
| 162 | 
            +
                    excluded = model_with_pg_search.create!(:content => 'foo')
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                    results = model_with_pg_search.search_content('foo bar')
         | 
| 165 | 
            +
                    results.should =~ included
         | 
| 166 | 
            +
                    results.should_not include(excluded)
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  it "returns rows that match the query but not its case" do
         | 
| 170 | 
            +
                    # \303\241 is a with acute accent
         | 
| 171 | 
            +
                    # \303\251 is e with acute accent
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    included = [model_with_pg_search.create!(:content => "foo"),
         | 
| 174 | 
            +
                                model_with_pg_search.create!(:content => "FOO")]
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                    results = model_with_pg_search.search_content("Foo")
         | 
| 177 | 
            +
                    results.should =~ included
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  it "returns rows that match the query only if their diacritics match" do
         | 
| 181 | 
            +
                    # \303\241 is a with acute accent
         | 
| 182 | 
            +
                    # \303\251 is e with acute accent
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                    included = model_with_pg_search.create!(:content => "abcd\303\251f")
         | 
| 185 | 
            +
                    excluded = model_with_pg_search.create!(:content => "\303\241bcdef")
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                    results = model_with_pg_search.search_content("abcd\303\251f")
         | 
| 188 | 
            +
                    results.should == [included]
         | 
| 189 | 
            +
                    results.should_not include(excluded)
         | 
| 190 | 
            +
                  end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                  it "returns rows that match the query but not rows that are prefixed by the query" do
         | 
| 193 | 
            +
                    included = model_with_pg_search.create!(:content => 'pre')
         | 
| 194 | 
            +
                    excluded = model_with_pg_search.create!(:content => 'prefix')
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                    results = model_with_pg_search.search_content("pre")
         | 
| 197 | 
            +
                    results.should == [included]
         | 
| 198 | 
            +
                    results.should_not include(excluded)
         | 
| 199 | 
            +
                  end
         | 
| 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"),
         | 
| 204 | 
            +
                                model_with_pg_search.create!(:content => "jumping")]
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                    results = model_with_pg_search.search_content("jump")
         | 
| 207 | 
            +
                    results.should =~ included
         | 
| 208 | 
            +
                  end
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                  it "returns rows that match sorted by rank" do
         | 
| 211 | 
            +
                    loser = model_with_pg_search.create!(:content => 'foo')
         | 
| 212 | 
            +
                    winner = model_with_pg_search.create!(:content => 'foo foo')
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                    results = model_with_pg_search.search_content("foo")
         | 
| 215 | 
            +
                    results[0].rank.should > results[1].rank
         | 
| 216 | 
            +
                    results.should == [winner, loser]
         | 
| 217 | 
            +
                  end
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                  it "returns results that match sorted by primary key for records that rank the same" do
         | 
| 220 | 
            +
                    sorted_results = [model_with_pg_search.create!(:content => 'foo'),
         | 
| 221 | 
            +
                                      model_with_pg_search.create!(:content => 'foo')].sort_by(&:id)
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                    results = model_with_pg_search.search_content("foo")
         | 
| 224 | 
            +
                    results.should == sorted_results
         | 
| 225 | 
            +
                  end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                  it "returns results that match a query with multiple space-separated search terms" do
         | 
| 228 | 
            +
                    included = [
         | 
| 229 | 
            +
                      model_with_pg_search.create!(:content => 'foo bar'),
         | 
| 230 | 
            +
                      model_with_pg_search.create!(:content => 'bar foo'),
         | 
| 231 | 
            +
                      model_with_pg_search.create!(:content => 'bar foo baz'),
         | 
| 232 | 
            +
                    ]
         | 
| 233 | 
            +
                    excluded = [
         | 
| 234 | 
            +
                      model_with_pg_search.create!(:content => 'foo'),
         | 
| 235 | 
            +
                      model_with_pg_search.create!(:content => 'foo baz')
         | 
| 236 | 
            +
                    ]
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                    results = model_with_pg_search.search_content('foo bar')
         | 
| 239 | 
            +
                    results.should =~ included
         | 
| 240 | 
            +
                    results.should_not include(excluded)
         | 
| 241 | 
            +
                  end
         | 
| 242 | 
            +
             | 
| 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?, \\")
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                    results = model_with_pg_search.search_content("foo bar .,?() \\")
         | 
| 247 | 
            +
                    results.should == [included]
         | 
| 248 | 
            +
                  end
         | 
| 249 | 
            +
                  it "accepts non-string queries and calls #to_s on them" do
         | 
| 250 | 
            +
                    foo = model_with_pg_search.create!(:content => "foo")
         | 
| 251 | 
            +
                    not_a_string = stub(:to_s => "foo")
         | 
| 252 | 
            +
                    model_with_pg_search.search_content(not_a_string).should == [foo]
         | 
| 253 | 
            +
                  end
         | 
| 254 | 
            +
                end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                context "against multiple columns" do
         | 
| 257 | 
            +
                  before do
         | 
| 258 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 259 | 
            +
                      pg_search_scope :search_title_and_content, :against => [:title, :content]
         | 
| 260 | 
            +
                    end
         | 
| 261 | 
            +
                  end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                  it "returns rows whose columns contain all of the terms in the query across columns" do
         | 
| 264 | 
            +
                    included = [
         | 
| 265 | 
            +
                      model_with_pg_search.create!(:title => 'foo', :content => 'bar'),
         | 
| 266 | 
            +
                      model_with_pg_search.create!(:title => 'bar', :content => 'foo')
         | 
| 267 | 
            +
                    ]
         | 
| 268 | 
            +
                    excluded = [
         | 
| 269 | 
            +
                      model_with_pg_search.create!(:title => 'foo', :content => 'foo'),
         | 
| 270 | 
            +
                      model_with_pg_search.create!(:title => 'bar', :content => 'bar')
         | 
| 271 | 
            +
                    ]
         | 
| 272 | 
            +
             | 
| 273 | 
            +
                    results = model_with_pg_search.search_title_and_content('foo bar')
         | 
| 274 | 
            +
             | 
| 275 | 
            +
                    results.should =~ included
         | 
| 276 | 
            +
                    excluded.each do |result|
         | 
| 277 | 
            +
                      results.should_not include(result)
         | 
| 278 | 
            +
                    end
         | 
| 279 | 
            +
                  end
         | 
| 280 | 
            +
             | 
| 281 | 
            +
                  it "returns rows where at one column contains all of the terms in the query and another does not" do
         | 
| 282 | 
            +
                    in_title = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
         | 
| 283 | 
            +
                    in_content = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
         | 
| 284 | 
            +
             | 
| 285 | 
            +
                    results  = model_with_pg_search.search_title_and_content('foo')
         | 
| 286 | 
            +
                    results.should =~ [in_title, in_content]
         | 
| 287 | 
            +
                  end
         | 
| 288 | 
            +
             | 
| 289 | 
            +
                  # Searching with a NULL column will prevent any matches unless we coalesce it.
         | 
| 290 | 
            +
                  it "returns rows where at one column contains all of the terms in the query and another is NULL" do
         | 
| 291 | 
            +
                    included = model_with_pg_search.create!(:title => 'foo', :content => nil)
         | 
| 292 | 
            +
                    results  = model_with_pg_search.search_title_and_content('foo')
         | 
| 293 | 
            +
                    results.should == [included]
         | 
| 294 | 
            +
                  end
         | 
| 295 | 
            +
                end
         | 
| 296 | 
            +
             | 
| 297 | 
            +
                context "using trigram" do
         | 
| 298 | 
            +
                  before do
         | 
| 299 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 300 | 
            +
                      pg_search_scope :with_trigrams, :against => [:title, :content], :using => :trigram
         | 
| 301 | 
            +
                    end
         | 
| 302 | 
            +
                  end
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                  it "returns rows where one searchable column and the query share enough trigrams" do
         | 
| 305 | 
            +
                    included = model_with_pg_search.create!(:title => 'abcdefghijkl', :content => nil)
         | 
| 306 | 
            +
                    results = model_with_pg_search.with_trigrams('cdefhijkl')
         | 
| 307 | 
            +
                    results.should == [included]
         | 
| 308 | 
            +
                  end
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                  it "returns rows where multiple searchable columns and the query share enough trigrams" do
         | 
| 311 | 
            +
                    included = model_with_pg_search.create!(:title => 'abcdef', :content => 'ghijkl')
         | 
| 312 | 
            +
                    results = model_with_pg_search.with_trigrams('cdefhijkl')
         | 
| 313 | 
            +
                    results.should == [included]
         | 
| 314 | 
            +
                  end
         | 
| 315 | 
            +
                end
         | 
| 316 | 
            +
             | 
| 317 | 
            +
                context "using tsearch" do
         | 
| 318 | 
            +
                  context "with :prefix => true" do
         | 
| 319 | 
            +
                    before do
         | 
| 320 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 321 | 
            +
                        pg_search_scope :search_title_with_prefixes,
         | 
| 322 | 
            +
                                        :against => :title,
         | 
| 323 | 
            +
                                        :using => {
         | 
| 324 | 
            +
                                          :tsearch => {:prefix => true}
         | 
| 325 | 
            +
                                        }
         | 
| 326 | 
            +
                      end
         | 
| 327 | 
            +
                    end
         | 
| 328 | 
            +
             | 
| 329 | 
            +
                    it "returns rows that match the query and that are prefixed by the query" do
         | 
| 330 | 
            +
                      included = model_with_pg_search.create!(:title => 'prefix')
         | 
| 331 | 
            +
                      excluded = model_with_pg_search.create!(:title => 'postfix')
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                      results = model_with_pg_search.search_title_with_prefixes("pre")
         | 
| 334 | 
            +
                      results.should == [included]
         | 
| 335 | 
            +
                      results.should_not include(excluded)
         | 
| 336 | 
            +
                    end
         | 
| 337 | 
            +
             | 
| 338 | 
            +
                    it "returns rows that match the query when the query has a hyphen" do
         | 
| 339 | 
            +
                      included = [
         | 
| 340 | 
            +
                        model_with_pg_search.create!(:title => 'foo bar'),
         | 
| 341 | 
            +
                        model_with_pg_search.create!(:title => 'foo-bar')
         | 
| 342 | 
            +
                      ]
         | 
| 343 | 
            +
                      excluded = model_with_pg_search.create!(:title => 'baz quux')
         | 
| 344 | 
            +
             | 
| 345 | 
            +
                      results = model_with_pg_search.search_title_with_prefixes("foo-bar")
         | 
| 346 | 
            +
                      results.should =~ included
         | 
| 347 | 
            +
                      results.should_not include(excluded)
         | 
| 348 | 
            +
                    end
         | 
| 349 | 
            +
                  end
         | 
| 350 | 
            +
             | 
| 351 | 
            +
                  context "with the simple dictionary" do
         | 
| 352 | 
            +
                    before do
         | 
| 353 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 354 | 
            +
                        pg_search_scope :search_title, :against => :title
         | 
| 355 | 
            +
             | 
| 356 | 
            +
                        pg_search_scope :search_title_with_simple,
         | 
| 357 | 
            +
                                        :against => :title,
         | 
| 358 | 
            +
                                        :using => {
         | 
| 359 | 
            +
                                          :tsearch => {:dictionary => :simple}
         | 
| 360 | 
            +
                                        }
         | 
| 361 | 
            +
                      end
         | 
| 362 | 
            +
                    end
         | 
| 363 | 
            +
             | 
| 364 | 
            +
                    it "returns rows that match the query exactly but not that match the query when stemmed by the default dictionary" do
         | 
| 365 | 
            +
                      included = model_with_pg_search.create!(:title => "jumped")
         | 
| 366 | 
            +
                      excluded = [model_with_pg_search.create!(:title => "jump"),
         | 
| 367 | 
            +
                                  model_with_pg_search.create!(:title => "jumping")]
         | 
| 368 | 
            +
             | 
| 369 | 
            +
                      default_results = model_with_pg_search.search_title("jumped")
         | 
| 370 | 
            +
                      default_results.should =~ [included] + excluded
         | 
| 371 | 
            +
             | 
| 372 | 
            +
                      simple_results = model_with_pg_search.search_title_with_simple("jumped")
         | 
| 373 | 
            +
                      simple_results.should == [included]
         | 
| 374 | 
            +
                      excluded.each do |result|
         | 
| 375 | 
            +
                        simple_results.should_not include(result)
         | 
| 376 | 
            +
                      end
         | 
| 377 | 
            +
                    end
         | 
| 378 | 
            +
                  end
         | 
| 379 | 
            +
             | 
| 380 | 
            +
                  context "against columns ranked with arrays" do
         | 
| 381 | 
            +
                    before do
         | 
| 382 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 383 | 
            +
                         pg_search_scope :search_weighted_by_array_of_arrays, :against => [[:content, 'B'], [:title, 'A']]
         | 
| 384 | 
            +
                       end
         | 
| 385 | 
            +
                    end
         | 
| 386 | 
            +
             | 
| 387 | 
            +
                    it "returns results sorted by weighted rank" do
         | 
| 388 | 
            +
                      loser = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
         | 
| 389 | 
            +
                      winner = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
         | 
| 390 | 
            +
             | 
| 391 | 
            +
                      results = model_with_pg_search.search_weighted_by_array_of_arrays('foo')
         | 
| 392 | 
            +
                      results[0].rank.should > results[1].rank
         | 
| 393 | 
            +
                      results.should == [winner, loser]
         | 
| 394 | 
            +
                    end
         | 
| 395 | 
            +
                  end
         | 
| 396 | 
            +
             | 
| 397 | 
            +
                  context "against columns ranked with a hash" do
         | 
| 398 | 
            +
                    before do
         | 
| 399 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 400 | 
            +
                        pg_search_scope :search_weighted_by_hash, :against => {:content => 'B', :title => 'A'}
         | 
| 401 | 
            +
                      end
         | 
| 402 | 
            +
                    end
         | 
| 403 | 
            +
             | 
| 404 | 
            +
                    it "returns results sorted by weighted rank" do
         | 
| 405 | 
            +
                      loser = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
         | 
| 406 | 
            +
                      winner = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
         | 
| 407 | 
            +
             | 
| 408 | 
            +
                      results = model_with_pg_search.search_weighted_by_hash('foo')
         | 
| 409 | 
            +
                      results[0].rank.should > results[1].rank
         | 
| 410 | 
            +
                      results.should == [winner, loser]
         | 
| 411 | 
            +
                    end
         | 
| 412 | 
            +
                  end
         | 
| 413 | 
            +
             | 
| 414 | 
            +
                  context "against columns of which only some are ranked" do
         | 
| 415 | 
            +
                    before do
         | 
| 416 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 417 | 
            +
                        pg_search_scope :search_weighted, :against => [:content, [:title, 'A']]
         | 
| 418 | 
            +
                      end
         | 
| 419 | 
            +
                    end
         | 
| 420 | 
            +
             | 
| 421 | 
            +
                    it "returns results sorted by weighted rank using an implied low rank for unranked columns" do
         | 
| 422 | 
            +
                      loser = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
         | 
| 423 | 
            +
                      winner = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
         | 
| 424 | 
            +
             | 
| 425 | 
            +
                      results = model_with_pg_search.search_weighted('foo')
         | 
| 426 | 
            +
                      results[0].rank.should > results[1].rank
         | 
| 427 | 
            +
                      results.should == [winner, loser]
         | 
| 428 | 
            +
                    end
         | 
| 429 | 
            +
                  end
         | 
| 430 | 
            +
                end
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                context "using dmetaphone" do
         | 
| 433 | 
            +
                  before do
         | 
| 434 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 435 | 
            +
                      pg_search_scope :with_dmetaphones, :against => [:title, :content], :using => :dmetaphone
         | 
| 436 | 
            +
                    end
         | 
| 437 | 
            +
                  end
         | 
| 438 | 
            +
             | 
| 439 | 
            +
                  it "returns rows where one searchable column and the query share enough dmetaphones" do
         | 
| 440 | 
            +
                    included = model_with_pg_search.create!(:title => 'Geoff', :content => nil)
         | 
| 441 | 
            +
                    excluded = model_with_pg_search.create!(:title => 'Bob', :content => nil)
         | 
| 442 | 
            +
                    results = model_with_pg_search.with_dmetaphones('Jeff')
         | 
| 443 | 
            +
                    results.should == [included]
         | 
| 444 | 
            +
                  end
         | 
| 445 | 
            +
             | 
| 446 | 
            +
                  it "returns rows where multiple searchable columns and the query share enough dmetaphones" do
         | 
| 447 | 
            +
                    included = model_with_pg_search.create!(:title => 'Geoff', :content => 'George')
         | 
| 448 | 
            +
                    excluded = model_with_pg_search.create!(:title => 'Bob', :content => 'Jones')
         | 
| 449 | 
            +
                    results = model_with_pg_search.with_dmetaphones('Jeff Jorge')
         | 
| 450 | 
            +
                    results.should == [included]
         | 
| 451 | 
            +
                  end
         | 
| 452 | 
            +
             | 
| 453 | 
            +
                  it "returns rows that match dmetaphones that are English stopwords" do
         | 
| 454 | 
            +
                    included = model_with_pg_search.create!(:title => 'White', :content => nil)
         | 
| 455 | 
            +
                    excluded = model_with_pg_search.create!(:title => 'Black', :content => nil)
         | 
| 456 | 
            +
                    results = model_with_pg_search.with_dmetaphones('Wight')
         | 
| 457 | 
            +
                    results.should == [included]
         | 
| 458 | 
            +
                  end
         | 
| 459 | 
            +
                end
         | 
| 460 | 
            +
             | 
| 461 | 
            +
                context "using multiple features" do
         | 
| 462 | 
            +
                  before do
         | 
| 463 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 464 | 
            +
                      pg_search_scope :with_tsearch, :against => :title, :using => :tsearch
         | 
| 465 | 
            +
             | 
| 466 | 
            +
                      pg_search_scope :with_trigram, :against => :title, :using => :trigram
         | 
| 467 | 
            +
             | 
| 468 | 
            +
                      pg_search_scope :with_tsearch_and_trigram_using_array,
         | 
| 469 | 
            +
                                      :against => :title,
         | 
| 470 | 
            +
                                      :using => [:tsearch, :trigram]
         | 
| 471 | 
            +
             | 
| 472 | 
            +
                    end
         | 
| 473 | 
            +
                  end
         | 
| 474 | 
            +
             | 
| 475 | 
            +
                  it "returns rows that match using any of the features" do
         | 
| 476 | 
            +
                    record = model_with_pg_search.create!(:title => "tiling is grouty")
         | 
| 477 | 
            +
             | 
| 478 | 
            +
                    # matches trigram only
         | 
| 479 | 
            +
                    trigram_query = "ling is grouty"
         | 
| 480 | 
            +
                    model_with_pg_search.with_trigram(trigram_query).should include(record)
         | 
| 481 | 
            +
                    model_with_pg_search.with_tsearch(trigram_query).should_not include(record)
         | 
| 482 | 
            +
                    model_with_pg_search.with_tsearch_and_trigram_using_array(trigram_query).should == [record]
         | 
| 483 | 
            +
             | 
| 484 | 
            +
                    # matches tsearch only
         | 
| 485 | 
            +
                    tsearch_query = "tile"
         | 
| 486 | 
            +
                    model_with_pg_search.with_tsearch(tsearch_query).should include(record)
         | 
| 487 | 
            +
                    model_with_pg_search.with_trigram(tsearch_query).should_not include(record)
         | 
| 488 | 
            +
                    model_with_pg_search.with_tsearch_and_trigram_using_array(tsearch_query).should == [record]
         | 
| 489 | 
            +
                  end
         | 
| 490 | 
            +
             | 
| 491 | 
            +
                  context "with feature-specific configuration" do
         | 
| 492 | 
            +
                    before do
         | 
| 493 | 
            +
                      @tsearch_config = tsearch_config = {:dictionary => 'english'}
         | 
| 494 | 
            +
                      @trigram_config = trigram_config = {:foo => 'bar'}
         | 
| 495 | 
            +
             | 
| 496 | 
            +
                      model_with_pg_search.class_eval do
         | 
| 497 | 
            +
                        pg_search_scope :with_tsearch_and_trigram_using_hash,
         | 
| 498 | 
            +
                                        :against => :title,
         | 
| 499 | 
            +
                                        :using => {
         | 
| 500 | 
            +
                                          :tsearch => tsearch_config,
         | 
| 501 | 
            +
                                          :trigram => trigram_config
         | 
| 502 | 
            +
                                        }
         | 
| 503 | 
            +
                      end
         | 
| 504 | 
            +
                    end
         | 
| 505 | 
            +
             | 
| 506 | 
            +
                    it "should pass the custom configuration down to the specified feature" do
         | 
| 507 | 
            +
                      stub_feature = stub(:conditions => "1 = 1", :rank => "1.0")
         | 
| 508 | 
            +
                      PgSearch::Features::TSearch.should_receive(:new).with(anything, @tsearch_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
         | 
| 509 | 
            +
                      PgSearch::Features::Trigram.should_receive(:new).with(anything, @trigram_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
         | 
| 510 | 
            +
             | 
| 511 | 
            +
                      model_with_pg_search.with_tsearch_and_trigram_using_hash("foo")
         | 
| 512 | 
            +
                    end
         | 
| 513 | 
            +
                  end
         | 
| 514 | 
            +
                end
         | 
| 515 | 
            +
             | 
| 516 | 
            +
                context "normalizing diacritics" do
         | 
| 517 | 
            +
                  before do
         | 
| 518 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 519 | 
            +
                      pg_search_scope :search_title_without_diacritics, :against => :title, :normalizing => :diacritics
         | 
| 520 | 
            +
                    end
         | 
| 521 | 
            +
                  end
         | 
| 522 | 
            +
             | 
| 523 | 
            +
                  it "returns rows that match the query but not its diacritics" do
         | 
| 524 | 
            +
                    # \303\241 is a with acute accent
         | 
| 525 | 
            +
                    # \303\251 is e with acute accent
         | 
| 526 | 
            +
             | 
| 527 | 
            +
                    included = model_with_pg_search.create!(:title => "\303\241bcdef")
         | 
| 528 | 
            +
             | 
| 529 | 
            +
                    results = model_with_pg_search.search_title_without_diacritics("abcd\303\251f")
         | 
| 530 | 
            +
                    results.should == [included]
         | 
| 531 | 
            +
                  end
         | 
| 532 | 
            +
                end
         | 
| 533 | 
            +
             | 
| 534 | 
            +
                context "when passed a :ranked_by expression" do
         | 
| 535 | 
            +
                  before do
         | 
| 536 | 
            +
                    model_with_pg_search.class_eval do
         | 
| 537 | 
            +
                      pg_search_scope :search_content_with_default_rank,
         | 
| 538 | 
            +
                                      :against => :content
         | 
| 539 | 
            +
                      pg_search_scope :search_content_with_importance_as_rank,
         | 
| 540 | 
            +
                                      :against => :content,
         | 
| 541 | 
            +
                                      :ranked_by => "importance"
         | 
| 542 | 
            +
                      pg_search_scope :search_content_with_importance_as_rank_multiplier,
         | 
| 543 | 
            +
                                      :against => :content,
         | 
| 544 | 
            +
                                      :ranked_by => ":tsearch * importance"
         | 
| 545 | 
            +
                    end
         | 
| 546 | 
            +
                  end
         | 
| 547 | 
            +
             | 
| 548 | 
            +
                  it "should return records with a rank attribute equal to the :ranked_by expression" do
         | 
| 549 | 
            +
                    model_with_pg_search.create!(:content => 'foo', :importance => 10)
         | 
| 550 | 
            +
                    results = model_with_pg_search.search_content_with_importance_as_rank("foo")
         | 
| 551 | 
            +
                    results.first.rank.should == 10
         | 
| 552 | 
            +
                  end
         | 
| 553 | 
            +
             | 
| 554 | 
            +
                  it "should substitute :tsearch with the tsearch rank expression in the :ranked_by expression" do
         | 
| 555 | 
            +
                    model_with_pg_search.create!(:content => 'foo', :importance => 10)
         | 
| 556 | 
            +
             | 
| 557 | 
            +
                    tsearch_rank = model_with_pg_search.search_content_with_default_rank("foo").first.rank
         | 
| 558 | 
            +
                    multiplied_rank = model_with_pg_search.search_content_with_importance_as_rank_multiplier("foo").first.rank
         | 
| 559 | 
            +
             | 
| 560 | 
            +
                    multiplied_rank.should be_within(0.001).of(tsearch_rank * 10)
         | 
| 561 | 
            +
                  end
         | 
| 562 | 
            +
             | 
| 563 | 
            +
                  it "should return results in descending order of the value of the rank expression" do
         | 
| 564 | 
            +
                    records = [
         | 
| 565 | 
            +
                      model_with_pg_search.create!(:content => 'foo', :importance => 1),
         | 
| 566 | 
            +
                      model_with_pg_search.create!(:content => 'foo', :importance => 3),
         | 
| 567 | 
            +
                      model_with_pg_search.create!(:content => 'foo', :importance => 2)
         | 
| 568 | 
            +
                    ]
         | 
| 569 | 
            +
             | 
| 570 | 
            +
                    results = model_with_pg_search.search_content_with_importance_as_rank("foo")
         | 
| 571 | 
            +
                    results.should == records.sort_by(&:importance).reverse
         | 
| 572 | 
            +
                  end
         | 
| 573 | 
            +
             | 
| 574 | 
            +
                  %w[tsearch trigram dmetaphone].each do |feature|
         | 
| 575 | 
            +
             | 
| 576 | 
            +
                    context "using the #{feature} ranking algorithm" do
         | 
| 577 | 
            +
                      before do
         | 
| 578 | 
            +
                        @scope_name = scope_name = :"search_content_ranked_by_#{feature}"
         | 
| 579 | 
            +
                        model_with_pg_search.class_eval do
         | 
| 580 | 
            +
                          pg_search_scope scope_name,
         | 
| 581 | 
            +
                                          :against => :content,
         | 
| 582 | 
            +
                                          :ranked_by => ":#{feature}"
         | 
| 583 | 
            +
                        end
         | 
| 584 | 
            +
                      end
         | 
| 585 | 
            +
             | 
| 586 | 
            +
                      it "should return results with a rank" do
         | 
| 587 | 
            +
                        model_with_pg_search.create!(:content => 'foo')
         | 
| 588 | 
            +
             | 
| 589 | 
            +
                        results = model_with_pg_search.send(@scope_name, 'foo')
         | 
| 590 | 
            +
                        results.first.rank.should_not be_nil
         | 
| 591 | 
            +
                      end
         | 
| 592 | 
            +
                    end
         | 
| 593 | 
            +
                  end
         | 
| 594 | 
            +
                end
         | 
| 595 | 
            +
              end
         | 
| 596 | 
            +
            end
         |