texticle 1.0.4.20101004123327 → 2.0.pre

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,27 @@
1
+ === 2.0rc1
2
+
3
+ * Complete refactoring of Texticle
4
+
5
+ * For users:
6
+
7
+ * Texticle should only be used for its simplicity; if you need to deeply configure your text search, please give `gem install pg_search` a try.
8
+ * #search method is now included in all ActiveRecord models by default, and searches across a model's :string columns.
9
+ * #search_by_<column> dynamic methods are now available.
10
+ * #search can now be chained; Game.search_by_title("Street Fighter").search_by_system("PS3") works.
11
+ * #search now accepts a hash to specify columns to be searched, e.g. Game.search(:name => "Mario")
12
+ * No more access to #rank values for results (though they're still ordered by rank).
13
+ * No way to give different weights to different columns in this release.
14
+
15
+ * For devs:
16
+
17
+ * We now have actual tests to run against; this will make accepting pull requests much more enjoyable.
18
+
19
+ === HEAD (unreleased)
20
+
21
+ * 1 minor bugfix
22
+
23
+ * Multiple named indices are now supported.
24
+
1
25
  === 1.0.4 / 2010-08-19
2
26
 
3
27
  * 2 major enhancements
data/README.rdoc CHANGED
@@ -1,12 +1,11 @@
1
1
  = texticle
2
2
 
3
- * http://texticle.rubyforge.org/
3
+ * http://tenderlove.github.com/texticle
4
4
 
5
5
  == DESCRIPTION:
6
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!
7
+ Texticle exposes full text search capabilities from PostgreSQL,
8
+ extending ActiveRecord with scopes making search easy and fun!
10
9
 
11
10
  == FEATURES/PROBLEMS:
12
11
 
@@ -14,82 +13,40 @@ named_scope methods making searching easy and fun!
14
13
 
15
14
  == SYNOPSIS:
16
15
 
17
- === Rails 3 Configuration
16
+ === Configuration
18
17
 
19
- In the project's Gemfile add
20
-
21
- gem 'texticle', '1.0.4'
22
-
23
- === Rails 2 Configuration
18
+ * Rails 3
24
19
 
25
- In environment.rb:
20
+ In the project's Gemfile add
26
21
 
27
- config.gem 'texticle'
22
+ gem 'texticle', '~> 2.0rc1', :require => 'texticle/rails'
28
23
 
29
- In your Rakefile:
24
+ * ActiveRecord outside of Rails 3
30
25
 
31
- require 'rubygems'
32
- require 'texticle/tasks'
26
+ require 'texticle'
27
+ ActiveRecord::Base.extend(Texticle)
33
28
 
34
29
  === Usage
35
30
 
36
- Declare your index in your model:
37
-
38
- class Product < ActiveRecord::Base
39
- index do
40
- name
41
- description
42
- end
43
- end
44
-
45
- Use the search method
46
-
47
- Product.search('hello world')
48
-
49
- Full text searches can be sped up by creating indexes. To create them, run:
50
-
51
- $ rake texticle:create_indexes
52
-
53
- Alternatively, you can create a migration that will build your indexes:
54
-
55
- $ rake texticle:migration
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:
31
+ Your models now have access to the search method:
74
32
 
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.
33
+ Game.search('Sonic') # will search through the model's :string columns
34
+ Game.search(:title => 'Mario')
35
+ Game.search_by_title('Street Fighter').search_by_system('PS3')
79
36
 
80
37
  == REQUIREMENTS:
81
38
 
82
- * Texticle may be used outside rails, but works best with rails.
39
+ * ActiveRecord
83
40
 
84
41
  == INSTALL:
85
42
 
86
- * sudo gem install texticle
43
+ * gem install texticle
87
44
 
88
45
  == LICENSE:
89
46
 
90
47
  (The MIT License)
91
48
 
92
- Copyright (c) 2009 Aaron Patterson
49
+ Copyright (c) 2011 Aaron Patterson
93
50
 
94
51
  Permission is hereby granted, free of charge, to any person obtaining
95
52
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -1,14 +1,20 @@
1
- # -*- ruby -*-
2
-
3
1
  require 'rubygems'
4
- require 'hoe'
5
2
 
