texticle 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +23 -0
- data/CHANGELOG.rdoc +6 -0
- data/Manifest.txt +12 -0
- data/README.rdoc +78 -0
- data/Rakefile +16 -0
- data/lib/texticle.rb +76 -0
- data/lib/texticle/full_text_index.rb +48 -0
- data/lib/texticle/tasks.rb +28 -0
- data/rails/init.rb +3 -0
- data/test/helper.rb +43 -0
- data/test/test_full_text_index.rb +74 -0
- data/test/test_texticle.rb +47 -0
- metadata +83 -0
data/.autotest
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'autotest/restart'
|
4
|
+
|
5
|
+
# Autotest.add_hook :initialize do |at|
|
6
|
+
# at.extra_files << "../some/external/dependency.rb"
|
7
|
+
#
|
8
|
+
# at.libs << ":../some/external"
|
9
|
+
#
|
10
|
+
# at.add_exception 'vendor'
|
11
|
+
#
|
12
|
+
# at.add_mapping(/dependency.rb/) do |f, _|
|
13
|
+
# at.files_matching(/test_.*rb$/)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# %w(TestA TestB).each do |klass|
|
17
|
+
# at.extra_class_map[klass] = "test/test_misc.rb"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
|
21
|
+
# Autotest.add_hook :run_command do |at|
|
22
|
+
# system "rake build"
|
23
|
+
# end
|
data/CHANGELOG.rdoc
ADDED
data/Manifest.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
= texticle
|
2
|
+
|
3
|
+
* http://texticle.rubyforge.org/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Texticle exposes full text search capabilities from PostgreSQL, and allows
|
8
|
+
you to declare full text indexes. Texticle will extend ActiveRecord with
|
9
|
+
named_scope methods making searching easy and fun!
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
12
|
+
|
13
|
+
* Only works with PostgreSQL
|
14
|
+
|
15
|
+
== SYNOPSIS:
|
16
|
+
|
17
|
+
###
|
18
|
+
# In environment.rb
|
19
|
+
|
20
|
+
config.gem 'texticle'
|
21
|
+
|
22
|
+
###
|
23
|
+
# Declare your index in your model
|
24
|
+
|
25
|
+
class Product < ActiveRecord::Base
|
26
|
+
index do
|
27
|
+
name
|
28
|
+
description
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
###
|
33
|
+
# Use the search method
|
34
|
+
|
35
|
+
Product.search('hello world')
|
36
|
+
|
37
|
+
###
|
38
|
+
# Full text searches can be sped up by creating indexes. To create the
|
39
|
+
# indexes, add these lines to your Rakefile:
|
40
|
+
require 'rubygems'
|
41
|
+
require 'texticle/tasks'
|
42
|
+
|
43
|
+
# Then run:
|
44
|
+
|
45
|
+
$ rake texticle:create_indexes
|
46
|
+
|
47
|
+
== REQUIREMENTS:
|
48
|
+
|
49
|
+
* Texticle may be used outside rails, but works best with rails.
|
50
|
+
|
51
|
+
== INSTALL:
|
52
|
+
|
53
|
+
* sudo gem install texticle
|
54
|
+
|
55
|
+
== LICENSE:
|
56
|
+
|
57
|
+
(The MIT License)
|
58
|
+
|
59
|
+
Copyright (c) 2009 Aaron Patterson
|
60
|
+
|
61
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
62
|
+
a copy of this software and associated documentation files (the
|
63
|
+
'Software'), to deal in the Software without restriction, including
|
64
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
65
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
66
|
+
permit persons to whom the Software is furnished to do so, subject to
|
67
|
+
the following conditions:
|
68
|
+
|
69
|
+
The above copyright notice and this permission notice shall be
|
70
|
+
included in all copies or substantial portions of the Software.
|
71
|
+
|
72
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
73
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
74
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
75
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
76
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
77
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
78
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
|
6
|
+
$: << "lib"
|
7
|
+
require 'texticle'
|
8
|
+
|
9
|
+
Hoe.new('texticle', Texticle::VERSION) do |p|
|
10
|
+
p.developer('Aaron Patterson', 'aaronp@rubyforge.org')
|
11
|
+
p.readme_file = 'README.rdoc'
|
12
|
+
p.history_file = 'CHANGELOG.rdoc'
|
13
|
+
p.extra_rdoc_files = FileList['*.rdoc']
|
14
|
+
end
|
15
|
+
|
16
|
+
# vim: syntax=Ruby
|
data/lib/texticle.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'texticle/full_text_index'
|
2
|
+
|
3
|
+
####
|
4
|
+
# Texticle exposes full text search capabilities from PostgreSQL, and allows
|
5
|
+
# you to declare full text indexes. Texticle will extend ActiveRecord with
|
6
|
+
# named_scope methods making searching easy and fun!
|
7
|
+
#
|
8
|
+
# Texticle.index is automatically added to ActiveRecord::Base.
|
9
|
+
#
|
10
|
+
# To declare an index on a model, just use the index method:
|
11
|
+
#
|
12
|
+
# class Product < ActiveRecord::Base
|
13
|
+
# index do
|
14
|
+
# name
|
15
|
+
# description
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# This will allow you to do full text search on the name and description
|
20
|
+
# columns for the Product model. It defines a named_scope method called
|
21
|
+
# "search", so you can take advantage of the search like this:
|
22
|
+
#
|
23
|
+
# Product.search('foo bar')
|
24
|
+
#
|
25
|
+
# Indexes may also be named. For example:
|
26
|
+
#
|
27
|
+
# class Product < ActiveRecord::Base
|
28
|
+
# index 'author' do
|
29
|
+
# name
|
30
|
+
# author
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# A named index will add a named_scope with the index name prefixed by
|
35
|
+
# "search". In order to take advantage of the "author" index, just call:
|
36
|
+
#
|
37
|
+
# Product.search_author('foo bar')
|
38
|
+
#
|
39
|
+
# Finally, column names can be ranked. The ranks are A, B, C, and D. This
|
40
|
+
# lets us declare that matches in the "name" column are more important
|
41
|
+
# than matches in the "description" column:
|
42
|
+
#
|
43
|
+
# class Product < ActiveRecord::Base
|
44
|
+
# index do
|
45
|
+
# name 'A'
|
46
|
+
# description 'B'
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
module Texticle
|
50
|
+
# The version of Texticle you are using.
|
51
|
+
VERSION = '1.0.0'
|
52
|
+
|
53
|
+
# A list of full text indexes
|
54
|
+
attr_accessor :full_text_indexes
|
55
|
+
|
56
|
+
###
|
57
|
+
# Create an index with +name+ using +dictionary+
|
58
|
+
def index name = nil, dictionary = 'english', &block
|
59
|
+
search_name = ['search', name].compact.join('_')
|
60
|
+
|
61
|
+
class_eval(<<-eoruby)
|
62
|
+
named_scope :#{search_name}, lambda { |term|
|
63
|
+
{
|
64
|
+
:select => "\#{table_name}.*, ts_rank_cd((\#{full_text_indexes.first.to_s}),
|
65
|
+
plainto_tsquery(\#{connection.quote(term)\})) as rank",
|
66
|
+
:conditions =>
|
67
|
+
["\#{full_text_indexes.first.to_s} @@ plainto_tsquery(?)", term],
|
68
|
+
:order => 'rank DESC'
|
69
|
+
}
|
70
|
+
}
|
71
|
+
eoruby
|
72
|
+
index_name = [table_name, name, 'fts_idx'].compact.join('_')
|
73
|
+
(self.full_text_indexes ||= []) <<
|
74
|
+
FullTextIndex.new(index_name, dictionary, self, &block)
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Texticle
|
2
|
+
class FullTextIndex # :nodoc:
|
3
|
+
attr_accessor :index_columns
|
4
|
+
|
5
|
+
def initialize name, dictionary, model_class, &block
|
6
|
+
@name = name
|
7
|
+
@dictionary = dictionary
|
8
|
+
@model_class = model_class
|
9
|
+
@index_columns = {}
|
10
|
+
@string = nil
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
@model_class.connection.execute(<<-eosql)
|
16
|
+
CREATE index #{@name}
|
17
|
+
ON #{@model_class.table_name}
|
18
|
+
USING gin((#{to_s}))
|
19
|
+
eosql
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy
|
23
|
+
@model_class.connection.execute(<<-eosql)
|
24
|
+
DROP index IF EXISTS #{@name}
|
25
|
+
eosql
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
return @string if @string
|
30
|
+
vectors = []
|
31
|
+
@index_columns.sort_by { |k,v| k }.each do |weight, columns|
|
32
|
+
c = columns.map { |x| "coalesce(#{@model_class.table_name}.#{x}, '')" }
|
33
|
+
if weight == 'none'
|
34
|
+
vectors << "to_tsvector('#{@dictionary}', #{c.join(" || ' ' || ")})"
|
35
|
+
else
|
36
|
+
vectors <<
|
37
|
+
"setweight(to_tsvector('#{@dictionary}', #{c.join(" || ' ' || ")}), '#{weight}')"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
@string = vectors.join(" || ' ' || ")
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_missing name, *args
|
44
|
+
weight = args.shift || 'none'
|
45
|
+
(index_columns[weight] ||= []) << name.to_s
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'texticle'
|
3
|
+
|
4
|
+
namespace :texticle do
|
5
|
+
desc "Create full text indexes"
|
6
|
+
task :create_indexes => ['texticle:destroy_indexes'] do
|
7
|
+
Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')].each do |f|
|
8
|
+
klass = File.basename(f, '.rb').classify.constantize
|
9
|
+
if klass.respond_to?(:full_text_indexes)
|
10
|
+
(klass.full_text_indexes || []).each do |fti|
|
11
|
+
fti.create
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Destroy full text indexes"
|
18
|
+
task :destroy_indexes => [:environment] do
|
19
|
+
Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')].each do |f|
|
20
|
+
klass = File.basename(f, '.rb').classify.constantize
|
21
|
+
if klass.respond_to?(:full_text_indexes)
|
22
|
+
(klass.full_text_indexes || []).each do |fti|
|
23
|
+
fti.destroy
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/rails/init.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "texticle"
|
3
|
+
|
4
|
+
class TexticleTestCase < Test::Unit::TestCase
|
5
|
+
unless RUBY_VERSION >= '1.9'
|
6
|
+
undef :default_test
|
7
|
+
end
|
8
|
+
|
9
|
+
def setup
|
10
|
+
warn "#{name}" if ENV['TESTOPTS'] == '-v'
|
11
|
+
end
|
12
|
+
|
13
|
+
def fake_model
|
14
|
+
Class.new do
|
15
|
+
@connected = false
|
16
|
+
@executed = []
|
17
|
+
@named_scopes = []
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_accessor :connected, :executed, :named_scopes
|
21
|
+
|
22
|
+
def connection
|
23
|
+
@connected = true
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute sql
|
28
|
+
@executed << sql
|
29
|
+
end
|
30
|
+
|
31
|
+
def table_name; 'fake_model'; end
|
32
|
+
|
33
|
+
def named_scope *args
|
34
|
+
@named_scopes << args
|
35
|
+
end
|
36
|
+
|
37
|
+
def quote thing
|
38
|
+
"'#{thing}'"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestFullTextIndex < TexticleTestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
@fm = fake_model
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_initialize
|
10
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
11
|
+
name
|
12
|
+
value 'A'
|
13
|
+
end
|
14
|
+
assert_equal 'name', fti.index_columns['none'].first
|
15
|
+
assert_equal 'value', fti.index_columns['A'].first
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_destroy
|
19
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
20
|
+
name
|
21
|
+
value 'A'
|
22
|
+
end
|
23
|
+
fti.destroy
|
24
|
+
assert @fm.connected
|
25
|
+
assert_equal 1, @fm.executed.length
|
26
|
+
executed = @fm.executed.first
|
27
|
+
assert_match "DROP index IF EXISTS #{fti.instance_variable_get(:@name)}", executed
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_create
|
31
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
32
|
+
name
|
33
|
+
value 'A'
|
34
|
+
end
|
35
|
+
fti.create
|
36
|
+
assert @fm.connected
|
37
|
+
assert_equal 1, @fm.executed.length
|
38
|
+
executed = @fm.executed.first
|
39
|
+
assert_match fti.to_s, executed
|
40
|
+
assert_match "CREATE index #{fti.instance_variable_get(:@name)}", executed
|
41
|
+
assert_match "ON #{@fm.table_name}", executed
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_to_s_no_weight
|
45
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
46
|
+
name
|
47
|
+
end
|
48
|
+
assert_equal "to_tsvector('english', coalesce(#{@fm.table_name}.name, ''))", fti.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_to_s_A_weight
|
52
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
53
|
+
name 'A'
|
54
|
+
end
|
55
|
+
assert_equal "setweight(to_tsvector('english', coalesce(#{@fm.table_name}.name, '')), 'A')", fti.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_to_s_multi_weight
|
59
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
60
|
+
name 'A'
|
61
|
+
value 'A'
|
62
|
+
description 'B'
|
63
|
+
end
|
64
|
+
assert_equal "setweight(to_tsvector('english', coalesce(#{@fm.table_name}.name, '') || ' ' || coalesce(#{@fm.table_name}.value, '')), 'A') || ' ' || setweight(to_tsvector('english', coalesce(#{@fm.table_name}.description, '')), 'B')", fti.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_mixed_weight
|
68
|
+
fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
|
69
|
+
name
|
70
|
+
value 'A'
|
71
|
+
end
|
72
|
+
assert_equal "setweight(to_tsvector('english', coalesce(#{@fm.table_name}.value, '')), 'A') || ' ' || to_tsvector('english', coalesce(#{@fm.table_name}.name, ''))", fti.to_s
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestTexticle < TexticleTestCase
|
4
|
+
def test_index_method
|
5
|
+
x = fake_model
|
6
|
+
x.class_eval do
|
7
|
+
extend Texticle
|
8
|
+
index do
|
9
|
+
name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
assert_equal 1, x.full_text_indexes.length
|
13
|
+
assert_equal 1, x.named_scopes.length
|
14
|
+
|
15
|
+
x.full_text_indexes.first.create
|
16
|
+
assert_match "#{x.table_name}_fts_idx", x.executed.first
|
17
|
+
assert_equal :search, x.named_scopes.first.first
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_named_index
|
21
|
+
x = fake_model
|
22
|
+
x.class_eval do
|
23
|
+
extend Texticle
|
24
|
+
index('awesome') do
|
25
|
+
name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
assert_equal 1, x.full_text_indexes.length
|
29
|
+
assert_equal 1, x.named_scopes.length
|
30
|
+
|
31
|
+
x.full_text_indexes.first.create
|
32
|
+
assert_match "#{x.table_name}_awesome_fts_idx", x.executed.first
|
33
|
+
assert_equal :search_awesome, x.named_scopes.first.first
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_named_scope_select
|
37
|
+
x = fake_model
|
38
|
+
x.class_eval do
|
39
|
+
extend Texticle
|
40
|
+
index('awesome') do
|
41
|
+
name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
ns = x.named_scopes.first[1].call('foo')
|
45
|
+
assert_match(/^#{x.table_name}\.\*/, ns[:select])
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: texticle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Patterson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-15 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.12.1
|
24
|
+
version:
|
25
|
+
description: |-
|
26
|
+
Texticle exposes full text search capabilities from PostgreSQL, and allows
|
27
|
+
you to declare full text indexes. Texticle will extend ActiveRecord with
|
28
|
+
named_scope methods making searching easy and fun!
|
29
|
+
email:
|
30
|
+
- aaronp@rubyforge.org
|
31
|
+
executables: []
|
32
|
+
|
33
|
+
extensions: []
|
34
|
+
|
35
|
+
extra_rdoc_files:
|
36
|
+
- Manifest.txt
|
37
|
+
- CHANGELOG.rdoc
|
38
|
+
- README.rdoc
|
39
|
+
files:
|
40
|
+
- .autotest
|
41
|
+
- CHANGELOG.rdoc
|
42
|
+
- Manifest.txt
|
43
|
+
- README.rdoc
|
44
|
+
- Rakefile
|
45
|
+
- lib/texticle.rb
|
46
|
+
- lib/texticle/full_text_index.rb
|
47
|
+
- lib/texticle/tasks.rb
|
48
|
+
- rails/init.rb
|
49
|
+
- test/helper.rb
|
50
|
+
- test/test_full_text_index.rb
|
51
|
+
- test/test_texticle.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://texticle.rubyforge.org/
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README.rdoc
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project: texticle
|
77
|
+
rubygems_version: 1.3.2
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: Texticle exposes full text search capabilities from PostgreSQL, and allows you to declare full text indexes
|
81
|
+
test_files:
|
82
|
+
- test/test_full_text_index.rb
|
83
|
+
- test/test_texticle.rb
|