Commit af65bed179046707e3b00e4b288befdc868d0c82

Authored by Rodrigo Souto
1 parent 07b7133b

Adding pg_search

Showing 39 changed files with 98 additions and 2102 deletions   Show diff stats
app/controllers/application_controller.rb
... ... @@ -165,12 +165,7 @@ class ApplicationController < ActionController::Base
165 165  
166 166 def fallback_find_by_contents(asset, scope, query, paginate_options, options)
167 167 return {:results => scope.paginate(paginate_options)} if query.blank?
168   - klass = asset.to_s.singularize.camelize.constantize
169   - fields = klass::SEARCHABLE_FIELDS.keys.map(&:to_s) & klass.column_names
170   - conditions = fields.map do |field|
171   - "lower(#{klass.table_name}.#{field}) LIKE \"%#{query.downcase.strip}%\""
172   - end.join(' OR ')
173   - {:results => scope.where(conditions).paginate(paginate_options)}
  168 + {:results => scope.like_search(conditions).paginate(paginate_options)}
174 169 end
175 170  
176 171 end
... ...
lib/noosfero/core_ext/active_record.rb
... ... @@ -13,4 +13,17 @@ class ActiveRecord::Base
13 13 key.join('/')
14 14 end
15 15  
  16 + def self.like_search(query)
  17 + if defined?(self::SEARCHABLE_FIELDS)
  18 + fields = self::SEARCHABLE_FIELDS.keys.map(&:to_s) & column_names
  19 + query = query.downcase.strip
  20 + conditions = fields.map do |field|
  21 + "lower(#{table_name}.#{field}) LIKE '%#{query}%'"
  22 + end.join(' OR ')
  23 + where(conditions)
  24 + else
  25 + raise "No searchable fields defined for #{self.name}"
  26 + end
  27 + end
  28 +