6
- Hoe.plugin :gemspec
7
- Hoe.spec 'texticle' do
8
- developer('Aaron Patterson', 'aaronp@rubyforge.org')
9
- self.readme_file = 'README.rdoc'
10
- self.history_file = 'CHANGELOG.rdoc'
11
- self.extra_rdoc_files = FileList['*.rdoc']
12
- end
3
+ require 'rake'
4
+ require 'pg'
5
+ require 'active_record'
6
+ require 'benchmark'
13
7
 
14
- # vim: syntax=Ruby
8
+ require File.expand_path(File.dirname(__FILE__) + '/spec/spec_helper')
9
+
10
+ namespace :db do
11
+ desc 'Run migrations for test database'
12
+ task :migrate do
13
+ ActiveRecord::Migration.instance_eval do
14
+ create_table :games do |table|
15
+ table.string :system
16
+ table.string :title
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,12 +1,8 @@
1
1
  # Module used to conform to Rails 3 plugin API
2
- require File.expand_path( File.dirname(__FILE__) + '/../texticle')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../texticle')
3
3
 
4
4
  module Texticle
5
5
  class Railtie < Rails::Railtie
6
- rake_tasks do
7
- load File.dirname(__FILE__) + '/tasks.rb'
8
- end
9
-
10
6
  initializer "texticle.configure_rails_initialization" do
11
7
  ActiveRecord::Base.extend(Texticle)
12
8
  end
data/lib/texticle.rb CHANGED
@@ -1,113 +1,86 @@
1
- require 'texticle/full_text_index'
2
- require 'texticle/railtie' if defined?(Rails) and Rails::VERSION::MAJOR > 2
3
-
4
- ####
5
- # Texticle exposes full text search capabilities from PostgreSQL, and allows
6
- # you to declare full text indexes. Texticle will extend ActiveRecord with
7
- # named_scope methods making searching easy and fun!
8
- #
9
- # Texticle.index is automatically added to ActiveRecord::Base.
10
- #
11
- # To declare an index on a model, just use the index method:
12
- #
13
- # class Product < ActiveRecord::Base
14
- # index do
15
- # name
16
- # description
17
- # end
18
- # end
19
- #
20
- # This will allow you to do full text search on the name and description
21
- # columns for the Product model. It defines a named_scope method called
22
- # "search", so you can take advantage of the search like this:
23
- #
24
- # Product.search('foo bar')
25
- #
26
- # Indexes may also be named. For example:
27
- #
28
- # class Product < ActiveRecord::Base
29
- # index 'author' do
30
- # name
31
- # author
32
- # end
33
- # end
34
- #
35
- # A named index will add a named_scope with the index name prefixed by
36
- # "search". In order to take advantage of the "author" index, just call:
37
- #
38
- # Product.search_author('foo bar')
39
- #
40
- # Finally, column names can be ranked. The ranks are A, B, C, and D. This
41
- # lets us declare that matches in the "name" column are more important
42
- # than matches in the "description" column:
43
- #
44
- # class Product < ActiveRecord::Base
45
- # index do
46
- # name 'A'
47
- # description 'B'
48
- # end
49
- # end
1
+ require 'active_record'
2
+
50
3
  module Texticle
51
- # The version of Texticle you are using.
52
- VERSION = '1.0.4' unless defined?(Texticle::VERSION)
53
-
54
- # A list of full text indexes
55
- attr_accessor :full_text_indexes
56
-
57
- ###
58
- # Create an index with +name+ using +dictionary+
59
- def index name = nil, dictionary = 'english', &block
60
- search_name = ['search', name].compact.join('_')
61
-
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'
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
4
+
5
+ def search(query = {})
6
+ language = connection.quote('english')
7
+
8
+ exclusive = true
9
+
10
+ unless query.is_a?(Hash)
11
+ exclusive = false
12
+ query = columns.select {|column| column.type == :string }.map(&:name).inject({}) do |terms, column|
13
+ terms.merge column => query.to_s
14
+ end
15
+ end
16
+
17
+ similarities = []
18
+ conditions = []
19
+
20
+ query.each do |column, search_term|
21
+ column = connection.quote_column_name(column)
22
+ search_term = connection.quote normalize(Helper.normalize(search_term))
23
+ similarities << "ts_rank(to_tsvector(#{quoted_table_name}.#{column}), to_tsquery(#{search_term}))"
24
+ conditions << "to_tsvector(#{language}, #{column}) @@ to_tsquery(#{search_term})"
25
+ end
26
+
27
+ rank = connection.quote_column_name('rank' + rand.to_s)
28
+
29
+ select("#{quoted_table_name}.*, #{similarities.join(" + ")} AS #{rank}").
30
+ where(conditions.join(exclusive ? " AND " : " OR ")).
31
+ order("#{rank} DESC")
32
+ end
33
+
34
+ def method_missing(method, *search_terms)
35
+ if Helper.dynamic_search_method?(method, self.columns)
36
+ columns = Helper.dynamic_search_columns(method)
37
+ metaclass = class << self; self; end
38
+ metaclass.__send__(:define_method, method) do |*args|
39
+ query = columns.inject({}) do |query, column|
40
+ query.merge column => args.shift
41
+ end
42
+ search(query)
106
43
  end
