texticle 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-04-14
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,12 @@
1
+ .autotest
2
+ CHANGELOG.rdoc
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/texticle.rb
7
+ lib/texticle/full_text_index.rb
8
+ lib/texticle/tasks.rb
9
+ rails/init.rb
10
+ test/helper.rb
11
+ test/test_full_text_index.rb
12
+ test/test_texticle.rb
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'texticle'
2
+
3
+ ActiveRecord::Base.extend(Texticle)
@@ -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