pg_search 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,5 @@
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
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ .idea
5
+ tags
6
+ *~
7
+ gemfiles/**/Gemfile.lock
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree@pg_search
data/CHANGELOG ADDED
@@ -0,0 +1,7 @@
1
+ ### 0.0.2
2
+
3
+ Fix gem ownership.
4
+
5
+ ### 0.0.1
6
+
7
+ Initial release.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
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
data/LICENSE ADDED
@@ -0,0 +1,19 @@
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.
data/README.rdoc ADDED
@@ -0,0 +1,290 @@
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
+ == INSTALL:
10
+
11
+ gem install pg_search
12
+
13
+ === Rails 3
14
+
15
+ In Gemfile
16
+
17
+ gem 'pg_search'
18
+
19
+ === Rails 2
20
+
21
+ In environment.rb
22
+
23
+ config.gem 'pg_search'
24
+
25
+ In Rakefile
26
+
27
+ require 'rubygems'
28
+ require 'pg_search/tasks'
29
+
30
+ == USAGE
31
+
32
+ To add PgSearch to an ActiveRecord model, simply include the PgSearch module.
33
+
34
+ class Shape < ActiveRecord::Base
35
+ include PgSearch
36
+ end
37
+
38
+ === pg_search_scope
39
+
40
+ 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.
41
+
42
+ ==== Searching against one column
43
+
44
+ To search against a column, pass a symbol as the :against option.
45
+
46
+ class BlogPost < ActiveRecord::Base
47
+ include PgSearch
48
+ pg_search_scope :search_by_title, :against => :title
49
+ end
50
+
51
+ We now have an ActiveRecord scope named search_by_title on our BlogPost model. It takes one parameter, a search query string.
52
+
53
+ BlogPost.create!(:title => "Recent Developments in the World of Pastrami")
54
+ BlogPost.create!(:title => "Prosciutto and You: A Retrospective")
55
+ BlogPost.search_by_title("pastrami") # => [#<BlogPost id: 2, title: "Recent Developments in the World of Pastrami">]
56
+
57
+ ==== Searching against multiple columns
58
+
59
+ Just pass an Array if you'd like to search more than one column.
60
+
61
+ class Person < ActiveRecord::Base
62
+ include PgSearch
63
+ pg_search_scope :search_by_full_name, :against => [:first_name, :last_name]
64
+ end
65
+
66
+ Now our search query can match either or both of the columns.
67
+
68
+ person_1 = Person.create!(:first_name => "Grant", :last_name => "Hill")
69
+ person_2 = Person.create!(:first_name => "Hugh", :last_name => "Grant")
70
+
71
+ Person.search_by_full_name("Grant") # => [person_1, person_2]
72
+ Person.search_by_full_name("Grant Hill") # => [person_1]
73
+
74
+ ==== Dynamic search scopes
75
+
76
+ 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.
77
+
78
+ 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.
79
+
80
+ class Person < ActiveRecord::Base
81
+ include PgSearch
82
+ pg_search_scope :search_by_name, lambda do |name_part, query|
83
+ raise ArgumentError unless [:first, :last].include?(name_part)
84
+ {
85
+ :against => name_part,
86
+ :query => query
87
+ }
88
+ end
89
+ end
90
+
91
+ person_1 = Person.create!(:first_name => "Grant", :last_name => "Hill")
92
+ person_2 = Person.create!(:first_name => "Hugh", :last_name => "Grant")
93
+
94
+ Person.search_by_name :first, "Grant" # => [person_1]
95
+ Person.search_by_name :last, "Grant" # => [person_2]
96
+
97
+ ==== Searching through associations
98
+
99
+ 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.
100
+
101
+ class Cracker < ActiveRecord::Base
102
+ end
103
+
104
+ class Cheese < ActiveRecord::Base
105
+ has_many :cheeses
106
+ end
107
+
108
+ class Salami < ActiveRecord::Base
109
+ include PgSearch
110
+
111
+ belongs_to :cracker
112
+ has_many :cheeses, :through => :cracker
113
+
114
+ pg_search_scope :tasty_search, :associated_against => {
115
+ :cheeses => [:kind, :brand],
116
+ :cracker => :kind
117
+ }
118
+ end
119
+
120
+ salami_1 = Salami.create!
121
+ salami_2 = Salami.create!
122
+ salami_3 = Salami.create!
123
+
124
+ limburger = Cheese.create!(:kind => "Limburger")
125
+ brie = Cheese.create!(:kind => "Brie")
126
+ pepper_jack = Cheese.create!(:kind => "Pepper Jack")
127
+
128
+ Cracker.create!(:kind => "Black Pepper", :cheeses => [brie], :salami => salami_1)
129
+ Cracker.create!(:kind => "Ritz", :cheeses => [limburger, pepper_jack], :salami => salami_2)
130
+ Cracker.create!(:kind => "Graham", :cheeses => [limburger], :salami => salami_3)
131
+
132
+ Salami.tasty_search("pepper") # => [salami_1, salami_2]
133
+
134
+ === Searching using different search features
135
+
136
+ 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.
137
+
138
+ class Beer < ActiveRecord::Base
139
+ include PgSearch
140
+ pg_search_scope :against => :name, :features => [:tsearch, :trigram, :dmetaphone]
141
+ end
142
+
143
+ The currently implemented features are
144
+
145
+ * :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)
146
+ * :trigram - {Trigram search}[http://www.postgresql.org/docs/current/static/pgtrgm.html], which requires the trigram contrib package
147
+ * :dmetaphone - {Double Metaphone search}[http://www.postgresql.org/docs/9.0/static/fuzzystrmatch.html#AEN120188], which requires the fuzzystrmatch contrib package
148
+
149
+ ==== :tsearch (Full Text Search)
150
+
151
+ PostgreSQL's built-in full text search supports weighting, prefix searches, and stemming in multiple languages.
152
+
153
+ ===== Weighting
154
+ 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.
155
+
156
+ class NewsArticle < ActiveRecord::Base
157
+ include PgSearch
158
+ pg_search_scope :against => {
159
+ :title => 'A',
160
+ :subtitle => 'B',
161
+ :content => 'C'
162
+ }
163
+ end
164
+
165
+ 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.
166
+
167
+ class NewsArticle < ActiveRecord::Base
168
+ include PgSearch
169
+ pg_search_scope :against => [
170
+ [:title, 'A'],
171
+ [:subtitle, 'B'],
172
+ [:content, 'C']
173
+ ]
174
+ end
175
+
176
+ class NewsArticle < ActiveRecord::Base
177
+ include PgSearch
178
+ pg_search_scope :against => [
179
+ [:title, 'A'],
180
+ {:subtitle => 'B'},
181
+ :content
182
+ ]
183
+ end
184
+
185
+ ===== :prefix
186
+
187
+ 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.
188
+
189
+ class Superhero < ActiveRecord::Base
190
+ include PgSearch
191
+ pg_search_scope :whose_name_starts_with,
192
+ :against => :name,
193
+ :using => {
194
+ :tsearch => {:prefix => true}
195
+ }
196
+ end
197
+
198
+ batman = Superhero.create :name => 'Batman'
199
+ batgirl = Superhero.create :name => 'Batgirl'
200
+ robin = Superhero.create :name => 'Robin'
201
+
202
+ Superhero.whose_name_starts_with("Bat") # => [batman, batgirl]
203
+
204
+ ===== :dictionary
205
+
206
+ PostgreSQL full text search also support multiple dictionaries for stemming. The default dictionary depends on your PostgreSQL setup. You can learn more about how dictionaries work by reading the {PostgreSQL documention}[http://www.postgresql.org/docs/current/static/textsearch-dictionaries.html]. If you use one of the language dictionaries, such as "english", then variants of words (e.g. "jumping" and "jumped") will match each other. If you don't want stemming, you should pick the "simple" dictionary which does not do any stemming.
207
+
208
+ class BoringTweet < ActiveRecord::Base
209
+ include PgSearch
210
+ pg_search_scope :kinda_matching,
211
+ :against => :text,
212
+ :using => {
213
+ :tsearch => {:dictionary => "english"}
214
+ }
215
+ pg_search_scope :literally_matching,
216
+ :against => :text,
217
+ :using => {
218
+ :tsearch => {:dictionary => "simple"}
219
+ }
220
+ end
221
+
222
+ sleepy = BoringTweet.create! :text => "I snoozed my alarm for fourteen hours today. I bet I can beat that tomorrow! #sleepy"
223
+ sleeping = BoringTweet.create! :text => "You know what I like? Sleeping. That's what. #enjoyment"
224
+ sleeper = BoringTweet.create! :text => "Have you seen Woody Allen's movie entitled Sleeper? Me neither. #boycott"
225
+
226
+ BoringTweet.kinda_matching("sleeping") # => [sleepy, sleeping, sleeper]
227
+ BoringTweet.literally_matching("sleeping") # => [sleeping]
228
+
229
+ ==== :dmetaphone (Double Metaphone soundalike search)
230
+
231
+ {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.
232
+
233
+ 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:
234
+
235
+ include "pg_search/tasks"
236
+
237
+ and then run:
238
+
239
+ $ rake pg_search:migration:dmetaphone
240
+
241
+ The following example shows how to use :dmetaphone.
242
+
243
+ class Word < ActiveRecord::Base
244
+ include PgSearch
245
+ pg_search_scope :that_sounds_like,
246
+ :against => :spelling,
247
+ :using => :dmetaphone
248
+ end
249
+
250
+ four = Word.create! :spelling => 'four'
251
+ far = Word.create! :spelling => 'far'
252
+ fur = Word.create! :spelling => 'fur'
253
+ five = Word.create! :spelling => 'five'
254
+
255
+ Word.that_sounds_like("fir") # => [four, far, fur]
256
+
257
+ ==== :trigram (Trigram search)
258
+
259
+ 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:
260
+
261
+ [" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
262
+
263
+ Trigram search has some ability to work even with typos and misspellings in the query or text.
264
+
265
+ 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.
266
+
267
+
268
+ class Website < ActiveRecord::Base
269
+ include PgSearch
270
+ pg_search_scope :kinda_spelled_like,
271
+ :against => :name,
272
+ :using => :trigram
273
+ end
274
+
275
+ yahooo = Website.create! :name => "Yahooo!"
276
+ yohoo = Website.create! :name => "Yohoo!"
277
+ gogle = Website.create! :name => "Gogle"
278
+ facebook = Website.create! :name => "Facebook"
279
+
280
+ Website.kinda_spelled_like("Yahoo!") # => [yahooo, yohoo]
281
+
282
+ == REQUIREMENTS
283
+
284
+ * ActiveRecord 2 or 3
285
+ * Postgresql
286
+ * Postgresql contrib modules for certain features
287
+
288
+ == LICENSE:
289
+
290
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :default => :spec
5
+
6
+ environments = %w[rails2 rails3]
7
+
8
+ in_environment = lambda do |environment, command|
9
+ sh %Q{export BUNDLE_GEMFILE="gemfiles/#{environment}/Gemfile"; bundle --quiet update && bundle exec #{command}}
10
+ end
11
+
12
+ in_all_environments = lambda do |command|
13
+ environments.each do |environment|
14
+ puts "\n---#{environment}---\n"
15
+ in_environment.call(environment, command)
16
+ end
17
+ end
18
+
19
+ desc "Run all specs against ActiveRecord 2 and 3"
20
+ task "spec" do
21
+ in_all_environments.call('rspec spec')
22
+ end
23
+
24
+ task "doc" do
25
+ in_environment.call("rails3", "rspec --format d spec")
26
+ end
27
+
28
+ namespace "autotest" do
29
+ environments.each do |environment|
30
+ desc "Run autotest in #{environment}"
31
+ task environment do
32
+ in_environment.call(environment, 'autotest -s rspec2')
33
+ end
34
+ end
35
+ end
data/TODO ADDED
@@ -0,0 +1,12 @@
1
+ * Railtie for rake tasks
2
+ * README
3
+ * Tracker project
4
+ * Mailing list
5
+ * License
6
+ * Publish gem
7
+ * Exceptions for missing trigram, dmetaphone, etc. support
8
+ * LIKE search
9
+ * ability to specify multiple ranks: :ranked_by => [:tsearch, :trigram]
10
+ * ability to mix ranks together with weights: :ranked_by => {:tsearch => 1.0, :trigram => 0.5}
11
+ * accept a block and pass it to the underlying scope
12
+ * ability to search again a tsvector column
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "pg"
4
+
5
+ group :test do
6
+ gem "rspec"
7
+ gem "autotest"
8
+ gem "with_model", '0.1'
9
+ end
@@ -0,0 +1,4 @@
1
+ filename = File.join(File.dirname(__FILE__), '..', 'Gemfile.common')
2
+ eval(File.read(filename), binding, filename, 1)
3
+
4
+ gem "activerecord", "~>2.3.5"
@@ -0,0 +1,4 @@
1
+ filename = File.join(File.dirname(__FILE__), '..', 'Gemfile.common')
2
+ eval(File.read(filename), binding, filename, 1)
3
+
4
+ gem "activerecord", "~>3.0.1"
data/lib/pg_search.rb ADDED
@@ -0,0 +1,32 @@
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