44
+ __send__(method, *search_terms)
45
+ else
46
+ super
107
47
  end
48
+ end
49
+
50
+ def respond_to?(method)
51
+ Helper.dynamic_search_method?(method, self.columns) ? true : super
52
+ end
53
+
54
+ private
108
55
 
109
- index_name = [table_name, name, 'fts_idx'].compact.join('_')
110
- (self.full_text_indexes ||= []) <<
111
- FullTextIndex.new(index_name, dictionary, self, &block)
56
+ def normalize(query)
57
+ query
112
58
  end
59
+
60
+ module Helper
61
+ class << self
62
+ def normalize(query)
63
+ query.to_s.gsub(' ', '\\\\ ')
64
+ end
65
+
66
+ def dynamic_search_columns(method)
67
+ if match = method.to_s.match(/search_by_(?<columns>[_a-zA-Z]\w*)/)
68
+ match[:columns].split('_and_')
69
+ else
70
+ []
71
+ end
72
+ end
73
+
74
+ def dynamic_search_method?(method, class_columns)
75
+ string_columns = class_columns.select {|column| column.type == :string }.map(&:name)
76
+ columns = dynamic_search_columns(method)
77
+ unless columns.empty?
78
+ columns.all? {|column| string_columns.include?(column) }
79
+ else
80
+ false
81
+ end
82
+ end
83
+ end
84
+ end
85
+
113
86
  end
