pg_searchable 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +10 -0
- data/lib/pg_searchable.rb +10 -0
- data/lib/pg_searchable/active_record.rb +3 -0
- data/lib/pg_searchable/active_record/extensions.rb +43 -0
- data/lib/pg_searchable/active_record/migration.rb +98 -0
- data/lib/pg_searchable/active_record/relation.rb +38 -0
- data/lib/pg_searchable/arel.rb +4 -0
- data/lib/pg_searchable/arel/nodes.rb +6 -0
- data/lib/pg_searchable/arel/nodes/dmetaphone.rb +20 -0
- data/lib/pg_searchable/arel/nodes/pg_searchable_dmetaphone.rb +11 -0
- data/lib/pg_searchable/arel/nodes/tgrm.rb +11 -0
- data/lib/pg_searchable/arel/nodes/to_tsquery.rb +11 -0
- data/lib/pg_searchable/arel/nodes/to_tsvector.rb +11 -0
- data/lib/pg_searchable/arel/nodes/tsearch.rb +20 -0
- data/lib/pg_searchable/arel/predications.rb +17 -0
- data/lib/pg_searchable/arel/visitors.rb +8 -0
- data/lib/pg_searchable/migrations/dmetaphone_generator.rb +15 -0
- data/lib/pg_searchable/migrations/templates/add_pg_searchable_dmetaphone_support_function.rb.erb +21 -0
- data/lib/pg_searchable/railtie.rb +7 -0
- data/lib/pg_searchable/version.rb +3 -0
- data/pg_searchable.gemspec +27 -0
- data/test/active_record/migration_test.rb +15 -0
- data/test/arel/predication_test.rb +34 -0
- data/test/support/fake_record.rb +121 -0
- data/test/test_helper.rb +13 -0
- data/test/version_test.rb +7 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d0244ce37cfcef0e4b9391ef9751d01a146660bf
|
4
|
+
data.tar.gz: 235f975076ef6ba068f78f6135d792c78e6e882b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9293c648bdd76a2b81fe062ebfa787b72c03a74abe2a24ea3af433fdef62dca73fd2dfc7e2ffe49e1d0e679a2429fa9582e44c874d487ec8dc3360e1d7c77685
|
7
|
+
data.tar.gz: a7093702d6aa93692140325374e1f5a5f209baeabaf4d7fbd7d0c2b54c57eb1c321b41676b14a7a3bd3abf3958f29dedba40a1b7ec10cd218abe4b3eae316b69
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Stephen St. Martin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# PgSearchable
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'pg_searchable'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install pg_searchable
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'pg_searchable/version'
|
2
|
+
require 'pg_searchable/active_record'
|
3
|
+
require 'pg_searchable/arel'
|
4
|
+
require "pg_searchable/railtie" if defined?(Rails)
|
5
|
+
|
6
|
+
# TODO:
|
7
|
+
# 1) add ranking functions for full text search
|
8
|
+
# 2) write tests for migration helpers
|
9
|
+
# 3) add support for postgis?
|
10
|
+
# 4) add Relation methods
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module PgSearchable
|
5
|
+
module ActiveRecord
|
6
|
+
module Extensions
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
DEFAULT_WEIGHTS = [0.1, 0.2, 0.4, 1.0]
|
10
|
+
DEFAULT_DICTIONARY = 'english'
|
11
|
+
DEFAULT_OPTIONS = { weights: DEFAULT_WEIGHTS, dictionary: DEFAULT_DICTIONARY }
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def pg_searchable (name, options = {})
|
15
|
+
pg_searchable_configs[name.to_sym] = {
|
16
|
+
tgrm: _pg_searchable_options,
|
17
|
+
dmetaphone: _pg_searchable_options,
|
18
|
+
tsearch: _pg_searchable_options
|
19
|
+
}.deep_merge(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def pg_searchable_configs
|
23
|
+
@pg_searchable_configs ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def search_for(term, options = {})
|
27
|
+
scoped.search_for(term, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def _pg_searchable_options
|
32
|
+
@_pg_searchable_options ||= DEFAULT_OPTIONS.merge(columns: _pg_searchable_columns)
|
33
|
+
end
|
34
|
+
|
35
|
+
def _pg_searchable_columns
|
36
|
+
@_pg_searchable_columns ||= columns_hash.find_all {|k,v| [:string, :text].include?(v.type) }.map {|k,v| v.name }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ActiveRecord::Base.send(:include, PgSearchable::ActiveRecord::Extensions)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'active_record/migration'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Migration
|
5
|
+
def add_pg_searchable_tsearch_trigger(table_name, column_name, options = {})
|
6
|
+
options.reverse_merge({ dictionary: 'simple', columns: nil })
|
7
|
+
column_data = case options[:columns]
|
8
|
+
when Array then "to_tsvector('#{options['dictionary']}', #{options[:columns].map {|column_name| "coalesce(new.#{column_name}, '')" }.join(" || ' ' || ") });"
|
9
|
+
when Hash then "#{options[:columns].map {|column_name, weight| "setweight(to_tsvector('#{options[:dictionary]}', coalesce(new.#{column_name}, '')), '#{weight}')" }.join(" || ")};"
|
10
|
+
end
|
11
|
+
|
12
|
+
_add_pg_searchable_trigger(table_name, column_name, :tsearch, column_data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove_pg_searchable_tsearch_trigger(table_name, column_name)
|
16
|
+
_remove_pg_searchable_trigger(table_name, column_name, :tsearch)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_pg_searchable_dmetaphone_trigger(table_name, column_name, options = {})
|
20
|
+
options.reverse_merge({ dictionary: 'simple', columns: nil })
|
21
|
+
column_data = case options[:columns]
|
22
|
+
when Array then "to_tsvector('#{options['dictionary']}', #{options[:columns].map {|column_name| "pg_searchable_dmetaphone(coalesce(new.#{column_name}, ''))" }.join(" || ' ' || ") });"
|
23
|
+
when Hash then "#{options[:columns].map {|column_name, weight| "setweight(to_tsvector('#{options[:dictionary]}', pg_searchable_dmetaphone(coalesce(new.#{column_name}, ''))), '#{weight}')" }.join(" || ")};"
|
24
|
+
end
|
25
|
+
|
26
|
+
_add_pg_searchable_trigger(table_name, column_name, :dmetaphone, column_data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_pg_searchable_dmetaphone_trigger(table_name, column_name)
|
30
|
+
_remove_pg_searchable_trigger(table_name, column_name, :dmetaphone)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_pg_searchable_dictionary(dictionary, options = {})
|
34
|
+
remove_pg_searchable_dictionary(dictionary)
|
35
|
+
|
36
|
+
options = {
|
37
|
+
catalog: 'english',
|
38
|
+
template: 'ispell',
|
39
|
+
dict_file: 'english',
|
40
|
+
aff_file: 'english',
|
41
|
+
stop_words: 'english',
|
42
|
+
mappings: 'asciiword, asciihword, hword_asciipart, word, hword, hword_part'
|
43
|
+
}.merge(options)
|
44
|
+
|
45
|
+
execute <<-EOS
|
46
|
+
CREATE TEXT SEARCH CONFIGURATION #{dictionary} ( COPY = pg_catalog.#{options[:catalog]} );
|
47
|
+
CREATE TEXT SEARCH DICTIONARY #{dictionary}_dict (
|
48
|
+
TEMPLATE = #{options[:template]},
|
49
|
+
DictFile = #{options[:dict_file]},
|
50
|
+
AffFile = #{options[:aff_file]},
|
51
|
+
StopWords = #{options[:stop_words]}
|
52
|
+
);
|
53
|
+
ALTER TEXT SEARCH CONFIGURATION #{dictionary}
|
54
|
+
ALTER MAPPING FOR #{options[:mappings]}
|
55
|
+
WITH unaccent, #{dictionary}_dict, english_stem;
|
56
|
+
|
57
|
+
EOS
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_pg_searchable_dictionary(dictionary)
|
61
|
+
execute <<-SQL
|
62
|
+
DROP TEXT SEARCH CONFIGURATION IF EXISTS #{dictionary};
|
63
|
+
DROP TEXT SEARCH DICTIONARY IF EXISTS #{dictionary}_dict;
|
64
|
+
SQL
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def _add_pg_searchable_trigger(table_name, column_name, trigger_type = 'tsearch', column_data = '')
|
70
|
+
trigger_name = "#{table_name}_#{column_name}_#{trigger_type}"
|
71
|
+
|
72
|
+
# be sure to remove trigger / function before recreating it
|
73
|
+
_remove_pg_searchable_trigger(table_name, column_name, trigger_type)
|
74
|
+
|
75
|
+
execute <<-SQL
|
76
|
+
CREATE OR REPLACE FUNCTION #{trigger_name}_update() RETURNS trigger AS $$
|
77
|
+
begin
|
78
|
+
new.#{column_name} := #{column_data}
|
79
|
+
return new;
|
80
|
+
end
|
81
|
+
$$ LANGUAGE plpgsql;
|
82
|
+
|
83
|
+
CREATE TRIGGER #{trigger_name} BEFORE INSERT OR UPDATE
|
84
|
+
ON #{table_name}
|
85
|
+
FOR EACH ROW EXECUTE PROCEDURE #{trigger_name}_update();
|
86
|
+
SQL
|
87
|
+
end
|
88
|
+
|
89
|
+
def _remove_pg_searchable_trigger(table_name, column_name, trigger_type = 'tsearch')
|
90
|
+
trigger_name = "#{table_name}_#{column_name}_#{trigger_type}"
|
91
|
+
|
92
|
+
execute <<-SQL
|
93
|
+
DROP TRIGGER IF EXISTS #{trigger_name} ON #{table_name};
|
94
|
+
DROP FUNCTION IF EXISTS #{trigger_name}_update();
|
95
|
+
SQL
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_record/relation'
|
2
|
+
|
3
|
+
module PgSearchable
|
4
|
+
module ActiveRecord
|
5
|
+
module Relation
|
6
|
+
def search_for(term, options = {})
|
7
|
+
using = Array(options.delete(:using) || :default)
|
8
|
+
models = Array(options[:in]).map {|association| klass.reflect_on_association(association).klass }
|
9
|
+
conditions = [self.klass, models].flatten.inject([]) do |conditions, klass|
|
10
|
+
name = using.find {|name| klass.pg_searchable_configs.key?(name.to_sym) }
|
11
|
+
config = klass.pg_searchable_configs[name]
|
12
|
+
|
13
|
+
Array(config[:tgrm][:columns]).each {|name| conditions << klass.arel_table[name].tgrm(term).to_sql }
|
14
|
+
Array(config[:tsearch][:columns]).each {|name| conditions << klass.arel_table[name].tsearch(term, config[:tsearch][:dictionary]).to_sql }
|
15
|
+
Array(config[:dmetaphone][:columns]).each {|name| conditions << klass.arel_table[name].dmetaphone(term, config[:dmetaphone][:dictionary]).to_sql }
|
16
|
+
conditions
|
17
|
+
end
|
18
|
+
|
19
|
+
p conditions
|
20
|
+
|
21
|
+
joins(options[:in]).where("#{conditions.join(' OR ')}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def near(latitude, longitude)
|
25
|
+
puts "searching near #{latitude},#{longitude}"
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def rank_by(rank)
|
30
|
+
# TODO: add ranks to projections
|
31
|
+
puts "ranking by #{rank}"
|
32
|
+
self
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
ActiveRecord::Relation.send(:include, PgSearchable::ActiveRecord::Relation)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require 'pg_searchable/arel/nodes/dmetaphone'
|
2
|
+
require 'pg_searchable/arel/nodes/pg_searchable_dmetaphone'
|
3
|
+
require 'pg_searchable/arel/nodes/tgrm'
|
4
|
+
require 'pg_searchable/arel/nodes/tsearch'
|
5
|
+
require 'pg_searchable/arel/nodes/to_tsquery'
|
6
|
+
require 'pg_searchable/arel/nodes/to_tsvector'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'arel/nodes/infix_operation'
|
2
|
+
|
3
|
+
module Arel
|
4
|
+
module Nodes
|
5
|
+
class Dmetaphone < Arel::Nodes::InfixOperation
|
6
|
+
def initialize(attribute, query, dictionary)
|
7
|
+
relation = attribute.relation
|
8
|
+
columns = relation.engine.connection.columns(relation.name)
|
9
|
+
left = case columns.find {|c| c.name == attribute.name.to_s }.type
|
10
|
+
when :tsvector
|
11
|
+
attribute
|
12
|
+
else
|
13
|
+
Arel::Nodes::ToTsvector.new(attribute, dictionary)
|
14
|
+
end
|
15
|
+
|
16
|
+
super(:'@@', left, Arel::Nodes::ToTsquery.new(Arel::Nodes::PgSearchableDmetaphone.new(query), dictionary))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'arel/nodes/infix_operation'
|
2
|
+
|
3
|
+
module Arel
|
4
|
+
module Nodes
|
5
|
+
class Tsearch < Arel::Nodes::InfixOperation
|
6
|
+
def initialize(attribute, query, dictionary)
|
7
|
+
relation = attribute.relation
|
8
|
+
columns = relation.engine.connection.columns(relation.name)
|
9
|
+
left = case columns.find {|c| c.name == attribute.name.to_s }.type
|
10
|
+
when :tsvector
|
11
|
+
attribute
|
12
|
+
else
|
13
|
+
Arel::Nodes::ToTsvector.new(attribute, dictionary)
|
14
|
+
end
|
15
|
+
|
16
|
+
super(:'@@', left, Arel::Nodes::ToTsquery.new(query, dictionary))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'arel/predications'
|
2
|
+
|
3
|
+
module Arel
|
4
|
+
module Predications
|
5
|
+
def tgrm(text)
|
6
|
+
Nodes::Tgrm.new(self, text)
|
7
|
+
end
|
8
|
+
|
9
|
+
def tsearch(text, dictionary = 'simple')
|
10
|
+
Nodes::Tsearch.new(self, text, dictionary)
|
11
|
+
end
|
12
|
+
|
13
|
+
def dmetaphone(text, dictionary = 'simple')
|
14
|
+
Nodes::Dmetaphone.new(self, text, dictionary)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module PgSearchable
|
4
|
+
module Migrations
|
5
|
+
class DmetaphoneGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
7
|
+
|
8
|
+
def create_migration
|
9
|
+
now = Time.now.utc
|
10
|
+
filename = "#{now.strftime('%Y%m%d%H%M%S')}_add_pg_searchable_dmetaphone_support_function.rb"
|
11
|
+
template 'add_pg_searchable_dmetaphone_support_function.rb.erb', "db/migrate/#{filename}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/pg_searchable/migrations/templates/add_pg_searchable_dmetaphone_support_function.rb.erb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class AddPgSearchableDmetaphoneSupportFunction < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
say_with_time("Adding support function for pg_searchable :dmetaphone") do
|
4
|
+
sql = <<-SQL
|
5
|
+
CREATE OR REPLACE FUNCTION pg_searchable_dmetaphone(text) RETURNS text LANGUAGE SQL IMMUTABLE STRICT AS $function$
|
6
|
+
SELECT array_to_string(ARRAY(SELECT dmetaphone(unnest(regexp_split_to_array($1, E'\\s+')))), ' ')
|
7
|
+
$function$;
|
8
|
+
SQL
|
9
|
+
|
10
|
+
execute sql.strip
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
say_with_time("Dropping support function for pg_searchable :dmetaphone") do
|
16
|
+
execute <<-SQL
|
17
|
+
DROP FUNCTION IF EXISTS pg_searchable_dmetaphone(text)
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pg_searchable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pg_searchable"
|
8
|
+
spec.version = PgSearchable::VERSION
|
9
|
+
spec.authors = ["Stephen St. Martin"]
|
10
|
+
spec.email = ["steve@stevestmartin.com"]
|
11
|
+
spec.description = %q{Simple ActiveRecord PostgreSQL full text backed by Arel}
|
12
|
+
spec.summary = %q{Simple ActiveRecord PostgreSQL full text backed by Arel}
|
13
|
+
spec.homepage = "https://github.com/stevestmartin/pg_searchable"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "arel", ">= 3.0.0"
|
22
|
+
spec.add_dependency "activerecord", ">= 3.0.0"
|
23
|
+
spec.add_dependency "activesupport", ">= 3.0.0"
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "minitest", "~> 4.6"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
describe "migration helpers" do
|
4
|
+
describe "add tsearch trigger" do
|
5
|
+
end
|
6
|
+
|
7
|
+
describe "remove tsearch trigger" do
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "add dmetaphone trigger" do
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "remove dmetaphone trigger" do
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
describe "text search predications" do
|
4
|
+
describe "tgrm" do
|
5
|
+
it "should compare using tgrm operator" do
|
6
|
+
table = Arel::Table.new :articles
|
7
|
+
table[:title].tgrm("query").to_sql.must_be_like %{"articles"."title" % 'query'}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "tsearch" do
|
12
|
+
it "should compare column as tsvector when a string" do
|
13
|
+
table = Arel::Table.new :articles
|
14
|
+
table[:title].tsearch("query", "dictionary").to_sql.must_be_like %{to_tsvector('dictionary', "articles"."title") @@ to_tsquery('dictionary', 'query')}
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should compare column as is when a tsvector" do
|
18
|
+
table = Arel::Table.new :articles
|
19
|
+
table[:tsvector].tsearch("query", "dictionary").to_sql.must_be_like %{"articles"."tsvector" @@ to_tsquery('dictionary', 'query')}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "dmetaphone" do
|
24
|
+
it "should compare column as tsvector when a string" do
|
25
|
+
table = Arel::Table.new :articles
|
26
|
+
table[:title].dmetaphone("query", "dictionary").to_sql.must_be_like %{to_tsvector('dictionary', "articles"."title") @@ to_tsquery('dictionary', pg_searchable_dmetaphone('query'))}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should compare column as tsvector when a string" do
|
30
|
+
table = Arel::Table.new :articles
|
31
|
+
table[:tsvector].dmetaphone("query", "dictionary").to_sql.must_be_like %{"articles"."tsvector" @@ to_tsquery('dictionary', pg_searchable_dmetaphone('query'))}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module FakeRecord
|
2
|
+
class Column < Struct.new(:name, :type)
|
3
|
+
end
|
4
|
+
|
5
|
+
class Connection
|
6
|
+
attr_reader :columns_hash, :tables
|
7
|
+
attr_accessor :visitor
|
8
|
+
|
9
|
+
def initialize(visitor = nil)
|
10
|
+
@tables = %w{ articles categories comments users}
|
11
|
+
@columns = {
|
12
|
+
'articles' => [
|
13
|
+
Column.new('id', :integer),
|
14
|
+
Column.new('title', :string),
|
15
|
+
Column.new('body', :text),
|
16
|
+
Column.new('created_at', :date),
|
17
|
+
Column.new('tsvector', :tsvector)
|
18
|
+
],
|
19
|
+
'categories' => [
|
20
|
+
Column.new('id', :integer),
|
21
|
+
Column.new('name', :string),
|
22
|
+
Column.new('tsvector', :tsvector)
|
23
|
+
]
|
24
|
+
}
|
25
|
+
@columns_hash = {
|
26
|
+
'articles' => Hash[@columns['articles'].map { |x| [x.name, x] }],
|
27
|
+
'categories' => Hash[@columns['categories'].map { |x| [x.name, x] }]
|
28
|
+
}
|
29
|
+
@primary_keys = {
|
30
|
+
'articles' => 'id',
|
31
|
+
'categories' => 'id'
|
32
|
+
}
|
33
|
+
@visitor = visitor
|
34
|
+
end
|
35
|
+
|
36
|
+
def primary_key name
|
37
|
+
@primary_keys[name.to_s]
|
38
|
+
end
|
39
|
+
|
40
|
+
def table_exists? name
|
41
|
+
@tables.include? name.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def columns name, message = nil
|
45
|
+
@columns[name.to_s]
|
46
|
+
end
|
47
|
+
|
48
|
+
def quote_table_name name
|
49
|
+
"\"#{name.to_s}\""
|
50
|
+
end
|
51
|
+
|
52
|
+
def quote_column_name name
|
53
|
+
"\"#{name.to_s}\""
|
54
|
+
end
|
55
|
+
|
56
|
+
def schema_cache
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def quote thing, column = nil
|
61
|
+
if column && column.type == :integer
|
62
|
+
return 'NULL' if thing.nil?
|
63
|
+
return thing.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
case thing
|
67
|
+
when true
|
68
|
+
"'t'"
|
69
|
+
when false
|
70
|
+
"'f'"
|
71
|
+
when nil
|
72
|
+
'NULL'
|
73
|
+
when Numeric
|
74
|
+
thing
|
75
|
+
else
|
76
|
+
"'#{thing}'"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class ConnectionPool
|
82
|
+
class Spec < Struct.new(:config)
|
83
|
+
end
|
84
|
+
|
85
|
+
attr_reader :spec, :connection
|
86
|
+
|
87
|
+
def initialize
|
88
|
+
@spec = Spec.new(:adapter => 'america')
|
89
|
+
@connection = Connection.new
|
90
|
+
@connection.visitor = Arel::Visitors::ToSql.new(connection)
|
91
|
+
end
|
92
|
+
|
93
|
+
def with_connection
|
94
|
+
yield connection
|
95
|
+
end
|
96
|
+
|
97
|
+
def table_exists? name
|
98
|
+
connection.tables.include? name.to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
def columns_hash
|
102
|
+
connection.columns_hash
|
103
|
+
end
|
104
|
+
|
105
|
+
def schema_cache
|
106
|
+
connection
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Base
|
111
|
+
attr_accessor :connection_pool
|
112
|
+
|
113
|
+
def initialize
|
114
|
+
@connection_pool = ConnectionPool.new
|
115
|
+
end
|
116
|
+
|
117
|
+
def connection
|
118
|
+
connection_pool.connection
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'minitest/pride'
|
4
|
+
require_relative '../lib/pg_searchable'
|
5
|
+
require_relative 'support/fake_record'
|
6
|
+
|
7
|
+
Arel::Table.engine = Arel::Sql::Engine.new(FakeRecord::Base.new)
|
8
|
+
|
9
|
+
class Object
|
10
|
+
def must_be_like other
|
11
|
+
gsub(/\s+/, ' ').strip.must_equal other.gsub(/\s+/, ' ').strip
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_searchable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen St. Martin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: arel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Simple ActiveRecord PostgreSQL full text backed by Arel
|
98
|
+
email:
|
99
|
+
- steve@stevestmartin.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- .gitignore
|
105
|
+
- Gemfile
|
106
|
+
- LICENSE.txt
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- lib/pg_searchable.rb
|
110
|
+
- lib/pg_searchable/active_record.rb
|
111
|
+
- lib/pg_searchable/active_record/extensions.rb
|
112
|
+
- lib/pg_searchable/active_record/migration.rb
|
113
|
+
- lib/pg_searchable/active_record/relation.rb
|
114
|
+
- lib/pg_searchable/arel.rb
|
115
|
+
- lib/pg_searchable/arel/nodes.rb
|
116
|
+
- lib/pg_searchable/arel/nodes/dmetaphone.rb
|
117
|
+
- lib/pg_searchable/arel/nodes/pg_searchable_dmetaphone.rb
|
118
|
+
- lib/pg_searchable/arel/nodes/tgrm.rb
|
119
|
+
- lib/pg_searchable/arel/nodes/to_tsquery.rb
|
120
|
+
- lib/pg_searchable/arel/nodes/to_tsvector.rb
|
121
|
+
- lib/pg_searchable/arel/nodes/tsearch.rb
|
122
|
+
- lib/pg_searchable/arel/predications.rb
|
123
|
+
- lib/pg_searchable/arel/visitors.rb
|
124
|
+
- lib/pg_searchable/migrations/dmetaphone_generator.rb
|
125
|
+
- lib/pg_searchable/migrations/templates/add_pg_searchable_dmetaphone_support_function.rb.erb
|
126
|
+
- lib/pg_searchable/railtie.rb
|
127
|
+
- lib/pg_searchable/version.rb
|
128
|
+
- pg_searchable.gemspec
|
129
|
+
- test/active_record/migration_test.rb
|
130
|
+
- test/arel/predication_test.rb
|
131
|
+
- test/support/fake_record.rb
|
132
|
+
- test/test_helper.rb
|
133
|
+
- test/version_test.rb
|
134
|
+
homepage: https://github.com/stevestmartin/pg_searchable
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - '>='
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.0.3
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: Simple ActiveRecord PostgreSQL full text backed by Arel
|
158
|
+
test_files:
|
159
|
+
- test/active_record/migration_test.rb
|
160
|
+
- test/arel/predication_test.rb
|
161
|
+
- test/support/fake_record.rb
|
162
|
+
- test/test_helper.rb
|
163
|
+
- test/version_test.rb
|
164
|
+
has_rdoc:
|