texticle 1.0.3 → 1.0.4.20101004123327

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.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,16 @@
1
+ === 1.0.4 / 2010-08-19
2
+
3
+ * 2 major enhancements
4
+
5
+ * use Rails.root instead of RAILS_ROOT
6
+ * refactored tasks to ease maintainance and patchability
7
+
8
+ * 3 minor enhancements
9
+
10
+ * fix timestamp for migrationfile
11
+ * fixed deprecation warning for rails3 (dropping rails2-support)
12
+ * prevented warning about defined constant
13
+
1
14
  === 1.0.3 / 2010-07-07
2
15
 
3
16
  * 1 major enhancement
data/README.rdoc CHANGED
@@ -18,7 +18,7 @@ named_scope methods making searching easy and fun!
18
18
 
19
19
  In the project's Gemfile add
20
20
 
21
- gem 'texticle', '1.0.2', :git => 'git://github.com/tenderlove/texticle'
21
+ gem 'texticle', '1.0.4'
22
22
 
23
23
  === Rails 2 Configuration
24
24
 
@@ -54,6 +54,29 @@ Alternatively, you can create a migration that will build your indexes:
54
54
 
55
55
  $ rake texticle:migration
56
56
 
57
+ == BETA: FUZZY SEARCH
58
+
59
+ We're adding support for Postgres' optional trigram matching module, which allows for fuzzier text searches.
60
+
61
+ Since it's an optional module, first step is to install it into the current database:
62
+
63
+ $ rake texticle:install_trigram
64
+
65
+ If you installed Postgres with its optional 'contrib' modules, you should be safe.
66
+
67
+ Declare your indexes as you normally would:
68
+
69
+ class Book < ActiveRecord::Base
70
+ index { title }
71
+ end
72
+
73
+ Finally, say hello to #tsearch:
74
+
75
+ Book.create :title => "_why's Poignant Guide to Ruby"
76
+ Book.tsearch "_whys Poignnt Gd t Rb" # => [#<Book title: "_why's Poignant Guide to Ruby">]
77
+
78
+ Trigram searching is still in beta, so expect the API to change and/or improve.
79
+
57
80
  == REQUIREMENTS:
58
81
 
59
82
  * Texticle may be used outside rails, but works best with rails.
data/Rakefile CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'rubygems'
4
4
  require 'hoe'
5
5
 
6
+ Hoe.plugin :gemspec
6
7
  Hoe.spec 'texticle' do
7
8
  developer('Aaron Patterson', 'aaronp@rubyforge.org')
8
9
  self.readme_file = 'README.rdoc'
data/lib/texticle.rb CHANGED
@@ -49,7 +49,7 @@ require 'texticle/railtie' if defined?(Rails) and Rails::VERSION::MAJOR > 2
49
49
  # end
50
50
  module Texticle
51
51
  # The version of Texticle you are using.
52
- VERSION = '1.0.3'
52
+ VERSION = '1.0.4' unless defined?(Texticle::VERSION)
53
53
 
54
54
  # A list of full text indexes
55
55
  attr_accessor :full_text_indexes
@@ -59,19 +59,53 @@ module Texticle
59
59
  def index name = nil, dictionary = 'english', &block
60
60
  search_name = ['search', name].compact.join('_')
61
61
 