data/spec/config.yml ADDED
@@ -0,0 +1,4 @@
1
+ database: texticle
2
+ username: <username>
3
+ pool: 5
4
+ timeout: 5000
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'yaml'
4
+ require 'texticle'
5
+ require 'shoulda'
6
+ require 'ruby-debug'
7
+
8
+ config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/config.yml')
9
+ ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
@@ -0,0 +1,119 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ class Game < ActiveRecord::Base
5
+ # string :system
6
+ # string :title
7
+
8
+ def to_s
9
+ "#{system}: #{title}"
10
+ end
11
+ end
12
+
13
+ class TexticleTest < Test::Unit::TestCase
14
+
15
+ context "after extending an ActiveRecord::Base subclass" do
16
+ setup do
17
+ Game.extend(Texticle)
18
+ @zelda = Game.create :system => "NES", :title => "Legend of Zelda"
19
+ @mario = Game.create :system => "NES", :title => "Super Mario Bros."
20
+ @sonic = Game.create :system => "Genesis", :title => "Sonic the Hedgehog"
21
+ @dkong = Game.create :system => "SNES", :title => "Diddy's Kong Quest"
22
+ @megam = Game.create :system => nil, :title => "Mega Man"
23
+ @sfnes = Game.create :system => "SNES", :title => "Street Fighter 2"
24
+ @sfgen = Game.create :system => "Genesis", :title => "Street Fighter 2"
25
+ @takun = Game.create :system => "Saturn", :title => "Magical Tarurūto-kun"
26
+ end
27
+
28
+ teardown do
29
+ Game.delete_all
30
+ end
31
+
32
+ should "define a #search method" do
33
+ assert Game.respond_to?(:search)
34
+ end
35
+
36
+ context "when searching with a String argument" do
37
+ should "search across all :string columns if no indexes have been specified" do
38
+ assert_equal @mario, Game.search("Mario").first
39
+ assert_equal 1, Game.search("Mario").count
40
+
41
+ assert (Game.search("NES") && [@mario, @zelda]) == [@mario, @zelda]
42
+ assert_equal 2, Game.search("NES").count
43
+ end
44
+
45
+ should "work if the query contains an apostrophe" do
46
+ assert_equal @dkong, Game.search("Diddy's").first
47
+ assert_equal 1, Game.search("Diddy's").count
48
+ end
49
+
50
+ should "work if the query contains whitespace" do
51
+ assert_equal @megam, Game.search("Mega Man").first
52
+ end
53
+
54
+ should "work if the query contains an accent" do
55
+ assert_equal @takun, Game.search("Tarurūto-kun").first
56
+ end
57
+
58
+ should "search across records with NULL values" do
59
+ assert_equal @megam, Game.search("Mega").first
60
+ end
61
+
62
+ should "scope consecutively" do
63
+ assert_equal @sfgen, Game.search("Genesis").search("Street Fighter").first
64
+ end
65
+ end
66
+
67
+ context "when searching with a Hash argument" do
68
+ should "search across the given columns" do
69
+ assert Game.search(:title => "NES").empty?
70
+ assert Game.search(:system => "Mario").empty?
71
+ puts Game.search(:system => "NES", :title => "Sonic").to_a
72
+ assert Game.search(:system => "NES", :title => "Sonic").empty?
73
+
74
+ assert_equal @mario, Game.search(:title => "Mario").first
75
+ assert_equal 1, Game.search(:title => "Mario").count
76
+
77
+ assert_equal 2, Game.search(:system => "NES").count
78
+
79
+ assert_equal @zelda, Game.search(:system => "NES", :title => "Zelda").first
80
+ assert_equal @megam, Game.search(:title => "Mega").first
81
+ end
82
+
83
+ should "scope consecutively" do
84
+ assert_equal @sfgen, Game.search(:system => "Genesis").search(:title => "Street Fighter").first
85
+ end
86
+ end
87
+
88
+ context "when using dynamic search methods" do
89
+ should "generate methods for each :string column" do
90
+ assert_equal @mario, Game.search_by_title("Mario").first
91
+ assert_equal @takun, Game.search_by_system("Saturn").first
92
+ end
93
+
94
+ should "generate methods for any combination of :string columns" do
95
+ assert_equal @mario, Game.search_by_title_and_system("Mario", "NES").first
96
+ assert_equal @sonic, Game.search_by_system_and_title("Genesis", "Sonic").first
97
+ assert_equal @mario, Game.search_by_title_and_title("Mario", "Mario").first
98
+ end
99
+
100
+ should "scope consecutively" do
101
+ assert_equal @sfgen, Game.search_by_system("Genesis").search_by_title("Street Fighter").first
102
+ end
103
+
104
+ should "not generate methods for non-:string columns" do
105
+ assert_raise(NoMethodError) { Game.search_by_id }
106
+ end
107
+
108
+ should "work with #respond_to?" do
109
+ assert Game.respond_to?(:search_by_system)
110
+ assert Game.respond_to?(:search_by_title)
111
+ assert Game.respond_to?(:search_by_system_and_title)
112
+ assert Game.respond_to?(:search_by_title_and_title_and_title)
113
+
114
+ assert !Game.respond_to?(:search_by_id)
115
+ end
116
+ end
117
+ end
118
+
119
+ end
metadata CHANGED
@@ -1,62 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: texticle
3
3
  version: !ruby/object:Gem::Version
4
- hash: 40202008246577
5
- prerelease: false
6
- segments:
7
- - 1
8
- - 0
9
- - 4
10
- - 20101004123327
11
- version: 1.0.4.20101004123327
4
+ prerelease: 4
5
+ version: 2.0.pre
12
6
  platform: ruby
13
7
  authors:
8
+ - ecin
14
9
  - Aaron Patterson
15
10
  autorequire:
16
11
  bindir: bin
17
12
  cert_chain: []
18
13
 
19
- date: 2010-10-04 00:00:00 -04:00
20
- default_executable:
14
+ date: 2011-06-14 00:00:00 Z
21
15
  dependencies:
22
16
  - !ruby/object:Gem::Dependency
23
- name: rubyforge
17
+ name: pg
24
18
  prerelease: false
25
19
  requirement: &id001 !ruby/object:Gem::Requirement
26
20
  none: false
27
21
  requirements:
28
22
  - - ">="
29
23
  - !ruby/object:Gem::Version
30
- hash: 7
31
- segments:
32
- - 2
33
- - 0
34
- - 4
35
- version: 2.0.4
24
+ version: 0.11.0
36
25
  type: :development
37
26
  version_requirements: *id001
38
27
  - !ruby/object:Gem::Dependency