16 29 end
... ...
plugins/pg_search/README 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +== Future
  2 +
  3 +If in the future you're considering to make use of weight and ranking on
  4 +the search, you might use the pg_search lib:
  5 +
  6 +https://github.com/Casecommons/pg_search/tree/0.2-stable
  7 +
  8 +Here is how it would be done:
  9 +
  10 +== lib/pg_search_plugin.rb
  11 +
  12 +searchables = %w[ article comment qualifier national_region certifier profile license scrap category ]
  13 +searchables.each { |searchable| require_dependency searchable }
  14 +klasses = searchables.map {|searchable| searchable.camelize.constantize }
  15 +
  16 +klass.class_eval do
  17 + include PgSearch
  18 + pg_search_scope :pg_search_plugin_search,
  19 + :against => klass::SEARCHABLE_FIELDS.keys,
  20 + :using => { :tsearch => {:prefix => true, :tsvector_column => 'pg_search_plugin_tsv' } }
  21 +end
  22 +
  23 +==
  24 +
  25 +You also would want to add the adequate indexes to the this searches. Here is
  26 +an example with the profiles table:
  27 +
  28 +== db/migrate/000_create_indexes_for_profile_search.rb
  29 +
  30 +class CreateTsvIndexesForProfile < ActiveRecord::Migration
  31 + def self.up
  32 + execute "ALTER TABLE profiles ADD COLUMN pg_search_plugin_tsv tsvector"
  33 + fields = Profile::SEARCHABLE_FIELDS.map {|field, weight| "to_tsvector('simple', coalesce(\"profiles\".\"#{field}\", ''))"}.join(' || ')
  34 + execute <<-QUERY
  35 + UPDATE profiles SET pg_search_plugin_tsv = (#{fields});
  36 + QUERY
  37 +
  38 + triggers = "pg_search_plugin_tsv, 'pg_catalog.simple', "
  39 + triggers += Profile::SEARCHABLE_FIELDS.keys.join(', ')
  40 + execute "CREATE TRIGGER pg_search_plugin_profiles_tsvectorupdate BEFORE INSERT OR UPDATE
  41 + ON profiles FOR EACH ROW EXECUTE PROCEDURE
  42 + tsvector_update_trigger(#{triggers});"
  43 + end
  44 +
  45 + def self.down
  46 + execute "drop trigger pg_search_plugin_profiles_tsvectorupdate on profiles"
  47 + execute "alter table profiles drop column pg_search_plugin_tsv"
  48 + end
  49 +end
  50 +
  51 +==
... ...
plugins/pg_search/db/migrate/20130320010051_create_indexes_for_search.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +class CreateIndexesForSearch < ActiveRecord::Migration
  2 + def self.up
  3 + searchables = %w[ article comment qualifier national_region certifier profile license scrap category ]
  4 + klasses = searchables.map {|searchable| searchable.camelize.constantize }
  5 + klasses.each do |klass|
  6 + klass::SEARCHABLE_FIELDS.keys.each do |field|
  7 + execute "create index pg_search_plugin_#{klass.name.singularize.downcase}_#{field} on #{klass.table_name} using gin(to_tsvector('simple', #{field}))"
  8 + end
  9 + end
  10 + end
  11 +
  12 + def self.down
  13 + klasses.each do |klass|
  14 + klass::SEARCHABLE_FIELDS.keys.each do |field|
  15 + execute "drop index pg_search_plugin_#{klass.name.singularize.downcase}_#{field}"
  16 + end
  17 + end
  18 + end
  19 +end
... ...
plugins/pg_search/lib/ext/active_record.rb 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +require_dependency 'active_record'
  2 +
  3 +class ActiveRecord::Base
  4 + def self.pg_search_plugin_search(query)
  5 + if defined?(self::SEARCHABLE_FIELDS)
  6 + conditions = self::SEARCHABLE_FIELDS.map {|field, weight| "to_tsvector('simple', #{field}) @@ '#{query}'"}.join(' OR ')
  7 + where(conditions)
  8 + else
  9 + raise "No searchable fields defined for #{self.name}"
  10 + end
  11 + end
  12 +end
... ...
plugins/pg_search/lib/pg_search/.autotest
... ... @@ -1,5 +0,0 @@
1   -Autotest.add_hook :initialize do |at|
2   - at.add_mapping(%r%^lib/.*\.rb$%, true) { |filename, _|
3   - at.files_matching %r%^spec/.*_spec.rb$%
4   - }
5   -end
plugins/pg_search/lib/pg_search/.rvmrc
... ... @@ -1 +0,0 @@
1   -rvm use ree@pg_search
2 0 \ No newline at end of file
plugins/pg_search/lib/pg_search/CHANGELOG
... ... @@ -1,36 +0,0 @@
1   -### 0.2.1
2   -
3   - * Backport support for searching against tsvector columns (Kris Hicks)
4   -
5   -### 0.2
6   -
7   - * Set dictionary to :simple by default for :tsearch. Before it was unset,
8   - which would fall back to PostgreSQL's default dictionary, usually
9   - "english".
10   -
11   - * Fix a bug with search strings containing a colon ":"
12   -
13   - * Improve performance of :associated_against by only doing one INNER JOIN per
14   - association
15   -
16   -### 0.1.1
17   -
18   - * Fix a bug with dmetaphone searches containing " w " (which dmetaphone maps
19   - to an empty string)
20   -
21   -### 0.1
22   -
23   - * Change API to {:ignoring => :accents} from {:normalizing => :diacritics}
24   -
25   - * Improve documentation
26   -
27   - * Fix bug where :associated_against would not work without an :against
28   - present
29   -
30   -### 0.0.2
31   -
32   - * Fix gem ownership.
33   -
34   -### 0.0.1
35   -
36   - * Initial release.
plugins/pg_search/lib/pg_search/Gemfile
... ... @@ -1,5 +0,0 @@
1   -puts <<-MESSAGE
2   -This project uses multiple Gemfiles in subdirectories of ./gemfiles.
3   -The rake tasks automatically install these bundles as necessary. See rake -T.
4   -MESSAGE
5   -exit 1
6 0 \ No newline at end of file
plugins/pg_search/lib/pg_search/LICENSE
... ... @@ -1,19 +0,0 @@
1   -Copyright (c) 2010-11 Case Commons, LLC
2   -
3   -Permission is hereby granted, free of charge, to any person obtaining a copy
4   -of this software and associated documentation files (the "Software"), to deal
5   -in the Software without restriction, including without limitation the rights
6   -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7   -copies of the Software, and to permit persons to whom the Software is
8   -furnished to do so, subject to the following conditions:
9   -
10   -The above copyright notice and this permission notice shall be included in
11   -all copies or substantial portions of the Software.
12   -
13   -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14   -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15   -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16   -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17   -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18   -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19   -THE SOFTWARE.
plugins/pg_search/lib/pg_search/README.rdoc
... ... @@ -1,351 +0,0 @@
1   -= pg_search
2   -
3   -* http://github.com/casecommons/pg_search/
4   -
5   -== DESCRIPTION
6   -
7   -PgSearch builds named scopes that take advantage of PostgreSQL's full text search
8   -
9   -Read the blog post introducing PgSearch at http://bit.ly/pg_search
10   -
11   -== INSTALL
12   -
13   - gem install pg_search
14   -
15   -=== Rails 3
16   -
17   -In Gemfile
18   -
19   - gem 'pg_search'
20   -
21   -=== Rails 2
22   -
23   -In environment.rb
24   -
25   - config.gem 'pg_search'
26   -
27   -In Rakefile
28   -
29   - require 'rubygems'
30   - require 'pg_search/tasks'
31   -
32   -== USAGE
33   -
34   -To add PgSearch to an ActiveRecord model, simply include the PgSearch module.
35   -
36   - class Shape < ActiveRecord::Base
37   - include PgSearch
38   - end
39   -
40   -=== pg_search_scope
41   -
42   -You can use pg_search_scope to build a search scope. The first parameter is a scope name, and the second parameter is an options hash. The only required option is :against, which tells pg_search_scope which column or columns to search against.
43   -
44   -==== Searching against one column
45   -
46   -To search against a column, pass a symbol as the :against option.
47   -
48   - class BlogPost < ActiveRecord::Base
49   - include PgSearch
50   - pg_search_scope :search_by_title, :against => :title
51   - end
52   -
53   -We now have an ActiveRecord scope named search_by_title on our BlogPost model. It takes one parameter, a search query string.
54   -
55   - BlogPost.create!(:title => "Recent Developments in the World of Pastrami")
56   - BlogPost.create!(:title => "Prosciutto and You: A Retrospective")
57   - BlogPost.search_by_title("pastrami") # => [#<BlogPost id: 2, title: "Recent Developments in the World of Pastrami">]
58   -
59   -==== Searching against multiple columns
60   -
61   -Just pass an Array if you'd like to search more than one column.
62   -
63   - class Person < ActiveRecord::Base
64   - include PgSearch
65   - pg_search_scope :search_by_full_name, :against => [:first_name, :last_name]
66   - end
67   -
68   -Now our search query can match either or both of the columns.
69   -
70   - person_1 = Person.create!(:first_name => "Grant", :last_name => "Hill")
71   - person_2 = Person.create!(:first_name => "Hugh", :last_name => "Grant")
72   -
73   - Person.search_by_full_name("Grant") # => [person_1, person_2]
74   - Person.search_by_full_name("Grant Hill") # => [person_1]
75   -
76   -==== Dynamic search scopes
77   -
78   -Just like with Active Record named scopes, you can pass in a Proc object that returns a hash of options. For instance, the following scope takes a parameter that dynamically chooses which column to search against.
79   -
80   -Important: The returned hash must include a :query key. Its value does not necessary have to be dynamic. You could choose to hard-code it to a specific value if you wanted.
81   -
82   - class Person < ActiveRecord::Base
83   - include PgSearch
84   - pg_search_scope :search_by_name, lambda do |name_part, query|
85   - raise ArgumentError unless [:first, :last].include?(name_part)
86   - {
87   - :against => name_part,
88   - :query => query
89   - }
90   - end
91   - end
92   -
93   - person_1 = Person.create!(:first_name => "Grant", :last_name => "Hill")
94   - person_2 = Person.create!(:first_name => "Hugh", :last_name => "Grant")
95   -
96   - Person.search_by_name :first, "Grant" # => [person_1]
97   - Person.search_by_name :last, "Grant" # => [person_2]
98   -
99   -==== Searching through associations
100   -
101   -You can pass a Hash into the :associated_against option to search columns on other models. The keys are the names of the associations and the value works just like an :against option for the other model.
102   -
103   - class Cracker < ActiveRecord::Base
104   - has_many :cheeses
105   - end
106   -
107   - class Cheese < ActiveRecord::Base
108   - end
109   -
110   - class Salami < ActiveRecord::Base
111   - include PgSearch
112   -
113   - belongs_to :cracker
114   - has_many :cheeses, :through => :cracker
115   -
116   - pg_search_scope :tasty_search, :associated_against => {
117   - :cheeses => [:kind, :brand],
118   - :cracker => :kind
119   - }
120   - end
121   -
122   - salami_1 = Salami.create!
123   - salami_2 = Salami.create!
124   - salami_3 = Salami.create!
125   -
126   - limburger = Cheese.create!(:kind => "Limburger")
127   - brie = Cheese.create!(:kind => "Brie")
128   - pepper_jack = Cheese.create!(:kind => "Pepper Jack")
129   -
130   - Cracker.create!(:kind => "Black Pepper", :cheeses => [brie], :salami => salami_1)
131   - Cracker.create!(:kind => "Ritz", :cheeses => [limburger, pepper_jack], :salami => salami_2)
132   - Cracker.create!(:kind => "Graham", :cheeses => [limburger], :salami => salami_3)
133   -
134   - Salami.tasty_search("pepper") # => [salami_1, salami_2]
135   -
136   -=== Searching using different search features
137   -
138   -By default, pg_search_scope uses the built-in {PostgreSQL text search}[http://www.postgresql.org/docs/current/static/textsearch-intro.html]. If you pass the :features option to pg_search_scope, you can choose alternative search techniques.
139   -
140   - class Beer < ActiveRecord::Base
141   - include PgSearch
142   - pg_search_scope :against => :name, :features => [:tsearch, :trigram, :dmetaphone]
143   - end
144   -
145   -The currently implemented features are
146   -
147   -* :tsearch - {Full text search}[http://www.postgresql.org/docs/current/static/textsearch-intro.html] (built-in with 8.3 and later, available as a contrib package for some earlier versions)
148   -* :trigram - {Trigram search}[http://www.postgresql.org/docs/current/static/pgtrgm.html], which requires the trigram contrib package
149   -* :dmetaphone - {Double Metaphone search}[http://www.postgresql.org/docs/9.0/static/fuzzystrmatch.html#AEN120188], which requires the fuzzystrmatch contrib package
150   -
151   -==== :tsearch (Full Text Search)
152   -
153   -PostgreSQL's built-in full text search supports weighting, prefix searches, and stemming in multiple languages.
154   -
155   -===== Weighting
156   -Each searchable column can be given a weight of "A", "B", "C", or "D". Columns with earlier letters are weighted higher than those with later letters. So, in the following example, the title is the most important, followed by the subtitle, and finally the content.
157   -
158   - class NewsArticle < ActiveRecord::Base
159   - include PgSearch
160   - pg_search_scope :against => {
161   - :title => 'A',
162   - :subtitle => 'B',
163   - :content => 'C'
164   - }
165   - end
166   -
167   -You can also pass the weights in as an array of arrays, or any other structure that responds to #each and yields either a single symbol or a symbol and a weight. If you omit the weight, a default will be used.
168   -
169   - class NewsArticle < ActiveRecord::Base
170   - include PgSearch
171   - pg_search_scope :against => [
172   - [:title, 'A'],
173   - [:subtitle, 'B'],
174   - [:content, 'C']
175   - ]
176   - end
177   -
178   - class NewsArticle < ActiveRecord::Base
179   - include PgSearch
180   - pg_search_scope :against => [
181   - [:title, 'A'],
182   - {:subtitle => 'B'},
183   - :content
184   - ]
185   - end
186   -
187   -===== :prefix
188   -
189   -PostgreSQL's full text search matches on whole words by default. If you want to search for partial words, however, you can set :prefix to true. Since this is a :tsearch-specific option, you should pass it to :tsearch directly, as shown in the following example.
190   -
191   - class Superhero < ActiveRecord::Base
192   - include PgSearch
193   - pg_search_scope :whose_name_starts_with,
194   - :against => :name,
195   - :using => {
196   - :tsearch => {:prefix => true}
197   - }
198   - end
199   -
200   - batman = Superhero.create :name => 'Batman'
201   - batgirl = Superhero.create :name => 'Batgirl'
202   - robin = Superhero.create :name => 'Robin'
203   -
204   - Superhero.whose_name_starts_with("Bat") # => [batman, batgirl]
205   -
206   -===== :dictionary
207   -
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.
209   -
210   - class BoringTweet < ActiveRecord::Base
211   - include PgSearch
212   - pg_search_scope :kinda_matching,
213   - :against => :text,
214   - :using => {
215   - :tsearch => {:dictionary => "english"}
216   - }
217   - pg_search_scope :literally_matching,
218   - :against => :text,
219   - :using => {
220   - :tsearch => {:dictionary => "simple"}
221   - }
222   - end
223   -
224   - sleepy = BoringTweet.create! :text => "I snoozed my alarm for fourteen hours today. I bet I can beat that tomorrow! #sleepy"
225   - sleeping = BoringTweet.create! :text => "You know what I like? Sleeping. That's what. #enjoyment"
226   - sleeper = BoringTweet.create! :text => "Have you seen Woody Allen's movie entitled Sleeper? Me neither. #boycott"
227   -
228   - BoringTweet.kinda_matching("sleeping") # => [sleepy, sleeping, sleeper]
229   - BoringTweet.literally_matching("sleeping") # => [sleeping]
230   -
231   -==== :dmetaphone (Double Metaphone soundalike search)
232   -
233   -{Double Metaphone}[http://en.wikipedia.org/wiki/Double_Metaphone] is an algorithm for matching words that sound alike even if they are spelled very differently. For example, "Geoff" and "Jeff" sound identical and thus match. Currently, this is not a true double-metaphone, as only the first metaphone is used for searching.
234   -
235   -Double Metaphone support is currently available as part of the {fuzzystrmatch contrib package}[http://www.postgresql.org/docs/current/static/fuzzystrmatch.html] that must be installed before this feature can be used. In addition to the contrib package, you must install a utility function into your database. To generate a migration for this, add the following line to your Rakefile:
236   -
237   - include "pg_search/tasks"
238   -
239   -and then run:
240   -
241   - $ rake pg_search:migration:dmetaphone
242   -
243   -The following example shows how to use :dmetaphone.
244   -
245   - class Word < ActiveRecord::Base
246   - include PgSearch
247   - pg_search_scope :that_sounds_like,
248   - :against => :spelling,
249   - :using => :dmetaphone
250   - end
251   -
252   - four = Word.create! :spelling => 'four'
253   - far = Word.create! :spelling => 'far'
254   - fur = Word.create! :spelling => 'fur'
255   - five = Word.create! :spelling => 'five'
256   -
257   - Word.that_sounds_like("fir") # => [four, far, fur]
258   -
259   -==== :trigram (Trigram search)
260   -
261   -Trigram search works by counting how many three-letter substrings (or "trigrams") match between the query and the text. For example, the string "Lorem ipsum" can be split into the following trigrams:
262   -
263   - [" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
264   -
265   -Trigram search has some ability to work even with typos and misspellings in the query or text.
266   -
267   -Trigram support is currently available as part of the {pg_trgm contrib package}[http://www.postgresql.org/docs/current/static/pgtrgm.html] that must be installed before this feature can be used.
268   -
269   -
270   - class Website < ActiveRecord::Base
271   - include PgSearch
272   - pg_search_scope :kinda_spelled_like,
273   - :against => :name,
274   - :using => :trigram
275   - end
276   -
277   - yahooo = Website.create! :name => "Yahooo!"
278   - yohoo = Website.create! :name => "Yohoo!"
279   - gogle = Website.create! :name => "Gogle"
280   - facebook = Website.create! :name => "Facebook"
281   -
282   - Website.kinda_spelled_like("Yahoo!") # => [yahooo, yohoo]
283   -
284   -=== Ignoring accent marks
285   -
286   -Most of the time you will want to ignore accent marks when searching. This makes it possible to find words like "piñata" when searching with the query "pinata". If you set a pg_search_scope to ignore accents, it will ignore accents in both the searchable text and the query terms.
287   -
288   -Ignoring accents uses the {unaccent contrib package}[http://www.postgresql.org/docs/current/static/unaccent.html] that must be installed before this feature can be used.
289   -
290   -
291   - class SpanishQuestion < ActiveRecord::Base
292   - include PgSearch
293   - pg_search_scope :gringo_search,
294   - :against => :word,
295   - :ignoring => :accents
296   - end
297   -
298   - what = SpanishQuestion.create(:word => "Qué")
299   - how_many = SpanishQuestion.create(:word => "Cuánto")
300   - how = SpanishQuestion.create(:word => "Cómo")
301   -
302   - SpanishQuestion.gringo_search("Que") # => [what]
303   - SpanishQuestion.gringo_search("Cüåñtô") # => [how_many]
304   -
305   -=== Using tsvector columns
306   -
307   -PostgreSQL allows you the ability to search against a column with type tsvector instead of using an expression; this speeds up searching dramatically as it offloads creation of the tsvector that the tsquery is evaluated against.
308   -
309   -To use this functionality you'll need to do a few things:
310   -
311   -* Create a column of type tsvector that you'd like to search against. If you want to search using multiple search methods, for example tsearch and dmetaphone, you'll need a column for each.
312   -* Create a trigger function that will update the column(s) using the expression appropriate for that type of search. See: http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS
313   -* Should you have any pre-existing data in the table, update the newly-created tsvector columns with the expression that your trigger function uses.
314   -* Add the option to pg_search_scope, e.g:
315   -
316   - pg_search_scope :fast_content_search,
317   - :against => :content,
318   - :using => {
319   - dmetaphone: {
320   - tsvector_column: 'tsvector_content_dmetaphone'
321   - },
322   - tsearch: {
323   - dictionary: 'english',
324   - tsvector_column: 'tsvector_content_tsearch'
325   - }
326   - trigram: {} # trigram does not use tsvectors
327   - }
328   -
329   -Please note that the :against column is only used when the tsvector_column is not present for the search type.
330   -
331   -== REQUIREMENTS
332   -
333   -* ActiveRecord 2 or 3
334   -* Postgresql
335   -* Postgresql contrib modules for certain features
336   -
337   -== ATTRIBUTIONS
338   -
339   -PgSearch would not have been possible without inspiration from
340   -{texticle}[https://github.com/tenderlove/texticle]. Thanks to
341   -{Aaron Patterson}[http://tenderlovemaking.com/]!
342   -
343   -== CONTRIBUTIONS AND FEEDBACK
344   -
345   -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.
346   -
347   -We also have a {Google Group}[http://groups.google.com/group/casecommons-dev] for discussing pg_search and other Case Commons open source projects.
348   -
349   -== LICENSE
350   -
351   -MIT
plugins/pg_search/lib/pg_search/Rakefile
... ... @@ -1,44 +0,0 @@
1   -require 'bundler'
2   -Bundler::GemHelper.install_tasks
3   -
4   -task :default => :spec
5   -
6   -environments = %w[rails2 rails3]
7   -major, minor, revision = RUBY_VERSION.split(".").map{|str| str.to_i }
8   -
9   -in_environment = lambda do |environment, command|
10   - sh %Q{export BUNDLE_GEMFILE="gemfiles/#{environment}/Gemfile"; bundle update && bundle exec #{command}}
11   -end
12   -
13   -in_all_environments = lambda do |command|
14   - environments.each do |environment|
15   - next if environment == "rails2" && major == 1 && minor > 8
16   - puts "\n---#{environment}---\n"
17   - in_environment.call(environment, command)
18   - end
19   -end
20   -
21   -desc "Run all specs against ActiveRecord 2 and 3"
22   -task "spec" do
23   - in_all_environments.call('rspec spec')
24   -end
25   -
26   -task "doc" do
27   - in_environment.call("rails3", "rspec --format d spec")
28   -end
29   -
30   -namespace "autotest" do
31   - environments.each do |environment|
32   - desc "Run autotest in #{environment}"
33   - task environment do
34   - in_environment.call(environment, 'autotest -s rspec2')
35   - end
36   - end
37   -end
38   -
39   -namespace "doc" do
40   - desc "Generate README and preview in browser"
41   - task "readme" do
42   - sh "rdoc -c utf8 README.rdoc && open doc/files/README_rdoc.html"
43   - end
44   -end
plugins/pg_search/lib/pg_search/TODO
... ... @@ -1,10 +0,0 @@
1   - * Railtie for rake tasks
2   - * Tracker project
3   - * Exceptions for missing trigram, dmetaphone, etc. support
4   - * LIKE search
5   - * accept a block and pass it to the underlying scope
6   - * ability to search again a tsvector column
7   - * Generate indexes
8   - * Allow for chaining multiple pg_search scopes
9   -
10   -Also see https://github.com/Casecommons/pg_search/issues
plugins/pg_search/lib/pg_search/defaut_search.rb
... ... @@ -1,8 +0,0 @@
1   -searchables = %w[ article comment qualifier article national_region certifier profile license scrap category ]
2   -searchables.each { |searchable| require_dependency searchable }
3   -klasses = searchables.map {|searchable| searchable.camelize.constantize}
4   -
5   -klasses.module_eval do
6   - include PgSearch
7   - pg_search_scope :pg_search_plugin_search, :against => SEARCHABLE_FIELDS
8   -end
plugins/pg_search/lib/pg_search/gemfiles/Gemfile.common
... ... @@ -1,9 +0,0 @@
1   -source "http://rubygems.org"
2   -
3   -gem "pg"
4   -
5   -group :test do
6   - gem "rspec", ">=2.4"
7   - gem "autotest"
8   - gem "with_model"
9   -end
plugins/pg_search/lib/pg_search/gemfiles/rails2/Gemfile
... ... @@ -1,4 +0,0 @@
1   -filename = File.join(File.dirname(__FILE__), '..', 'Gemfile.common')
2   -eval(File.read(filename), binding, filename, 1)
3   -
4   -gem "activerecord", "~>2.3.5"
plugins/pg_search/lib/pg_search/gemfiles/rails3/Gemfile
... ... @@ -1,4 +0,0 @@
1   -filename = File.join(File.dirname(__FILE__), '..', 'Gemfile.common')
2   -eval(File.read(filename), binding, filename, 1)
3   -
4   -gem "activerecord", "~>3.0.1"
plugins/pg_search/lib/pg_search/lib/pg_search.rb
... ... @@ -1,32 +0,0 @@
1   -require "active_record"
2   -require "pg_search/configuration"
3   -require "pg_search/features"
4   -require "pg_search/normalizer"
5   -require "pg_search/scope"
6   -require "pg_search/scope_options"
7   -require "pg_search/version"
8   -#require "pg_search/railtie" if defined?(Rails) && defined?(Rails::Railtie)
9   -
10   -module PgSearch
11   - def self.included(base)
12   - base.send(:extend, ClassMethods)
13   - end
14   -
15   - module ClassMethods
16   - def pg_search_scope(name, options)
17   - scope = PgSearch::Scope.new(name, self, options)
18   - scope_method =
19   - if respond_to?(:scope) && !protected_methods.include?('scope')
20   - :scope # ActiveRecord 3.x
21   - else
22   - :named_scope # ActiveRecord 2.x
23   - end
24   -
25   - send(scope_method, name, scope.to_proc)
26   - end
27   - end
28   -
29   - def rank
30   - attributes['pg_search_rank'].to_f
31   - end
32   -end
plugins/pg_search/lib/pg_search/lib/pg_search/configuration.rb
... ... @@ -1,80 +0,0 @@
1   -require "pg_search/configuration/association"
2   -require "pg_search/configuration/column"
3   -
4   -module PgSearch
5   - class Configuration
6   - def initialize(options, model)
7   - options = options.reverse_merge(default_options)
8   - assert_valid_options(options)
9   - @options = options
10   - @model = model
11   - end
12   -
13   - def columns
14   - regular_columns + associated_columns
15   - end
16   -
17   - def regular_columns
18   - return [] unless @options[:against]
19   - Array(@options[:against]).map do |column_name, weight|
20   - Column.new(column_name, weight, @model)
21   - end
22   - end
23   -
24   - def associations
25   - return [] unless @options[:associated_against]
26   - @options[:associated_against].map do |association, column_names|
27   - association = Association.new(@model, association, column_names)
28   - association
29   - end.flatten
30   - end
31   -
32   - def associated_columns
33   - associations.map(&:columns).flatten
34   - end
35   -
36   - def query
37   - @options[:query].to_s
38   - end
39   -
40   - def ignore
41   - Array.wrap(@options[:ignoring])
42   - end
43   -
44   - def ranking_sql
45   - @options[:ranked_by]
46   - end
47   -
48   - def features
49   - Array(@options[:using])
50   - end
51   -
52   - private
53   -
54   - def default_options
55   - {:using => :tsearch}
56   - end
57   -
58   - def assert_valid_options(options)
59   - valid_keys = [:against, :ranked_by, :ignoring, :using, :query, :associated_against]
60   - valid_values = {
61   - :ignoring => [:accents]
62   - }
63   -
64   - unless options[:against] || options[:associated_against]
65   - raise ArgumentError, "the search scope #{@name} must have :against#{" or :associated_against" if defined?(ActiveRecord::Relation)} in its options"
66   - end
67   - raise ArgumentError, ":associated_against requires ActiveRecord 3 or later" if options[:associated_against] && !defined?(ActiveRecord::Relation)
68   -
69   - options.assert_valid_keys(valid_keys)
70   -
71   - valid_values.each do |key, values_for_key|
72   - Array.wrap(options[key]).each do |value|
73   - unless values_for_key.include?(value)
74   - raise ArgumentError, ":#{key} cannot accept #{value}"
75   - end
76   - end
77   - end
78   - end
79   - end
80   -end
plugins/pg_search/lib/pg_search/lib/pg_search/configuration/association.rb
... ... @@ -1,34 +0,0 @@
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
plugins/pg_search/lib/pg_search/lib/pg_search/configuration/column.rb
... ... @@ -1,42 +0,0 @@
1   -require 'digest'
2   -
3   -module PgSearch
4   - class Configuration
5   - class Column
6   - attr_reader :weight, :association
7   -
8   - def initialize(column_name, weight, model, association = nil)
9   - @column_name = column_name.to_s
10   - @weight = weight
11   - @model = model
12   - @association = association
13   - end
14   -
15   - def table
16   - foreign? ? @association.table_name : @model.table_name
17   - end
18   -
19   - def full_name
20   - "#{@model.connection.quote_table_name(table)}.#{@model.connection.quote_column_name(@column_name)}"
21   - end
22   -
23   - def to_sql
24   - name = if foreign?
25   - "#{@association.subselect_alias}.#{self.alias}"
26   - else
27   - full_name
28   - end
29   - "coalesce(#{name}, '')"
30   - end
31   -
32   - def foreign?
33   - @association.present?
34   - end
35   -
36   - def alias
37   - name = [association.subselect_alias, @column_name].compact.join('_')
38   - "pg_search_#{Digest::SHA2.hexdigest(name)}"
39   - end
40   - end
41   - end
42   -end
plugins/pg_search/lib/pg_search/lib/pg_search/features.rb
... ... @@ -1,7 +0,0 @@
1   -module PgSearch
2   - module Features
3   - end
4   -end
5   -require 'pg_search/features/dmetaphone'
6   -require 'pg_search/features/trigram'
7   -require 'pg_search/features/tsearch'
plugins/pg_search/lib/pg_search/lib/pg_search/features/dmetaphone.rb
... ... @@ -1,28 +0,0 @@
1   -require "active_support/core_ext/module/delegation"
2   -
3   -module PgSearch
4   - module Features
5   - class DMetaphone
6   - delegate :conditions, :rank, :to => :'@tsearch'
7   -
8   - # config is temporary as we refactor
9   - def initialize(query, options, config, model, normalizer)
10   - dmetaphone_normalizer = Normalizer.new(normalizer)
11   - options = (options || {}).merge(:dictionary => 'simple')
12   - @tsearch = TSearch.new(query, options, config, model, dmetaphone_normalizer)
13   - end
14   -
15   - # Decorates a normalizer with dmetaphone processing.
16   - class Normalizer
17   - def initialize(normalizer_to_wrap)
18   - @decorated_normalizer = normalizer_to_wrap
19   - end
20   -
21   - def add_normalization(original_sql)
22   - otherwise_normalized_sql = @decorated_normalizer.add_normalization(original_sql)
23   - "pg_search_dmetaphone(#{otherwise_normalized_sql})"
24   - end
25   - end
26   - end
27   - end
28   -end
plugins/pg_search/lib/pg_search/lib/pg_search/features/trigram.rb
... ... @@ -1,29 +0,0 @@
1   -require "active_support/core_ext/module/delegation"
2   -
3   -module PgSearch
4   - module Features
5   - class Trigram
6   - def initialize(query, options, columns, model, normalizer)
7   - @query = query
8   - @options = options
9   - @columns = columns
10   - @model = model
11   - @normalizer = normalizer
12   - end
13   -
14   - def conditions
15   - ["(#{@normalizer.add_normalization(document)}) % #{@normalizer.add_normalization(":query")}", {:query => @query}]
16   - end
17   -
18   - def rank
19   - ["similarity((#{@normalizer.add_normalization(document)}), #{@normalizer.add_normalization(":query")})", {:query => @query}]
20   - end
21   -
22   - private
23   -
24   - def document
25   - @columns.map { |column| column.to_sql }.join(" || ' ' || ")
26   - end
27   - end
28   - end
29   -end
plugins/pg_search/lib/pg_search/lib/pg_search/features/tsearch.rb
... ... @@ -1,72 +0,0 @@
1   -require "active_support/core_ext/module/delegation"
2   -
3   -module PgSearch
4   - module Features
5   - class TSearch
6   - delegate :connection, :quoted_table_name, :to => :'@model'
7   -
8   - def initialize(query, options, columns, model, normalizer)
9   - @query = query
10   - @options = options || {}
11   - @model = model
12   - @columns = columns
13   - @normalizer = normalizer
14   - end
15   -
16   - def conditions
17   - ["(#{tsdocument}) @@ (#{tsquery})", interpolations]
18   - end
19   -
20   - def rank
21   - tsearch_rank
22   - end
23   -
24   - private
25   -
26   - def interpolations
27   - {:query => @query.to_s, :dictionary => dictionary.to_s}
28   - end
29   -
30   - def document
31   - @columns.map { |column| column.to_sql }.join(" || ' ' || ")
32   - end
33   -
34   - def tsquery
35   - return "''" if @query.blank?
36   -
37   - @query.split(" ").compact.map do |term|
38   - sanitized_term = term.gsub(/['?\-\\:]/, " ")
39   -
40   - term_sql = @normalizer.add_normalization(connection.quote(sanitized_term))
41   -
42   - # After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
43   - tsquery_sql = "#{connection.quote("' ")} || #{term_sql} || #{connection.quote(" '")}"
44   -
45   - # Add tsearch prefix operator if we're using a prefix search.
46   - tsquery_sql = "#{tsquery_sql} || #{connection.quote(':*')}" if @options[:prefix]
47   -
48   - "to_tsquery(:dictionary, #{tsquery_sql})"
49   - end.join(" && ")
50   - end
51   -
52   - def tsdocument
53   - if @options[:tsvector_column]
54   - @options[:tsvector_column].to_s
55   - else
56   - @columns.map do |search_column|
57   - tsvector = "to_tsvector(:dictionary, #{@normalizer.add_normalization(search_column.to_sql)})"
58   - search_column.weight.nil? ? tsvector : "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
59   - end.join(" || ")
60   - end
61   - end
62   -
63   - def tsearch_rank
64   - ["ts_rank((#{tsdocument}), (#{tsquery}))", interpolations]
65   - end
66   -
67   - def dictionary
68   - @options[:dictionary] || :simple
69   - end
70   - end
71   - end
72   -end
plugins/pg_search/lib/pg_search/lib/pg_search/normalizer.rb
... ... @@ -1,13 +0,0 @@
1   -module PgSearch
2   - class Normalizer
3   - def initialize(config)
4   - @config = config
5   - end
6   -
7   - def add_normalization(original_sql)
8   - normalized_sql = original_sql
9   - normalized_sql = "unaccent(#{normalized_sql})" if @config.ignore.include?(:accents)
10   - normalized_sql
11   - end
12   - end
13   -end
plugins/pg_search/lib/pg_search/lib/pg_search/railtie.rb
... ... @@ -1,11 +0,0 @@
1   -require 'pg_search'
2   -#require 'rails'
3   -
4   -module PgSearch
5   - class Railtie < Rails::Railtie
6   - rake_tasks do
7   - raise
8   - load "pg_search/tasks.rb"
9   - end
10   - end
11   -end
plugins/pg_search/lib/pg_search/lib/pg_search/scope.rb
... ... @@ -1,31 +0,0 @@
1   -module PgSearch
2   - class Scope
3   - def initialize(name, model, scope_options_or_proc)
4   - @name = name
5   - @model = model
6   - @options_proc = build_options_proc(scope_options_or_proc)
7   - end
8   -
9   - def to_proc
10   - lambda { |*args|
11   - config = Configuration.new(@options_proc.call(*args), @model)
12   - ScopeOptions.new(@name, @model, config).to_hash
13   - }
14   - end
15   -
16   - private
17   -
18   - def build_options_proc(scope_options_or_proc)
19   - case scope_options_or_proc
20   - when Proc
21   - scope_options_or_proc
22   - when Hash
23   - lambda { |query|
24   - scope_options_or_proc.reverse_merge(:query => query)
25   - }
26   - else
27   - raise ArgumentError, "A PgSearch scope expects a Proc or Hash for its options"
28   - end
29   - end
30   - end
31   -end
plugins/pg_search/lib/pg_search/lib/pg_search/scope_options.rb
... ... @@ -1,73 +0,0 @@
1   -require "active_support/core_ext/module/delegation"
2   -
3   -module PgSearch
4   - class ScopeOptions
5   - attr_reader :model
6   -
7   - delegate :connection, :quoted_table_name, :sanitize_sql_array, :to => :model
8   -
9   - def initialize(name, model, config)
10   - @name = name
11   - @model = model
12   - @config = config
13   -
14   - @feature_options = @config.features.inject({}) do |features_hash, (feature_name, feature_options)|
15   - features_hash.merge(
16   - feature_name => feature_options
17   - )
18   - end
19   - @feature_names = @config.features.map { |feature_name, feature_options| feature_name }
20   - end
21   -
22   - def to_hash
23   - {
24   - :select => "#{quoted_table_name}.*, (#{rank}) AS pg_search_rank",
25   - :conditions => conditions,
26   - :order => "pg_search_rank DESC, #{primary_key} ASC",
27   - :joins => joins
28   - }
29   - end
30   -
31   - private
32   -
33   - def conditions
34   - @feature_names.map { |feature_name| "(#{sanitize_sql_array(feature_for(feature_name).conditions)})" }.join(" OR ")
35   - end
36   -
37   - def primary_key
38   - "#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
39   - end
40   -
41   - def joins
42   - @config.associations.map do |association|
43   - association.join(primary_key)
44   - end.join(' ')
45   - end
46   -
47   - def feature_for(feature_name)
48   - feature_name = feature_name.to_sym
49   -
50   - feature_class = {
51   - :dmetaphone => Features::DMetaphone,
52   - :tsearch => Features::TSearch,
53   - :trigram => Features::Trigram
54   - }[feature_name]
55   -
56   - raise ArgumentError.new("Unknown feature: #{feature_name}") unless feature_class
57   -
58   - normalizer = Normalizer.new(@config)
59   -
60   - feature_class.new(@config.query, @feature_options[feature_name], @config.columns, @model, normalizer)
61   - end
62   -
63   - def tsearch_rank
64   - sanitize_sql_array(@feature_names[Features::TSearch].rank)
65   - end
66   -
67   - def rank
68   - (@config.ranking_sql || ":tsearch").gsub(/:(\w*)/) do
69   - sanitize_sql_array(feature_for($1).rank)
70   - end
71   - end
72   - end
73   -end
plugins/pg_search/lib/pg_search/lib/pg_search/tasks.rb
... ... @@ -1,37 +0,0 @@
1   -require 'rake'
2   -require 'pg_search'
3   -
4   -namespace :pg_search do
5   - namespace :migration do
6   - desc "Generate migration to add support functions for :dmetaphone"
7   - task :dmetaphone do
8   - now = Time.now.utc
9   - filename = "#{now.strftime('%Y%m%d%H%M%S')}_add_pg_search_dmetaphone_support_functions.rb"
10   -
11   - dmetaphone_sql = File.read(File.join(File.dirname(__FILE__), '..', '..', 'sql', 'dmetaphone.sql')).chomp
12   - uninstall_dmetaphone_sql = File.read(File.join(File.dirname(__FILE__), '..', '..', 'sql', 'uninstall_dmetaphone.sql')).chomp
13   -
14   - File.open(Rails.root + 'db' + 'migrate' + filename, 'wb') do |migration_file|
15   - migration_file.puts <<-RUBY
16   -class AddPgSearchDmetaphoneSupportFunctions < ActiveRecord::Migration
17   - def self.up
18   - say_with_time("Adding support functions for pg_search :dmetaphone") do
19   - ActiveRecord::Base.connection.execute(<<-SQL)
20   - #{dmetaphone_sql}
21   - SQL
22   - end
23   - end
24   -
25   - def self.down
26   - say_with_time("Dropping support functions for pg_search :dmetaphone") do
27   - ActiveRecord::Base.connection.execute(<<-SQL)
28   - #{uninstall_dmetaphone_sql}
29   - SQL
30   - end
31   - end
32   -end
33   - RUBY
34   - end
35   - end
36   - end
37   -end
plugins/pg_search/lib/pg_search/lib/pg_search/version.rb
... ... @@ -1,3 +0,0 @@
1   -module PgSearch
2   - VERSION = "0.2.1"
3   -end
plugins/pg_search/lib/pg_search/pg_search.gemspec
... ... @@ -1,19 +0,0 @@
1   -# -*- encoding: utf-8 -*-
2   -$:.push File.expand_path("../lib", __FILE__)
3   -require "pg_search/version"
4   -
5   -Gem::Specification.new do |s|
6   - s.name = "pg_search"
7   - s.version = PgSearch::VERSION
8   - s.platform = Gem::Platform::RUBY
9   - s.authors = ["Case Commons, LLC"]
10   - s.email = ["casecommons-dev@googlegroups.com"]
11   - s.homepage = "https://github.com/Casecommons/pg_search"
12   - s.summary = %q{PgSearch builds ActiveRecord named scopes that take advantage of PostgreSQL's full text search}
13   - s.description = %q{PgSearch builds ActiveRecord named scopes that take advantage of PostgreSQL's full text search}
14   -
15   - s.files = `git ls-files`.split("\n")
16   - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17   - s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18   - s.require_paths = ["lib"]
19   -end
plugins/pg_search/lib/pg_search/script/setup-contrib
... ... @@ -1,12 +0,0 @@
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
plugins/pg_search/lib/pg_search/spec/associations_spec.rb
... ... @@ -1,316 +0,0 @@
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 "without an :against" do
8   - with_model :AssociatedModel do
9   - table do |t|
10   - t.string "title"
11   - end
12   - end
13   -
14   - with_model :ModelWithoutAgainst 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_another, :associated_against => {:another_model => :title}
25   - end
26   - end
27   -
28   - it "returns rows that match the query in the columns of the associated model only" do
29   - associated = AssociatedModel.create!(:title => 'abcdef')
30   - included = [
31   - ModelWithoutAgainst.create!(:title => 'abcdef', :another_model => associated),
32   - ModelWithoutAgainst.create!(:title => 'ghijkl', :another_model => associated)
33   - ]
34   - excluded = [
35   - ModelWithoutAgainst.create!(:title => 'abcdef')
36   - ]
37   -
38   - results = ModelWithoutAgainst.with_another('abcdef')
39   - results.map(&:title).should =~ included.map(&:title)
40   - results.should_not include(excluded)
41   - end
42   - end
43   -
44   - context "through a belongs_to association" do
45   - with_model :AssociatedModel do
46   - table do |t|
47   - t.string 'title'
48   - end
49   - end
50   -
51   - with_model :ModelWithBelongsTo do
52   - table do |t|
53   - t.string 'title'
54   - t.belongs_to 'another_model'
55   - end
56   -
57   - model do
58   - include PgSearch
59   - belongs_to :another_model, :class_name => 'AssociatedModel'
60   -
61   - pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
62   - end
63   - end
64   -
65   - it "returns rows that match the query in either its own columns or the columns of the associated model" do
66   - associated = AssociatedModel.create!(:title => 'abcdef')
67   - included = [
68   - ModelWithBelongsTo.create!(:title => 'ghijkl', :another_model => associated),
69   - ModelWithBelongsTo.create!(:title => 'abcdef')
70   - ]
71   - excluded = ModelWithBelongsTo.create!(:title => 'mnopqr',
72   - :another_model => AssociatedModel.create!(:title => 'stuvwx'))
73   -
74   - results = ModelWithBelongsTo.with_associated('abcdef')
75   - results.map(&:title).should =~ included.map(&:title)
76   - results.should_not include(excluded)
77   - end
78   - end
79   -
80   - context "through a has_many association" do
81   - with_model :AssociatedModelWithHasMany do
82   - table do |t|
83   - t.string 'title'
84   - t.belongs_to 'ModelWithHasMany'
85   - end
86   - end
87   -
88   - with_model :ModelWithHasMany do
89   - table do |t|
90   - t.string 'title'
91   - end
92   -
93   - model do
94   - include PgSearch
95   - has_many :other_models, :class_name => 'AssociatedModelWithHasMany', :foreign_key => 'ModelWithHasMany_id'
96   -
97   - pg_search_scope :with_associated, :against => [:title], :associated_against => {:other_models => :title}
98   - end
99   - end
100   -
101   - it "returns rows that match the query in either its own columns or the columns of the associated model" do
102   - included = [
103   - ModelWithHasMany.create!(:title => 'abcdef', :other_models => [
104   - AssociatedModelWithHasMany.create!(:title => 'foo'),
105   - AssociatedModelWithHasMany.create!(:title => 'bar')
106   - ]),
107   - ModelWithHasMany.create!(:title => 'ghijkl', :other_models => [
108   - AssociatedModelWithHasMany.create!(:title => 'foo bar'),
109   - AssociatedModelWithHasMany.create!(:title => 'mnopqr')
110   - ]),
111   - ModelWithHasMany.create!(:title => 'foo bar')
112   - ]
113   - excluded = ModelWithHasMany.create!(:title => 'stuvwx', :other_models => [
114   - AssociatedModelWithHasMany.create!(:title => 'abcdef')
115   - ])
116   -
117   - results = ModelWithHasMany.with_associated('foo bar')
118   - results.map(&:title).should =~ included.map(&:title)
119   - results.should_not include(excluded)
120   - end
121   - end
122   -
123   - context "across multiple associations" do
124   - context "on different tables" do
125   - with_model :FirstAssociatedModel do
126   - table do |t|
127   - t.string 'title'
128   - t.belongs_to 'model_with_many_associations'
129   - end
130   - model {}
131   - end
132   -
133   - with_model :SecondAssociatedModel do
134   - table do |t|
135   - t.string 'title'
136   - end
137   - model {}
138   - end
139   -
140   - with_model :ModelWithManyAssociations do
141   - table do |t|
142   - t.string 'title'
143   - t.belongs_to 'model_of_second_type'
144   - end
145   -
146   - model do
147   - include PgSearch
148   - has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'model_with_many_associations_id'
149   - belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
150   -
151   - pg_search_scope :with_associated, :against => :title,
152   - :associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
153   - end
154   - end
155   -
156   - it "returns rows that match the query in either its own columns or the columns of the associated model" do
157   - matching_second = SecondAssociatedModel.create!(:title => "foo bar")
158   - unmatching_second = SecondAssociatedModel.create!(:title => "uiop")
159   -
160   - included = [
161   - ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
162   - FirstAssociatedModel.create!(:title => 'foo'),
163   - FirstAssociatedModel.create!(:title => 'bar')
164   - ]),
165   - ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
166   - FirstAssociatedModel.create!(:title => 'foo bar'),
167   - FirstAssociatedModel.create!(:title => 'mnopqr')
168   - ]),
169   - ModelWithManyAssociations.create!(:title => 'foo bar'),
170   - ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
171   - ]
172   - excluded = [
173   - ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
174   - FirstAssociatedModel.create!(:title => 'abcdef')
175   - ]),
176   - ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
177   - ]
178   -
179   - results = ModelWithManyAssociations.with_associated('foo bar')
180   - results.map(&:title).should =~ included.map(&:title)
181   - excluded.each { |object| results.should_not include(object) }
182   - end
183   - end
184   -
185   - context "on the same table" do
186   - with_model :DoublyAssociatedModel do
187   - table do |t|
188   - t.string 'title'
189   - t.belongs_to 'model_with_double_association'
190   - t.belongs_to 'model_with_double_association_again'
191   - end
192   - model {}
193   - end
194   -
195   - with_model :model_with_double_association do
196   - table do |t|
197   - t.string 'title'
198   - end
199   -
200   - model do
201   - include PgSearch
202   - has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_id'
203   - has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_again_id'
204   -
205   - pg_search_scope :with_associated, :against => :title,
206   - :associated_against => {:things => :title, :thingamabobs => :title}
207   - end
208   - end
209   -
210   - it "returns rows that match the query in either its own columns or the columns of the associated model" do
211   - included = [
212   - ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
213   - DoublyAssociatedModel.create!(:title => 'foo'),
214   - DoublyAssociatedModel.create!(:title => 'bar')
215   - ]),
216   - ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
217   - DoublyAssociatedModel.create!(:title => 'foo bar'),
218   - DoublyAssociatedModel.create!(:title => 'mnopqr')
219   - ]),
220   - ModelWithDoubleAssociation.create!(:title => 'foo bar'),
221   - ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
222   - DoublyAssociatedModel.create!(:title => "foo bar")
223   - ])
224   - ]
225   - excluded = [
226   - ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
227   - DoublyAssociatedModel.create!(:title => 'abcdef')
228   - ]),
229   - ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
230   - DoublyAssociatedModel.create!(:title => "uiop")
231   - ])
232   - ]
233   -
234   - results = ModelWithDoubleAssociation.with_associated('foo bar')
235   - results.map(&:title).should =~ included.map(&:title)
236   - excluded.each { |object| results.should_not include(object) }
237   - end
238   - end
239   - end
240   -
241   - context "against multiple attributes on one association" do
242   - with_model :AssociatedModel do
243   - table do |t|
244   - t.string 'title'
245   - t.text 'author'
246   - end
247   - end
248   -
249   - with_model :ModelWithAssociation 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
294   - end
295   - else
296   - context "without Arel support" do
297   - with_model :Model do
298   - table do |t|
299   - t.string 'title'
300   - end
301   -
302   - model do
303   - include PgSearch
304   - pg_search_scope :with_joins, :against => :title, :joins => :another_model
305   - end
306   - end
307   -
308   - it "should raise an error" do
309   - lambda {
310   - Model.with_joins('foo')
311   - }.should raise_error(ArgumentError, /joins/)
312   - end
313   - end
314   - end
315   - end
316   -end
plugins/pg_search/lib/pg_search/spec/pg_search_spec.rb
... ... @@ -1,648 +0,0 @@
1   -require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2   -
3   -describe "an ActiveRecord model which includes PgSearch" do
4   -
5   - with_model :ModelWithPgSearch 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   - ModelWithPgSearch.class_eval do
20   - pg_search_scope "matching_query", :against => []
21   - end
22   -
23   - lambda {
24   - ModelWithPgSearch.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   - ModelWithPgSearch.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 = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')
40   - excluded = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
41   -
42   - ModelWithPgSearch.search_title_or_content('fo-remove-o', false).should == [included]
43   - ModelWithPgSearch.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   - ModelWithPgSearch.class_eval do
51   - pg_search_scope :with_unknown_option, :against => :content, :foo => :bar
52   - end
53   - ModelWithPgSearch.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   - ModelWithPgSearch.class_eval do
61   - pg_search_scope :with_unknown_option, lambda { |*| {:against => :content, :foo => :bar} }
62   - end
63   - ModelWithPgSearch.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   - ModelWithPgSearch.class_eval do
73   - pg_search_scope :with_unknown_using, :against => :content, :using => :foo
74   - end
75   - ModelWithPgSearch.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   - ModelWithPgSearch.class_eval do
83   - pg_search_scope :with_unknown_using, lambda { |*| {:against => :content, :using => :foo} }
84   - end
85   - ModelWithPgSearch.with_unknown_using("foo")
86   - }.should raise_error(ArgumentError, /foo/)
87   - end
88   - end
89   - end
90   -
91   - context "when an unknown :ignoring is passed" do
92   - it "raises an exception when invoked" do
93   - lambda {
94   - ModelWithPgSearch.class_eval do
95   - pg_search_scope :with_unknown_ignoring, :against => :content, :ignoring => :foo
96   - end
97   - ModelWithPgSearch.with_unknown_ignoring("foo")
98   - }.should raise_error(ArgumentError, /ignoring.*foo/)
99   - end
100   -
101   - context "dynamically" do
102   - it "raises an exception when invoked" do
103   - lambda {
104   - ModelWithPgSearch.class_eval do
105   - pg_search_scope :with_unknown_ignoring, lambda { |*| {:against => :content, :ignoring => :foo} }
106   - end
107   - ModelWithPgSearch.with_unknown_ignoring("foo")
108   - }.should raise_error(ArgumentError, /ignoring.*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   - ModelWithPgSearch.class_eval do
116   - pg_search_scope :with_unknown_ignoring, {}
117   - end
118   - ModelWithPgSearch.with_unknown_ignoring("foo")
119   - }.should raise_error(ArgumentError, /against/)
120   - end
121   - context "dynamically" do
122   - it "raises an exception when invoked" do
123   - lambda {
124   - ModelWithPgSearch.class_eval do
125   - pg_search_scope :with_unknown_ignoring, lambda { |*| {} }
126   - end
127   - ModelWithPgSearch.with_unknown_ignoring("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   - ModelWithPgSearch.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   - ModelWithPgSearch.create!(:content => 'foo')
145   -
146   - results = ModelWithPgSearch.search_content('')
147   - results.should == []
148   - end
149   -
150   - it "returns rows where the column contains the term in the query" do
151   - included = ModelWithPgSearch.create!(:content => 'foo')
152   - excluded = ModelWithPgSearch.create!(:content => 'bar')
153   -
154   - results = ModelWithPgSearch.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 = [ModelWithPgSearch.create!(:content => 'foo bar'),
161   - ModelWithPgSearch.create!(:content => 'bar foo')]
162   - excluded = ModelWithPgSearch.create!(:content => 'foo')
163   -
164   - results = ModelWithPgSearch.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 = [ModelWithPgSearch.create!(:content => "foo"),
174   - ModelWithPgSearch.create!(:content => "FOO")]
175   -
176   - results = ModelWithPgSearch.search_content("Foo")
177   - results.should =~ included
178   - end
179   -
180   - it "returns rows that match the query only if their accents match" do
181   - # \303\241 is a with acute accent
182   - # \303\251 is e with acute accent
183   -
184   - included = ModelWithPgSearch.create!(:content => "abcd\303\251f")
185   - excluded = ModelWithPgSearch.create!(:content => "\303\241bcdef")
186   -
187   - results = ModelWithPgSearch.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 = ModelWithPgSearch.create!(:content => 'pre')
194   - excluded = ModelWithPgSearch.create!(:content => 'prefix')
195   -
196   - results = ModelWithPgSearch.search_content("pre")
197   - results.should == [included]
198   - results.should_not include(excluded)
199   - end
200   -
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 = ModelWithPgSearch.create!(:content => "jumped")
203   - excluded = [ModelWithPgSearch.create!(:content => "jump"),
204   - ModelWithPgSearch.create!(:content => "jumping")]
205   -
206   - results = ModelWithPgSearch.search_content("jumped")
207   - results.should == [included]
208   - end
209   -
210   - it "returns rows that match sorted by rank" do
211   - loser = ModelWithPgSearch.create!(:content => 'foo')
212   - winner = ModelWithPgSearch.create!(:content => 'foo foo')
213   -
214   - results = ModelWithPgSearch.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 = [ModelWithPgSearch.create!(:content => 'foo'),
221   - ModelWithPgSearch.create!(:content => 'foo')].sort_by(&:id)
222   -
223   - results = ModelWithPgSearch.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   - ModelWithPgSearch.create!(:content => 'foo bar'),
230   - ModelWithPgSearch.create!(:content => 'bar foo'),
231   - ModelWithPgSearch.create!(:content => 'bar foo baz'),
232   - ]
233   - excluded = [
234   - ModelWithPgSearch.create!(:content => 'foo'),
235   - ModelWithPgSearch.create!(:content => 'foo baz')
236   - ]
237   -
238   - results = ModelWithPgSearch.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 = ModelWithPgSearch.create!(:content => "(:Foo.) Bar?, \\")
245   -
246   - results = ModelWithPgSearch.search_content("foo :bar .,?() \\")
247   - results.should == [included]
248   - end
249   -
250   - it "accepts non-string queries and calls #to_s on them" do
251   - foo = ModelWithPgSearch.create!(:content => "foo")
252   - not_a_string = stub(:to_s => "foo")
253   - ModelWithPgSearch.search_content(not_a_string).should == [foo]
254   - end
255   - end
256   -
257   - context "against multiple columns" do
258   - before do
259   - ModelWithPgSearch.class_eval do
260   - pg_search_scope :search_title_and_content, :against => [:title, :content]
261   - end
262   - end
263   -
264   - it "returns rows whose columns contain all of the terms in the query across columns" do
265   - included = [
266   - ModelWithPgSearch.create!(:title => 'foo', :content => 'bar'),
267   - ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
268   - ]
269   - excluded = [
270   - ModelWithPgSearch.create!(:title => 'foo', :content => 'foo'),
271   - ModelWithPgSearch.create!(:title => 'bar', :content => 'bar')
272   - ]
273   -
274   - results = ModelWithPgSearch.search_title_and_content('foo bar')
275   -
276   - results.should =~ included
277   - excluded.each do |result|
278   - results.should_not include(result)
279   - end
280   - end
281   -
282   - it "returns rows where at one column contains all of the terms in the query and another does not" do
283   - in_title = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')
284   - in_content = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
285   -
286   - results = ModelWithPgSearch.search_title_and_content('foo')
287   - results.should =~ [in_title, in_content]
288   - end
289   -
290   - # Searching with a NULL column will prevent any matches unless we coalesce it.
291   - it "returns rows where at one column contains all of the terms in the query and another is NULL" do
292   - included = ModelWithPgSearch.create!(:title => 'foo', :content => nil)
293   - results = ModelWithPgSearch.search_title_and_content('foo')
294   - results.should == [included]
295   - end
296   - end
297   -
298   - context "using trigram" do
299   - before do
300   - ModelWithPgSearch.class_eval do
301   - pg_search_scope :with_trigrams, :against => [:title, :content], :using => :trigram
302   - end
303   - end
304   -
305   - it "returns rows where one searchable column and the query share enough trigrams" do
306   - included = ModelWithPgSearch.create!(:title => 'abcdefghijkl', :content => nil)
307   - results = ModelWithPgSearch.with_trigrams('cdefhijkl')
308   - results.should == [included]
309   - end
310   -
311   - it "returns rows where multiple searchable columns and the query share enough trigrams" do
312   - included = ModelWithPgSearch.create!(:title => 'abcdef', :content => 'ghijkl')
313   - results = ModelWithPgSearch.with_trigrams('cdefhijkl')
314   - results.should == [included]
315   - end
316   - end
317   -
318   - context "using tsearch" do
319   - context "with :prefix => true" do
320   - before do
321   - ModelWithPgSearch.class_eval do
322   - pg_search_scope :search_title_with_prefixes,
323   - :against => :title,
324   - :using => {
325   - :tsearch => {:prefix => true}
326   - }
327   - end
328   - end
329   -
330   - it "returns rows that match the query and that are prefixed by the query" do
331   - included = ModelWithPgSearch.create!(:title => 'prefix')
332   - excluded = ModelWithPgSearch.create!(:title => 'postfix')
333   -
334   - results = ModelWithPgSearch.search_title_with_prefixes("pre")
335   - results.should == [included]
336   - results.should_not include(excluded)
337   - end
338   -
339   - it "returns rows that match the query when the query has a hyphen" do
340   - included = [
341   - ModelWithPgSearch.create!(:title => 'foo bar'),
342   - ModelWithPgSearch.create!(:title => 'foo-bar')
343   - ]
344   - excluded = ModelWithPgSearch.create!(:title => 'baz quux')
345   -
346   - results = ModelWithPgSearch.search_title_with_prefixes("foo-bar")
347   - results.should =~ included
348   - results.should_not include(excluded)
349   - end
350   - end
351   -
352   - context "with the english dictionary" do
353   - before do
354   - ModelWithPgSearch.class_eval do
355   - pg_search_scope :search_content_with_english,
356   - :against => :content,
357   - :using => {
358   - :tsearch => {:dictionary => :english}
359   - }
360   - end
361   - end
362   -
363   - it "returns rows that match the query when stemmed by the english dictionary" do
364   - included = [ModelWithPgSearch.create!(:content => "jump"),
365   - ModelWithPgSearch.create!(:content => "jumped"),
366   - ModelWithPgSearch.create!(:content => "jumping")]
367   -
368   - results = ModelWithPgSearch.search_content_with_english("jump")
369   - results.should =~ included
370   - end
371   - end
372   -
373   - context "against columns ranked with arrays" do
374   - before do
375   - ModelWithPgSearch.class_eval do
376   - pg_search_scope :search_weighted_by_array_of_arrays, :against => [[:content, 'B'], [:title, 'A']]
377   - end
378   - end
379   -
380   - it "returns results sorted by weighted rank" do
381   - loser = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
382   - winner = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')
383   -
384   - results = ModelWithPgSearch.search_weighted_by_array_of_arrays('foo')
385   - results[0].rank.should > results[1].rank
386   - results.should == [winner, loser]
387   - end
388   - end
389   -
390   - context "against columns ranked with a hash" do
391   - before do
392   - ModelWithPgSearch.class_eval do
393   - pg_search_scope :search_weighted_by_hash, :against => {:content => 'B', :title => 'A'}
394   - end
395   - end
396   -
397   - it "returns results sorted by weighted rank" do
398   - loser = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
399   - winner = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')
400   -
401   - results = ModelWithPgSearch.search_weighted_by_hash('foo')
402   - results[0].rank.should > results[1].rank
403   - results.should == [winner, loser]
404   - end
405   - end
406   -
407   - context "against columns of which only some are ranked" do
408   - before do
409   - ModelWithPgSearch.class_eval do
410   - pg_search_scope :search_weighted, :against => [:content, [:title, 'A']]
411   - end
412   - end
413   -
414   - it "returns results sorted by weighted rank using an implied low rank for unranked columns" do
415   - loser = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
416   - winner = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')
417   -
418   - results = ModelWithPgSearch.search_weighted('foo')
419   - results[0].rank.should > results[1].rank
420   - results.should == [winner, loser]
421   - end
422   - end
423   - end
424   -
425   - context "using dmetaphone" do
426   - before do
427   - ModelWithPgSearch.class_eval do
428   - pg_search_scope :with_dmetaphones, :against => [:title, :content], :using => :dmetaphone
429   - end
430   - end
431   -
432   - it "returns rows where one searchable column and the query share enough dmetaphones" do
433   - included = ModelWithPgSearch.create!(:title => 'Geoff', :content => nil)
434   - excluded = ModelWithPgSearch.create!(:title => 'Bob', :content => nil)
435   - results = ModelWithPgSearch.with_dmetaphones('Jeff')
436   - results.should == [included]
437   - end
438   -
439   - it "returns rows where multiple searchable columns and the query share enough dmetaphones" do
440   - included = ModelWithPgSearch.create!(:title => 'Geoff', :content => 'George')
441   - excluded = ModelWithPgSearch.create!(:title => 'Bob', :content => 'Jones')
442   - results = ModelWithPgSearch.with_dmetaphones('Jeff Jorge')
443   - results.should == [included]
444   - end
445   -
446   - it "returns rows that match dmetaphones that are English stopwords" do
447   - included = ModelWithPgSearch.create!(:title => 'White', :content => nil)
448   - excluded = ModelWithPgSearch.create!(:title => 'Black', :content => nil)
449   - results = ModelWithPgSearch.with_dmetaphones('Wight')
450   - results.should == [included]
451   - end
452   -
453   - it "can handle terms that do not have a dmetaphone equivalent" do
454   - term_with_blank_metaphone = "w"
455   -
456   - included = ModelWithPgSearch.create!(:title => 'White', :content => nil)
457   - excluded = ModelWithPgSearch.create!(:title => 'Black', :content => nil)
458   -
459   - results = ModelWithPgSearch.with_dmetaphones('Wight W')
460   - results.should == [included]
461   - end
462   - end
463   -
464   - context "using multiple features" do
465   - before do
466   - ModelWithPgSearch.class_eval do
467   - pg_search_scope :with_tsearch,
468   - :against => :title,
469   - :using => [
470   - [:tsearch, {:prefix => true}]
471   - ]
472   -
473   - pg_search_scope :with_trigram, :against => :title, :using => :trigram
474   -
475   - pg_search_scope :with_tsearch_and_trigram_using_array,
476   - :against => :title,
477   - :using => [
478   - [:tsearch, {:prefix => true}],
479   - :trigram
480   - ]
481   -
482   - end
483   - end
484   -
485   - it "returns rows that match using any of the features" do
486   - record = ModelWithPgSearch.create!(:title => "tiling is grouty")
487   -
488   - # matches trigram only
489   - trigram_query = "ling is grouty"
490   - ModelWithPgSearch.with_trigram(trigram_query).should include(record)
491   - ModelWithPgSearch.with_tsearch(trigram_query).should_not include(record)
492   - ModelWithPgSearch.with_tsearch_and_trigram_using_array(trigram_query).should == [record]
493   -
494   - # matches tsearch only
495   - tsearch_query = "til"
496   - ModelWithPgSearch.with_tsearch(tsearch_query).should include(record)
497   - ModelWithPgSearch.with_trigram(tsearch_query).should_not include(record)
498   - ModelWithPgSearch.with_tsearch_and_trigram_using_array(tsearch_query).should == [record]
499   - end
500   -
501   - context "with feature-specific configuration" do
502   - before do
503   - @tsearch_config = tsearch_config = {:dictionary => 'english'}
504   - @trigram_config = trigram_config = {:foo => 'bar'}
505   -
506   - ModelWithPgSearch.class_eval do
507   - pg_search_scope :with_tsearch_and_trigram_using_hash,
508   - :against => :title,
509   - :using => {
510   - :tsearch => tsearch_config,
511   - :trigram => trigram_config
512   - }
513   - end
514   - end
515   -
516   - it "should pass the custom configuration down to the specified feature" do
517   - stub_feature = stub(:conditions => "1 = 1", :rank => "1.0")
518   - PgSearch::Features::TSearch.should_receive(:new).with(anything, @tsearch_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
519   - PgSearch::Features::Trigram.should_receive(:new).with(anything, @trigram_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
520   -
521   - ModelWithPgSearch.with_tsearch_and_trigram_using_hash("foo")
522   - end
523   - end
524   - end
525   -
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   - context "ignoring accents" do
569   - before do
570   - ModelWithPgSearch.class_eval do
571   - pg_search_scope :search_title_without_accents, :against => :title, :ignoring => :accents
572   - end
573   - end
574   -
575   - it "returns rows that match the query but not its accents" do
576   - # \303\241 is a with acute accent
577   - # \303\251 is e with acute accent
578   -
579   - included = ModelWithPgSearch.create!(:title => "\303\241bcdef")
580   -
581   - results = ModelWithPgSearch.search_title_without_accents("abcd\303\251f")
582   - results.should == [included]
583   - end
584   - end
585   -
586   - context "when passed a :ranked_by expression" do
587   - before do
588   - ModelWithPgSearch.class_eval do
589   - pg_search_scope :search_content_with_default_rank,
590   - :against => :content
591   - pg_search_scope :search_content_with_importance_as_rank,
592   - :against => :content,
593   - :ranked_by => "importance"
594   - pg_search_scope :search_content_with_importance_as_rank_multiplier,
595   - :against => :content,
596   - :ranked_by => ":tsearch * importance"
597   - end
598   - end
599   -
600   - it "should return records with a rank attribute equal to the :ranked_by expression" do
601   - ModelWithPgSearch.create!(:content => 'foo', :importance => 10)
602   - results = ModelWithPgSearch.search_content_with_importance_as_rank("foo")
603   - results.first.rank.should == 10
604   - end
605   -
606   - it "should substitute :tsearch with the tsearch rank expression in the :ranked_by expression" do
607   - ModelWithPgSearch.create!(:content => 'foo', :importance => 10)
608   -
609   - tsearch_rank = ModelWithPgSearch.search_content_with_default_rank("foo").first.rank
610   - multiplied_rank = ModelWithPgSearch.search_content_with_importance_as_rank_multiplier("foo").first.rank
611   -
612   - multiplied_rank.should be_within(0.001).of(tsearch_rank * 10)
613   - end
614   -
615   - it "should return results in descending order of the value of the rank expression" do
616   - records = [
617   - ModelWithPgSearch.create!(:content => 'foo', :importance => 1),
618   - ModelWithPgSearch.create!(:content => 'foo', :importance => 3),
619   - ModelWithPgSearch.create!(:content => 'foo', :importance => 2)
620   - ]
621   -
622   - results = ModelWithPgSearch.search_content_with_importance_as_rank("foo")
623   - results.should == records.sort_by(&:importance).reverse
624   - end
625   -
626   - %w[tsearch trigram dmetaphone].each do |feature|
627   -
628   - context "using the #{feature} ranking algorithm" do
629   - before do
630   - @scope_name = scope_name = :"search_content_ranked_by_#{feature}"
631   - ModelWithPgSearch.class_eval do
632   - pg_search_scope scope_name,
633   - :against => :content,
634   - :ranked_by => ":#{feature}"
635   - end
636   - end
637   -
638   - it "should return results with a rank" do
639   - ModelWithPgSearch.create!(:content => 'foo')
640   -
641   - results = ModelWithPgSearch.send(@scope_name, 'foo')
642   - results.first.rank.should_not be_nil
643   - end
644   - end
645   - end
646   - end
647   - end
648   -end
plugins/pg_search/lib/pg_search/spec/spec_helper.rb
... ... @@ -1,92 +0,0 @@
1   -require "bundler/setup"
2   -require "pg_search"
3   -
4   -begin
5   - ActiveRecord::Base.establish_connection(:adapter => 'postgresql',
6   - :database => 'pg_search_test',
7   - :min_messages => 'warning')
8   - connection = ActiveRecord::Base.connection
9   - connection.execute("SELECT 1")
10   -rescue PGError => e
11   - puts "-" * 80
12   - puts "Unable to connect to database. Please run:"
13   - puts
14   - puts " createdb pg_search_test"
15   - puts "-" * 80
16   - raise e
17   -end
18   -
19   -def install_contrib_module_if_missing(name, query, expected_result)
20   - connection = ActiveRecord::Base.connection
21   - result = connection.select_value(query)
22   - raise "Unexpected output for #{query}: #{result.inspect}" unless result.downcase == expected_result.downcase
23   -rescue => e
24   - begin
25   - share_path = `pg_config --sharedir`.strip
26   - ActiveRecord::Base.connection.execute File.read(File.join(share_path, 'contrib', "#{name}.sql"))
27   - puts $!.message
28   - rescue
29   - puts "-" * 80
30   - puts "Please install the #{name} contrib module"
31   - puts "-" * 80
32   - raise e
33   - end
34   -end
35   -
36   -install_contrib_module_if_missing("pg_trgm", "SELECT 'abcdef' % 'cdef'", "t")
37   -install_contrib_module_if_missing("unaccent", "SELECT unaccent('foo')", "foo")
38   -install_contrib_module_if_missing("fuzzystrmatch", "SELECT dmetaphone('foo')", "f")
39   -
40   -ActiveRecord::Base.connection.execute(File.read(File.join(File.dirname(__FILE__), '..', 'sql', 'dmetaphone.sql')))
41   -
42   -require "with_model"
43   -
44   -RSpec.configure do |config|
45   - config.extend WithModel
46   -end
47   -
48   -if defined?(ActiveRecord::Relation)
49   - RSpec::Matchers::OperatorMatcher.register(ActiveRecord::Relation, '=~', RSpec::Matchers::MatchArray)
50   -end
51   -
52   -require 'irb'
53   -
54   -class IRB::Irb
55   - alias initialize_orig initialize
56   - def initialize(workspace = nil, *args)
57   - default = IRB.conf[:DEFAULT_OBJECT]
58   - workspace ||= IRB::WorkSpace.new default if default
59   - initialize_orig(workspace, *args)
60   - end
61   -end
62   -
63   -# Drop into an IRB session for whatever object you pass in:
64   -#
65   -# class Dude
66   -# def abides
67   -# true
68   -# end
69   -# end
70   -#
71   -# console_for(Dude.new)
72   -#
73   -# Then type "quit" or "exit" to get out. In a step definition, it should look like:
74   -#
75   -# When /^I console/ do
76   -# console_for(self)
77   -# end
78   -#
79   -# Also, I definitely stole this hack from some mailing list post somewhere. I wish I
80   -# could remember who did it, but I can't. Sorry!
81   -def console_for(target)
82   - puts "== ENTERING CONSOLE MODE. ==\nType 'exit' to move on.\nContext: #{target.inspect}"
83   -
84   - begin
85   - oldargs = ARGV.dup
86   - ARGV.clear
87   - IRB.conf[:DEFAULT_OBJECT] = target
88   - IRB.start
89   - ensure
90   - ARGV.replace(oldargs)
91   - end
92   -end
plugins/pg_search/lib/pg_search/sql/dmetaphone.sql
... ... @@ -1,4 +0,0 @@
1   -CREATE OR REPLACE FUNCTION pg_search_dmetaphone(text) RETURNS text LANGUAGE SQL IMMUTABLE STRICT AS $function$
2   - SELECT array_to_string(ARRAY(SELECT dmetaphone(unnest(regexp_split_to_array($1, E'\\\\s+')))), ' ')
3   -$function$;
4   -
plugins/pg_search/lib/pg_search/sql/uninstall_dmetaphone.sql
... ... @@ -1 +0,0 @@
1   -DROP FUNCTION pg_search_dmetaphone(text);
plugins/pg_search/lib/pg_search_plugin.rb
1   -lib_path = File.join(File.dirname(__FILE__), 'pg_search', 'lib')
2   -ActiveSupport::Dependencies.load_paths << lib_path
3   -$: << lib_path
4   -require 'pg_search'
  1 +require 'ext/active_record'
5 2  
6 3 class PgSearchPlugin < Noosfero::Plugin
7 4  
... ... @@ -14,18 +11,7 @@ class PgSearchPlugin &lt; Noosfero::Plugin
14 11 end
15 12  
16 13 def find_by_contents(asset, scope, query, paginate_options={}, options={})
17   - scope.pg_search_plugin_search(query)
  14 + {:results => scope.pg_search_plugin_search(query).paginate(paginate_options)}
18 15 end
19 16  
20 17 end
21   -
22   -searchables = %w[ article comment qualifier national_region certifier profile license scrap category ]
23   -searchables.each { |searchable| require_dependency searchable }
24   -klasses = searchables.map {|searchable| searchable.camelize.constantize }
25   -
26   -klasses.each do |klass|
27   - klass.class_eval do
28   - include PgSearch
29   - pg_search_scope :pg_search_plugin_search, :against => klass::SEARCHABLE_FIELDS
30   - end
31   -end
... ...