62
- class_eval do
63
- named_scope search_name.to_sym, lambda { |term|
64
- # Let's extract the individual terms to allow for quoted terms.
65
- term = term.scan(/"([^"]+)"|(\S+)/).flatten.compact.map {|lex| "'#{lex}'"}.join(' & ')
66
- {
67
- :select => "#{table_name}.*, ts_rank_cd((#{full_text_indexes.first.to_s}),
68
- to_tsquery(#{connection.quote(term)})) as rank",
69
- :conditions =>
70
- ["#{full_text_indexes.first.to_s} @@ to_tsquery(?)", term],
71
- :order => 'rank DESC'
72
- }
62
+ scope_lamba = lambda { |term|
63
+ # Let's extract the individual terms to allow for quoted and wildcard terms.
64
+ term = term.scan(/"([^"]+)"|(\S+)/).flatten.compact.map do |lex|
65
+ lex =~ /(.+)\*\s*$/ ? "'#{$1}':*" : "'#{lex}'"
66
+ end.join(' & ')
67
+
68
+ {
69
+ :select => "#{table_name}.*, ts_rank_cd((#{full_text_indexes.first.to_s}),
70
+ to_tsquery(#{connection.quote(term)})) as rank",
71
+ :conditions =>
72
+ ["#{full_text_indexes.first.to_s} @@ to_tsquery(?)", term],
73
+ :order => 'rank DESC'
74
+ }
75
+ }
76
+
77
+ # tsearch, i.e. trigram search
78
+ trigram_scope_lambda = lambda { |term|
79
+ term = "'#{term.gsub("'", "''")}'"
80
+
81
+ similarities = full_text_indexes.first.index_columns.values.flatten.inject([]) do |array, index|
82
+ array << "similarity(#{index}, #{term})"
83
+ end.join(" + ")
84
+
85
+ conditions = full_text_indexes.first.index_columns.values.flatten.inject([]) do |array, index|
86
+ array << "(#{index} % #{term})"
87
+ end.join(" OR ")
88
+
89
+ {
90
+ :select => "#{table_name}.*, #{similarities} as rank",
91
+ :conditions => conditions,
92
+ :order => 'rank DESC'
73
93
  }
94
+ }
95
+
96
+ class_eval do
97
+ # Trying to avoid the deprecation warning when using :named_scope
98
+ # that Rails 3 emits. Can't use #respond_to?(:scope) since scope
99
+ # is a protected method in Rails 2, and thus still returns true.
100
+ if self.respond_to?(:scope) and not protected_methods.include?('scope')
101
+ scope search_name.to_sym, scope_lamba
102
+ scope ('t' + search_name).to_sym, trigram_scope_lambda
103
+ elsif self.respond_to? :named_scope
104
+ named_scope search_name.to_sym, scope_lamba
105
+ named_scope ('t' + search_name).to_sym, trigram_scope_lambda
106
+ end
74
107
  end
108
+
75
109
  index_name = [table_name, name, 'fts_idx'].compact.join('_')
76
110
  (self.full_text_indexes ||= []) <<
77
111
  FullTextIndex.new(index_name, dictionary, self, &block)
@@ -11,6 +11,10 @@ module Texticle
11
11
  instance_eval(&block)
12
12
  end
13
13
 
14
+ def self.find_constant_of(filename)
15
+ File.basename(filename, '.rb').pluralize.classify.constantize
16
+ end
17
+
14
18
  def create
15
19
  @model_class.connection.execute create_sql
16
20
  end
@@ -20,10 +24,10 @@ module Texticle
20
24
  end
21
25
 
22
26
  def create_sql
23
- <<-eosql
24
- CREATE index #{@name}
25
- ON #{@model_class.table_name}
26
- USING gin((#{to_s}))
27
+ <<-eosql.chomp
28
+ CREATE index #{@name}
29
+ ON #{@model_class.table_name}
30
+ USING gin((#{to_s}))
27
31
  eosql
28
32
  end
29
33
 
@@ -35,7 +39,7 @@ module Texticle
35
39
  return @string if @string
36
40
  vectors = []
37
41
  @index_columns.sort_by { |k,v| k }.each do |weight, columns|
38
- c = columns.map { |x| "coalesce(#{@model_class.table_name}.#{x}, '')" }
42
+ c = columns.map { |x| "coalesce(\"#{@model_class.table_name}\".\"#{x}\", '')" }
39
43
  if weight == 'none'
40
44
  vectors << "to_tsvector('#{@dictionary}', #{c.join(" || ' ' || ")})"
41
45
  else
@@ -5,34 +5,39 @@ namespace :texticle do
5
5
  desc "Create full text index migration"
6
6
  task :migration => :environment do
7
7
  now = Time.now.utc
8
- filename = "#{now.strftime('%Y%m%d%H%m%S')}_full_text_search_#{now.to_i}.rb"
9
- File.open(File.join(RAILS_ROOT, 'db', 'migrate', filename), 'wb') { |fh|
10
- fh.puts "class FullTextSearch#{now.to_i} < ActiveRecord::Migration"
11
- fh.puts " def self.up"
12
- Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')].each do |f|
13
- klass = File.basename(f, '.rb').pluralize.classify.constantize
8
+ filename = "#{now.strftime('%Y%m%d%H%M%S')}_full_text_search_#{now.to_i}.rb"
9
+ File.open(Rails.root + 'db' + 'migrate' + filename, 'wb') do |fh|
10
+ up_sql_statements = []
11
+ dn_sql_statements = []
12
+
13
+ Dir[Rails.root + 'app' + 'models' + '*.rb'].each do |f|
14
+ klass = Texticle::FullTextIndex.find_constant_of(f)
14
15
  if klass.respond_to?(:full_text_indexes)
15
16
  (klass.full_text_indexes || []).each do |fti|
16
- fh.puts <<-eostmt
17
- ActiveRecord::Base.connection.execute(<<-'eosql')
18
- #{fti.destroy_sql}
19
- eosql
20
- ActiveRecord::Base.connection.execute(<<-'eosql')
21
- #{fti.create_sql}
22
- eosql
23
- eostmt
17
+ up_sql_statements << fti.destroy_sql
18
+ up_sql_statements << fti.create_sql
19
+ dn_sql_statements << fti.destroy_sql
24
20
  end
25
21
  end
26
22
  end
23
+
24
+ fh.puts "class FullTextSearch#{now.to_i} < ActiveRecord::Migration"
25
+ fh.puts " def self.up"
26
+ insert_sql_statements_into_migration_file(up_sql_statements, fh)
27
+ fh.puts " end"
28
+ fh.puts ""
29
+
30
+ fh.puts " def self.down"
31
+ insert_sql_statements_into_migration_file(dn_sql_statements, fh)
27
32
  fh.puts " end"
28
33
  fh.puts "end"
29
- }
34
+ end
30
35
  end
31
36
 
32
37
  desc "Create full text indexes"
33
38
  task :create_indexes => ['texticle:destroy_indexes'] do
34
- Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')].each do |f|
35
- klass = File.basename(f, '.rb').pluralize.classify.constantize
39
+ Dir[Rails.root + 'app' + 'models' + '*.rb'].each do |f|
40
+ klass = Texticle::FullTextIndex.find_constant_of(f)
36
41
  if klass.respond_to?(:full_text_indexes)
37
42
  (klass.full_text_indexes || []).each do |fti|
38
43
  begin
@@ -47,8 +52,8 @@ namespace :texticle do
47
52
 
48
53
  desc "Destroy full text indexes"
49
54
  task :destroy_indexes => [:environment] do
50
- Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')].each do |f|
51
- klass = File.basename(f, '.rb').pluralize.classify.constantize
55
+ Dir[Rails.root + 'app' + 'models' + '*.rb'].each do |f|
56
+ klass = Texticle::FullTextIndex.find_constant_of(f)
52
57
  if klass.respond_to?(:full_text_indexes)
53
58
  (klass.full_text_indexes || []).each do |fti|
54
59
  fti.destroy
@@ -56,4 +61,30 @@ namespace :texticle do
56
61
  end
57
62
  end
58
63
  end
64
+
65
+ desc "Install trigram text search module"
66
+ task :install_trigram => [:environment] do
67
+ share_dir = `pg_config --sharedir`.chomp
68
+ raise RuntimeError, 'cannot find Postgres\' shared directory' unless $?.exitstatus.zero?
69
+ trigram = "#{share_dir}/contrib/pg_trgm.sql"
70
+ unless system("ls #{trigram}")
71
+ raise RuntimeError, 'cannot find trigram module; was it compiled and installed?'
72
+ end
73
+ db_name = ActiveRecord::Base.connection.current_database
74
+ unless system("psql -d #{db_name} -f #{trigram}")
75
+ raise RuntimeError, "`psql -d #{db_name} -f #{trigram}` cannot complete successfully"
76
+ end
77
+ puts "Trigram text search module successfully installed into '#{db_name}' database."
78
+ end
79
+
80
+ def insert_sql_statements_into_migration_file statements, fh
81
+ statements.each do |statement|
82
+ fh.puts <<-eostmt
83
+ execute(<<-'eosql'.strip)
84
+ #{statement}
85
+ eosql
86
+ eostmt
87
+ end
88
+ end
59
89
  end
90
+
data/test/helper.rb CHANGED
@@ -33,6 +33,7 @@ class TexticleTestCase < Test::Unit::TestCase
33
33
  def named_scope *args
34
34
  @named_scopes << args
35
35
  end
36
+ alias :scope :named_scope
36
37
 
37
38
  def quote thing
38
39
  "'#{thing}'"
@@ -45,14 +45,14 @@ class TestFullTextIndex < TexticleTestCase
45
45
  fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
46
46
  name
47
47
  end
48
- assert_equal "to_tsvector('english', coalesce(#{@fm.table_name}.name, ''))", fti.to_s
48
+ assert_equal "to_tsvector('english', coalesce(\"#{@fm.table_name}\".\"name\", ''))", fti.to_s
49
49
  end
50
50
 
51
51
  def test_to_s_A_weight
52
52
  fti = Texticle::FullTextIndex.new('ft_index', 'english', @fm) do
53
53
  name 'A'
54
54
  end
55
- assert_equal "setweight(to_tsvector('english', coalesce(#{@fm.table_name}.name, '')), 'A')", fti.to_s
55
+ assert_equal "setweight(to_tsvector('english', coalesce(\"#{@fm.table_name}\".\"name\", '')), 'A')", fti.to_s
56
56
  end
57
57
 
58
58
  def test_to_s_multi_weight
@@ -61,7 +61,7 @@ class TestFullTextIndex < TexticleTestCase
61
61
  value 'A'
62
62
  description 'B'
63
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
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
65
  end
66
66
 
67
67
  def test_mixed_weight
@@ -69,6 +69,6 @@ class TestFullTextIndex < TexticleTestCase
69
69
  name
70
70
  value 'A'
71
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
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
73
  end
74
74
  end
@@ -10,11 +10,13 @@ class TestTexticle < TexticleTestCase
10
10
  end
11
11
  end
12
12
  assert_equal 1, x.full_text_indexes.length
13
- assert_equal 1, x.named_scopes.length
13
+ # One named_scope for search, another for trigram search
14
+ assert_equal 2, x.named_scopes.length
14
15
 
15
16
  x.full_text_indexes.first.create
16
17
  assert_match "#{x.table_name}_fts_idx", x.executed.first
17
18
  assert_equal :search, x.named_scopes.first.first
19
+ assert_equal :tsearch, x.named_scopes[1].first
18
20
  end
19
21
 
20
22
  def test_named_index
@@ -26,11 +28,12 @@ class TestTexticle < TexticleTestCase
26
28
  end
27
29
  end
28
30
  assert_equal 1, x.full_text_indexes.length
29
- assert_equal 1, x.named_scopes.length
31
+ assert_equal 2, x.named_scopes.length
30
32
 
31
33
  x.full_text_indexes.first.create
32
34
  assert_match "#{x.table_name}_awesome_fts_idx", x.executed.first
33
35
  assert_equal :search_awesome, x.named_scopes.first.first
36
+ assert_equal :tsearch_awesome, x.named_scopes[1].first
34
37
  end
35
38
 
36
39
  def test_named_scope_select
@@ -57,4 +60,17 @@ class TestTexticle < TexticleTestCase
57
60
  ns = x.named_scopes.first[1].call('foo bar "foo bar"')
58
61
  assert_match(/'foo' & 'bar' & 'foo bar'/, ns[:select])
59
62
  end
63
+
64
+ def test_wildcard_queries
65
+ x = fake_model
66
+ x.class_eval do
67
+ extend Texticle
68
+ index('awesome') do
69
+ name
70
+ end
71
+ end
72
+
73
+ ns = x.named_scopes.first[1].call('foo bar*')
74
+ assert_match(/'foo' & 'bar:*'/, ns[:select])
75
+ end
60
76
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: texticle
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 40202008246577
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
- - 3
10
- version: 1.0.3
9
+ - 4
10
+ - 20101004123327
11
+ version: 1.0.4.20101004123327
11
12
  platform: ruby
12
13
  authors:
13
14
  - Aaron Patterson
@@ -15,7 +16,7 @@ autorequire:
15
16
  bindir: bin
16
17
  cert_chain: []
17
18
 
18
- date: 2010-07-08 00:00:00 -07:00
19
+ date: 2010-10-04 00:00:00 -04:00
19
20
  default_executable:
20
21
  dependencies:
21
22
  - !ruby/object:Gem::Dependency
@@ -42,12 +43,12 @@ dependencies:
42
43
  requirements:
43
44
  - - ">="
44
45
  - !ruby/object:Gem::Version
45
- hash: 23
46
+ hash: 19
46
47
  segments:
47
48
  - 2
48
49
  - 6
49
- - 0
50
- version: 2.6.0
50
+ - 2
51
+ version: 2.6.2
51
52
  type: :development
52
53
  version_requirements: *id002
53
54
  description: |-