39
- name: hoe
28
+ name: shoulda
40
29
  prerelease: false
41
30
  requirement: &id002 !ruby/object:Gem::Requirement
42
31
  none: false
43
32
  requirements:
44
33
  - - ">="
45
34
  - !ruby/object:Gem::Version
46
- hash: 19
47
- segments:
48
- - 2
49
- - 6
50
- - 2
51
- version: 2.6.2
35
+ version: 2.11.3
52
36
  type: :development
53
37
  version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: activerecord
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ type: :runtime
48
+ version_requirements: *id003
54
49
  description: |-
55
- Texticle exposes full text search capabilities from PostgreSQL, and allows
56
- you to declare full text indexes. Texticle will extend ActiveRecord with
57
- named_scope methods making searching easy and fun!
50
+ Texticle exposes full text search capabilities from PostgreSQL, extending
51
+ ActiveRecord with scopes making search easy and fun!
58
52
  email:
59
- - aaronp@rubyforge.org
53
+ - ecin@copypastel.com
60
54
  executables: []
61
55
 
62
56
  extensions: []
@@ -66,21 +60,16 @@ extra_rdoc_files:
66
60
  - CHANGELOG.rdoc
67
61
  - README.rdoc
68
62
  files:
69
- - .autotest
70
63
  - CHANGELOG.rdoc
71
64
  - Manifest.txt
72
65
  - README.rdoc
73
66
  - Rakefile
74
67
  - lib/texticle.rb
75
- - lib/texticle/full_text_index.rb
76
- - lib/texticle/railtie.rb
77
- - lib/texticle/tasks.rb
78
- - rails/init.rb
79
- - test/helper.rb
80
- - test/test_full_text_index.rb
81
- - test/test_texticle.rb
82
- has_rdoc: true
83
- homepage: http://texticle.rubyforge.org/
68
+ - lib/texticle/rails.rb
69
+ - spec/spec_helper.rb
70
+ - spec/texticle_spec.rb
71
+ - spec/config.yml
72
+ homepage: http://tenderlove.github.com/texticle
84
73
  licenses: []
85
74
 
86
75
  post_install_message:
@@ -94,26 +83,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
83
  requirements:
95
84
  - - ">="
96
85
  - !ruby/object:Gem::Version
97
- hash: 3
98
- segments:
99
- - 0
100
86
  version: "0"
101
87
  required_rubygems_version: !ruby/object:Gem::Requirement
102
88
  none: false
103
89
  requirements:
104
90
  - - ">="
105
91
  - !ruby/object:Gem::Version
106
- hash: 3
107
- segments:
108
- - 0
109
92
  version: "0"
110
93
  requirements: []
111
94
 
112
95
  rubyforge_project: texticle
113
- rubygems_version: 1.3.7
96
+ rubygems_version: 1.7.2
114
97
  signing_key:
115
98
  specification_version: 3
116
- summary: Texticle exposes full text search capabilities from PostgreSQL, and allows you to declare full text indexes
99
+ summary: Texticle exposes full text search capabilities from PostgreSQL
117
100
  test_files:
118
- - test/test_full_text_index.rb
119
- - test/test_texticle.rb
101
+ - spec/spec_helper.rb
102
+ - spec/texticle_spec.rb
103
+ - spec/config.yml
data/.autotest DELETED
@@ -1,23 +0,0 @@
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
@@ -1,58 +0,0 @@
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 self.find_constant_of(filename)
15
- File.basename(filename, '.rb').pluralize.classify.constantize
16
- end
17
-
18
- def create
19
- @model_class.connection.execute create_sql
20
- end
21
-
22
- def destroy
23
- @model_class.connection.execute destroy_sql
24
- end
25
-
26
- def create_sql
27
- <<-eosql.chomp
28
- CREATE index #{@name}
29
- ON #{@model_class.table_name}
30
- USING gin((#{to_s}))
31
- eosql
32
- end
33
-
34
- def destroy_sql
35
- "DROP index IF EXISTS #{@name}"
36
- end
37
-
38
- def to_s
39
- return @string if @string
40
- vectors = []
41
- @index_columns.sort_by { |k,v| k }.each do |weight, columns|
42
- c = columns.map { |x| "coalesce(\"#{@model_class.table_name}\".\"#{x}\", '')" }
43
- if weight == 'none'
44
- vectors << "to_tsvector('#{@dictionary}', #{c.join(" || ' ' || ")})"
45
- else
46
- vectors <<
47
- "setweight(to_tsvector('#{@dictionary}', #{c.join(" || ' ' || ")}), '#{weight}')"
48
- end
49
- end
50
- @string = vectors.join(" || ' ' || ")
51
- end
52
-
53
- def method_missing name, *args
54
- weight = args.shift || 'none'
55
- (index_columns[weight] ||= []) << name.to_s
56
- end
57
- end
58
- end
@@ -1,90 +0,0 @@
1
- require 'rake'
2
- require 'texticle'
3
-
4
- namespace :texticle do
5
- desc "Create full text index migration"
6
- task :migration => :environment do
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(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)
15
- if klass.respond_to?(:full_text_indexes)
16
- (klass.full_text_indexes || []).each do |fti|
17
- up_sql_statements << fti.destroy_sql
18
- up_sql_statements << fti.create_sql
19
- dn_sql_statements << fti.destroy_sql
20
- end
21
- end
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)
32
- fh.puts " end"
33
- fh.puts "end"
34
- end
35
- end
36
-
37
- desc "Create full text indexes"
38
- task :create_indexes => ['texticle:destroy_indexes'] do
39
- Dir[Rails.root + 'app' + 'models' + '*.rb'].each do |f|
40
- klass = Texticle::FullTextIndex.find_constant_of(f)
41
- if klass.respond_to?(:full_text_indexes)
42
- (klass.full_text_indexes || []).each do |fti|
43
- begin
44
- fti.create
45
- rescue ActiveRecord::StatementInvalid => e
46
- warn "WARNING: Couldn't create index for #{klass.to_s}, skipping..."
47
- end
48
- end
49
- end
50
- end
51
- end
52
-
53
- desc "Destroy full text indexes"
54
- task :destroy_indexes => [:environment] do
55
- Dir[Rails.root + 'app' + 'models' + '*.rb'].each do |f|
56
- klass = Texticle::FullTextIndex.find_constant_of(f)
57
- if klass.respond_to?(:full_text_indexes)
58
- (klass.full_text_indexes || []).each do |fti|
59
- fti.destroy
60
- end
61
- end
62
- end
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
89
- end
90
-
data/rails/init.rb DELETED
@@ -1,3 +0,0 @@
1
- require 'texticle'
2
-
3
- ActiveRecord::Base.extend(Texticle)
data/test/helper.rb DELETED
@@ -1,44 +0,0 @@
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
- alias :scope :named_scope
37
-
38
- def quote thing
39
- "'#{thing}'"
40
- end
41
- end
42
- end
43
- end
44
- end
@@ -1,74 +0,0 @@
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
@@ -1,76 +0,0 @@
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
- # One named_scope for search, another for trigram search
14
- assert_equal 2, x.named_scopes.length
15
-
16
- x.full_text_indexes.first.create
17
- assert_match "#{x.table_name}_fts_idx", x.executed.first
18
- assert_equal :search, x.named_scopes.first.first
19
- assert_equal :tsearch, x.named_scopes[1].first
20
- end
21
-
22
- def test_named_index
23
- x = fake_model
24
- x.class_eval do
25
- extend Texticle
26
- index('awesome') do
27
- name
28
- end
29
- end
30
- assert_equal 1, x.full_text_indexes.length
31
- assert_equal 2, x.named_scopes.length
32
-
33
- x.full_text_indexes.first.create
34
- assert_match "#{x.table_name}_awesome_fts_idx", x.executed.first
35
- assert_equal :search_awesome, x.named_scopes.first.first
36
- assert_equal :tsearch_awesome, x.named_scopes[1].first
37
- end
38
-
39
- def test_named_scope_select
40
- x = fake_model
41
- x.class_eval do
42
- extend Texticle
43
- index('awesome') do
44
- name
45
- end
46
- end
47
- ns = x.named_scopes.first[1].call('foo')
48
- assert_match(/^#{x.table_name}\.\*/, ns[:select])
49
- end
50
-
51
- def test_double_quoted_queries
52
- x = fake_model
53
- x.class_eval do
54
- extend Texticle
55
- index('awesome') do
56
- name
57
- end
58
- end
59
-
60
- ns = x.named_scopes.first[1].call('foo bar "foo bar"')
61
- assert_match(/'foo' & 'bar' & 'foo bar'/, ns[:select])
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
76
- end