texticle 2.0.3 → 2.1.0
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 +21 -0
- data/Gemfile +3 -0
- data/README.rdoc +17 -0
- data/Rakefile +105 -13
- data/lib/texticle.rb +121 -22
- data/lib/texticle/full_text_indexer.rb +79 -0
- data/lib/texticle/postgres_module_installer.rb +57 -0
- data/lib/texticle/searchable.rb +4 -0
- data/lib/texticle/tasks.rb +18 -0
- data/spec/{config.yml → config.yml.example} +1 -1
- data/spec/fixtures/character.rb +0 -0
- data/spec/fixtures/game.rb +9 -0
- data/spec/fixtures/webcomic.rb +9 -0
- data/spec/spec_helper.rb +70 -1
- data/spec/texticle/searchable_spec.rb +55 -24
- data/spec/texticle_spec.rb +67 -82
- metadata +80 -24
data/CHANGELOG.rdoc
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
== 2.1.0
|
2
|
+
|
3
|
+
* 1 DEPRECATION
|
4
|
+
|
5
|
+
* `search` aliases new `advanced_search` method (same functionality as before), but will
|
6
|
+
alias `basic_search` in 3.0! Should print warnings.
|
7
|
+
|
8
|
+
* 3 new features
|
9
|
+
|
10
|
+
* Generate full text search indexes from a rake task (sort of like in 1.x). Supply a specific
|
11
|
+
model name.
|
12
|
+
* New search methods: `basic_search`, `advanced_search` and `fuzzy_search`. Basic allows special
|
13
|
+
characters like &, and % in search terms. Fuzzy is based on Postgres's trigram matching extension
|
14
|
+
pg_trgm. Advanced is the same functionality from `search` previously.
|
15
|
+
* Rake task that installs pg_trgm now works on Postgres 9.1 and up.
|
16
|
+
|
17
|
+
* 2 dev improvements
|
18
|
+
|
19
|
+
* Test database configuration not automatically generated from a rake task and ignored by git.
|
20
|
+
* New interactive developer console (powered by pry).
|
21
|
+
|
1
22
|
== 2.0.3
|
2
23
|
|
3
24
|
* 1 new feature
|
data/Gemfile
ADDED
data/README.rdoc
CHANGED
@@ -38,6 +38,20 @@ Your models now have access to the search method:
|
|
38
38
|
Game.search_by_title_and_system('Final Fantasy', 'PS2')
|
39
39
|
Game.search_by_title_or_system('Final Fantasy, 'PS3')
|
40
40
|
|
41
|
+
You can use '|' and '&' for logical conditions.
|
42
|
+
|
43
|
+
Game.search_by_title_or_system('Final Fantasy', 'PS3|Xbox')
|
44
|
+
|
45
|
+
=== Setting Language
|
46
|
+
|
47
|
+
To set proper searching dictionary just override class method on your model:
|
48
|
+
|
49
|
+
def self.searchable_language
|
50
|
+
'russian'
|
51
|
+
end
|
52
|
+
|
53
|
+
And all your queries would go right! And don`t forget to change the migration for indexes, like shown below.
|
54
|
+
|
41
55
|
=== Creating Indexes for Super Speed
|
42
56
|
You can have Postgresql use an index for the full-text search. To declare a full-text index, in a
|
43
57
|
migration add code like the following:
|
@@ -50,6 +64,9 @@ In the above example, the table email_logs has two text columns that we search a
|
|
50
64
|
You will need to add an index for every text/string column you query against, or else Postgresql will revert to a
|
51
65
|
full table scan instead of using the indexes.
|
52
66
|
|
67
|
+
If you create these indexes, you should also switch to sql for your schema_format in `config/application.rb`:
|
68
|
+
|
69
|
+
config.active_record.schema_format = :sql
|
53
70
|
|
54
71
|
== REQUIREMENTS:
|
55
72
|
|
data/Rakefile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
|
3
3
|
require 'rake'
|
4
|
+
require 'yaml'
|
4
5
|
require 'pg'
|
5
6
|
require 'active_record'
|
6
7
|
require 'benchmark'
|
@@ -8,29 +9,115 @@ require 'benchmark'
|
|
8
9
|
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/spec')
|
9
10
|
|
10
11
|
task :default do
|
11
|
-
|
12
|
-
if config.match /<username>/
|
13
|
-
print "Would you like to create and configure the test database? y/n "
|
14
|
-
continue = STDIN.getc
|
15
|
-
exit 0 unless continue == "Y" || continue == "y"
|
16
|
-
sh "createdb texticle"
|
17
|
-
File.open(File.expand_path(File.dirname(__FILE__) + '/spec/config.yml'), "w") do |writable_config|
|
18
|
-
writable_config << config.sub(/<username>/, `whoami`.chomp)
|
19
|
-
end
|
20
|
-
Rake::Task["db:migrate"].invoke
|
21
|
-
end
|
12
|
+
Rake::Task["db:setup"].invoke
|
22
13
|
Rake::Task["test"].invoke
|
23
14
|
end
|
24
15
|
|
16
|
+
desc "Fire up an interactive terminal to play with"
|
17
|
+
task :console do
|
18
|
+
require 'pry'
|
19
|
+
require File.expand_path(File.dirname(__FILE__) + '/lib/texticle')
|
20
|
+
|
21
|
+
config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')
|
22
|
+
ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
|
23
|
+
|
24
|
+
class Character < ActiveRecord::Base
|
25
|
+
belongs_to :web_comic
|
26
|
+
end
|
27
|
+
|
28
|
+
class WebComic < ActiveRecord::Base
|
29
|
+
has_many :characters
|
30
|
+
end
|
31
|
+
|
32
|
+
class Game < ActiveRecord::Base
|
33
|
+
end
|
34
|
+
|
35
|
+
# add ability to reload console
|
36
|
+
def reload
|
37
|
+
reload_msg = '# Reloading the console...'
|
38
|
+
puts CodeRay.scan(reload_msg, :ruby).term
|
39
|
+
Pry.save_history
|
40
|
+
exec('rake console')
|
41
|
+
end
|
42
|
+
|
43
|
+
# start the console! :-)
|
44
|
+
welcome = <<-EOS
|
45
|
+
Welcome to the Texticle devloper console. You have some classes you can play with:
|
46
|
+
|
47
|
+
class Character < ActiveRecord::Base
|
48
|
+
# string :name
|
49
|
+
# string :description
|
50
|
+
# integer :web_comic_id
|
51
|
+
|
52
|
+
belongs_to :web_comic
|
53
|
+
end
|
54
|
+
|
55
|
+
class WebComic < ActiveRecord::Base
|
56
|
+
# string :name
|
57
|
+
# string :author
|
58
|
+
# integer :id
|
59
|
+
|
60
|
+
has_many :characters
|
61
|
+
end
|
62
|
+
|
63
|
+
class Game < ActiveRecord::Base
|
64
|
+
# string :system
|
65
|
+
# string :title
|
66
|
+
# text :description
|
67
|
+
end
|
68
|
+
EOS
|
69
|
+
|
70
|
+
puts CodeRay.scan(welcome, :ruby).term
|
71
|
+
Pry.start
|
72
|
+
end
|
73
|
+
|
25
74
|
task :test do
|
26
75
|
require 'texticle_spec'
|
27
76
|
require 'texticle/searchable_spec'
|
77
|
+
require 'texticle/full_text_indexer_spec'
|
28
78
|
end
|
29
79
|
|
30
80
|
namespace :db do
|
81
|
+
desc 'Create and configure the test database'
|
82
|
+
task :setup do
|
83
|
+
spec_directory = "#{File.expand_path(File.dirname(__FILE__))}/spec"
|
84
|
+
|
85
|
+
STDOUT.puts "Detecting database configuration..."
|
86
|
+
|
87
|
+
if File.exists?("#{spec_directory}/config.yml")
|
88
|
+
STDOUT.puts "Configuration detected. Skipping confguration."
|
89
|
+
else
|
90
|
+
STDOUT.puts "Would you like to create and configure the test database? y/N"
|
91
|
+
continue = STDIN.gets.chomp
|
92
|
+
|
93
|
+
unless continue =~ /^[y]$/i
|
94
|
+
STDOUT.puts "Done."
|
95
|
+
exit 0
|
96
|
+
end
|
97
|
+
|
98
|
+
STDOUT.puts "Creating database..."
|
99
|
+
`createdb texticle`
|
100
|
+
|
101
|
+
STDOUT.puts "Writing configuration file..."
|
102
|
+
|
103
|
+
config_example = File.read("#{spec_directory}/config.yml.example")
|
104
|
+
|
105
|
+
File.open("#{spec_directory}/config.yml", "w") do |config|
|
106
|
+
config << config_example.sub(/<username>/, `whoami`.chomp)
|
107
|
+
end
|
108
|
+
|
109
|
+
STDOUT.puts "Running migrations..."
|
110
|
+
Rake::Task["db:migrate"].invoke
|
111
|
+
|
112
|
+
STDOUT.puts 'Done.'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
31
116
|
desc 'Run migrations for test database'
|
32
117
|
task :migrate do
|
33
|
-
|
118
|
+
config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')
|
119
|
+
ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
|
120
|
+
|
34
121
|
ActiveRecord::Migration.instance_eval do
|
35
122
|
create_table :games do |table|
|
36
123
|
table.string :system
|
@@ -38,11 +125,13 @@ namespace :db do
|
|
38
125
|
table.text :description
|
39
126
|
end
|
40
127
|
create_table :web_comics do |table|
|
128
|
+
|
41
129
|
table.string :name
|
42
130
|
table.string :author
|
43
131
|
table.text :review
|
44
132
|
table.integer :id
|
45
133
|
end
|
134
|
+
|
46
135
|
create_table :characters do |table|
|
47
136
|
table.string :name
|
48
137
|
table.string :description
|
@@ -50,9 +139,12 @@ namespace :db do
|
|
50
139
|
end
|
51
140
|
end
|
52
141
|
end
|
142
|
+
|
53
143
|
desc 'Drop tables from test database'
|
54
144
|
task :drop do
|
55
|
-
|
145
|
+
config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')
|
146
|
+
ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
|
147
|
+
|
56
148
|
ActiveRecord::Migration.instance_eval do
|
57
149
|
drop_table :games
|
58
150
|
drop_table :web_comics
|
data/lib/texticle.rb
CHANGED
@@ -1,24 +1,36 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
|
3
|
+
require 'texticle/version'
|
4
|
+
|
3
5
|
module Texticle
|
4
|
-
def
|
5
|
-
|
6
|
-
|
6
|
+
def self.searchable_language
|
7
|
+
'english'
|
8
|
+
end
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
13
|
-
end
|
10
|
+
def search(query = "", exclusive = true)
|
11
|
+
warn "[DEPRECATION] `search` is deprecated. Please use `advanced_search` instead. At the next major release `search` will become an alias for `basic_search`."
|
12
|
+
advanced_search(query, exclusive)
|
13
|
+
end
|
14
14
|
|
15
|
-
|
15
|
+
def basic_search(query = "", exclusive = true)
|
16
|
+
exclusive, query = munge_exclusive_and_query(exclusive, query)
|
17
|
+
parsed_query_hash = parse_query_hash(query)
|
18
|
+
similarities, conditions = basic_similarities_and_conditions(parsed_query_hash)
|
19
|
+
assemble_query(similarities, conditions, exclusive)
|
20
|
+
end
|
16
21
|
|
17
|
-
|
22
|
+
def advanced_search(query = "", exclusive = true)
|
23
|
+
exclusive, query = munge_exclusive_and_query(exclusive, query)
|
24
|
+
parsed_query_hash = parse_query_hash(query)
|
25
|
+
similarities, conditions = advanced_similarities_and_conditions(parsed_query_hash)
|
26
|
+
assemble_query(similarities, conditions, exclusive)
|
27
|
+
end
|
18
28
|
|
19
|
-
|
20
|
-
|
21
|
-
|
29
|
+
def fuzzy_search(query = '', exclusive = true)
|
30
|
+
exclusive, query = munge_exclusive_and_query(exclusive, query)
|
31
|
+
parsed_query_hash = parse_query_hash(query)
|
32
|
+
similarities, conditions = fuzzy_similarities_and_conditions(parsed_query_hash)
|
33
|
+
assemble_query(similarities, conditions, exclusive)
|
22
34
|
end
|
23
35
|
|
24
36
|
def method_missing(method, *search_terms)
|
@@ -31,7 +43,7 @@ module Texticle
|
|
31
43
|
query = columns.inject({}) do |query, column|
|
32
44
|
query.merge column => args.shift
|
33
45
|
end
|
34
|
-
|
46
|
+
self.send(Helper.search_type(method), query, exclusive)
|
35
47
|
end
|
36
48
|
__send__(method, *search_terms, exclusive)
|
37
49
|
else
|
@@ -50,20 +62,93 @@ module Texticle
|
|
50
62
|
|
51
63
|
private
|
52
64
|
|
65
|
+
def munge_exclusive_and_query(exclusive, query)
|
66
|
+
unless query.is_a?(Hash)
|
67
|
+
exclusive = false
|
68
|
+
query = searchable_columns.inject({}) do |terms, column|
|
69
|
+
terms.merge column => query.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
[exclusive, query]
|
74
|
+
end
|
75
|
+
|
53
76
|
def parse_query_hash(query, table_name = quoted_table_name)
|
54
|
-
language = connection.quote(searchable_language)
|
55
77
|
table_name = connection.quote_table_name(table_name)
|
56
78
|
|
79
|
+
results = []
|
80
|
+
|
57
81
|
query.each do |column_or_table, search_term|
|
58
82
|
if search_term.is_a?(Hash)
|
59
|
-
parse_query_hash(search_term, column_or_table)
|
83
|
+
results += parse_query_hash(search_term, column_or_table)
|
60
84
|
else
|
61
85
|
column = connection.quote_column_name(column_or_table)
|
62
86
|
search_term = connection.quote normalize(Helper.normalize(search_term))
|
63
|
-
|
64
|
-
|
87
|
+
|
88
|
+
results << [table_name, column, search_term]
|
65
89
|
end
|
66
90
|
end
|
91
|
+
|
92
|
+
results
|
93
|
+
end
|
94
|
+
|
95
|
+
def basic_similarities_and_conditions(parsed_query_hash)
|
96
|
+
parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
|
97
|
+
similarities << basic_similarity_string(*query_args)
|
98
|
+
conditions << basic_condition_string(*query_args)
|
99
|
+
|
100
|
+
[similarities, conditions]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def basic_similarity_string(table_name, column, search_term)
|
105
|
+
"ts_rank(to_tsvector(#{quoted_language}, #{table_name}.#{column}::text), plainto_tsquery(#{quoted_language}, #{search_term}::text))"
|
106
|
+
end
|
107
|
+
|
108
|
+
def basic_condition_string(table_name, column, search_term)
|
109
|
+
"to_tsvector(#{quoted_language}, #{table_name}.#{column}::text) @@ plainto_tsquery(#{quoted_language}, #{search_term}::text)"
|
110
|
+
end
|
111
|
+
|
112
|
+
def advanced_similarities_and_conditions(parsed_query_hash)
|
113
|
+
parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
|
114
|
+
similarities << advanced_similarity_string(*query_args)
|
115
|
+
conditions << advanced_condition_string(*query_args)
|
116
|
+
|
117
|
+
[similarities, conditions]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def advanced_similarity_string(table_name, column, search_term)
|
122
|
+
"ts_rank(to_tsvector(#{quoted_language}, #{table_name}.#{column}::text), to_tsquery(#{quoted_language}, #{search_term}::text))"
|
123
|
+
end
|
124
|
+
|
125
|
+
def advanced_condition_string(table_name, column, search_term)
|
126
|
+
"to_tsvector(#{quoted_language}, #{table_name}.#{column}::text) @@ to_tsquery(#{quoted_language}, #{search_term}::text)"
|
127
|
+
end
|
128
|
+
|
129
|
+
def fuzzy_similarities_and_conditions(parsed_query_hash)
|
130
|
+
parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
|
131
|
+
similarities << fuzzy_similarity_string(*query_args)
|
132
|
+
conditions << fuzzy_condition_string(*query_args)
|
133
|
+
|
134
|
+
[similarities, conditions]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def fuzzy_similarity_string(table_name, column, search_term)
|
139
|
+
"similarity(#{table_name}.#{column}, #{search_term})"
|
140
|
+
end
|
141
|
+
|
142
|
+
def fuzzy_condition_string(table_name, column, search_term)
|
143
|
+
"(#{table_name}.#{column} % #{search_term})"
|
144
|
+
end
|
145
|
+
|
146
|
+
def assemble_query(similarities, conditions, exclusive)
|
147
|
+
rank = connection.quote_column_name('rank' + rand.to_s)
|
148
|
+
|
149
|
+
select("#{quoted_table_name + '.*,' if scoped.select_values.empty?} #{similarities.join(" + ")} AS #{rank}").
|
150
|
+
where(conditions.join(exclusive ? " AND " : " OR ")).
|
151
|
+
order("#{rank} DESC")
|
67
152
|
end
|
68
153
|
|
69
154
|
def normalize(query)
|
@@ -74,8 +159,12 @@ module Texticle
|
|
74
159
|
columns.select {|column| [:string, :text].include? column.type }.map(&:name)
|
75
160
|
end
|
76
161
|
|
162
|
+
def quoted_language
|
163
|
+
@quoted_language ||= connection.quote(searchable_language)
|
164
|
+
end
|
165
|
+
|
77
166
|
def searchable_language
|
78
|
-
|
167
|
+
Texticle.searchable_language
|
79
168
|
end
|
80
169
|
|
81
170
|
module Helper
|
@@ -84,8 +173,16 @@ module Texticle
|
|
84
173
|
query.to_s.gsub(' ', '\\\\ ')
|
85
174
|
end
|
86
175
|
|
176
|
+
def method_name_regex
|
177
|
+
/^(?<search_type>((basic|advanced|fuzzy)_)?search)_by_(?<columns>[_a-zA-Z]\w*)$/
|
178
|
+
end
|
179
|
+
|
180
|
+
def search_type(method)
|
181
|
+
method.to_s.match(method_name_regex)[:search_type]
|
182
|
+
end
|
183
|
+
|
87
184
|
def exclusive_dynamic_search_columns(method)
|
88
|
-
if match = method.to_s.match(
|
185
|
+
if match = method.to_s.match(method_name_regex)
|
89
186
|
match[:columns].split('_and_')
|
90
187
|
else
|
91
188
|
[]
|
@@ -93,7 +190,7 @@ module Texticle
|
|
93
190
|
end
|
94
191
|
|
95
192
|
def inclusive_dynamic_search_columns(method)
|
96
|
-
if match = method.to_s.match(
|
193
|
+
if match = method.to_s.match(method_name_regex)
|
97
194
|
match[:columns].split('_or_')
|
98
195
|
else
|
99
196
|
[]
|
@@ -127,3 +224,5 @@ module Texticle
|
|
127
224
|
end
|
128
225
|
end
|
129
226
|
end
|
227
|
+
|
228
|
+
require File.expand_path(File.dirname(__FILE__) + '/texticle/full_text_indexer')
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Texticle::FullTextIndexer
|
2
|
+
def generate_migration(model_name)
|
3
|
+
stream_output do |io|
|
4
|
+
io.puts(<<-MIGRATION)
|
5
|
+
class #{model_name}FullTextSearch < ActiveRecord::Migration
|
6
|
+
def self.up
|
7
|
+
execute(<<-SQL.strip)
|
8
|
+
#{up_migration(model_name)}
|
9
|
+
SQL
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.down
|
13
|
+
execute(<<-SQL.strip)
|
14
|
+
#{down_migration(model_name)}
|
15
|
+
SQL
|
16
|
+
end
|
17
|
+
end
|
18
|
+
MIGRATION
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def stream_output(now = Time.now.utc, &block)
|
23
|
+
if !@output_stream && defined?(Rails)
|
24
|
+
File.open(migration_file_name(now), 'w', &block)
|
25
|
+
else
|
26
|
+
@output_stream ||= $stdout
|
27
|
+
|
28
|
+
yield @output_stream
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def migration_file_name(now = Time.now.utc)
|
35
|
+
File.join(Rails.root, 'db', 'migrate',"#{now.strftime('%Y%m%d%H%M%S')}_full_text_search.rb")
|
36
|
+
end
|
37
|
+
|
38
|
+
def up_migration(model_name)
|
39
|
+
migration_with_type(model_name, :up)
|
40
|
+
end
|
41
|
+
|
42
|
+
def down_migration(model_name)
|
43
|
+
migration_with_type(model_name, :down)
|
44
|
+
end
|
45
|
+
|
46
|
+
def migration_with_type(model_name, type)
|
47
|
+
sql_lines = ''
|
48
|
+
|
49
|
+
model = Kernel.const_get(model_name)
|
50
|
+
model.indexable_columns.each do |column|
|
51
|
+
sql_lines << drop_index_sql_for(model, column)
|
52
|
+
sql_lines << create_index_sql_for(model, column) if type == :up
|
53
|
+
end
|
54
|
+
|
55
|
+
sql_lines.strip.gsub("\n","\n ")
|
56
|
+
end
|
57
|
+
|
58
|
+
def drop_index_sql_for(model, column)
|
59
|
+
"DROP index IF EXISTS #{index_name_for(model, column)};\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_index_sql_for(model, column)
|
63
|
+
# The spacing gets sort of wonky in here.
|
64
|
+
|
65
|
+
<<-SQL
|
66
|
+
CREATE index #{index_name_for(model, column)}
|
67
|
+
ON #{model.table_name}
|
68
|
+
USING gin(to_tsvector("#{dictionary}", "#{model.table_name}"."#{column}"::text));
|
69
|
+
SQL
|
70
|
+
end
|
71
|
+
|
72
|
+
def index_name_for(model, column)
|
73
|
+
"#{model.table_name}_#{column}_fts_idx"
|
74
|
+
end
|
75
|
+
|
76
|
+
def dictionary
|
77
|
+
Texticle.searchable_language
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Texticle
|
2
|
+
class PostgresModuleInstaller
|
3
|
+
def install_module(module_name)
|
4
|
+
major, minor, patch = postgres_version.split('.')
|
5
|
+
|
6
|
+
if major.to_i >= 9 && minor.to_i >= 1
|
7
|
+
install_postgres_91_module(module_name)
|
8
|
+
else
|
9
|
+
install_postgres_90_module(module_name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def db_name
|
14
|
+
@db_name ||= ActiveRecord::Base.connection.current_database
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def postgres_version
|
20
|
+
@postgres_version ||= ask_pg_config('version').match(/PostgreSQL ([0-9]+(\.[0-9]+)*)/)[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def postgres_share_dir
|
24
|
+
@share_dir ||= ask_pg_config('sharedir')
|
25
|
+
end
|
26
|
+
|
27
|
+
def ask_pg_config(argument)
|
28
|
+
result = `pg_config --#{argument}`.chomp
|
29
|
+
|
30
|
+
raise RuntimeError, "Cannot find Postgres's #{argument}." unless $?.success?
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def install_postgres_90_module(module_name)
|
36
|
+
module_location = "#{postgres_share_dir}/contrib/#{module_name}.sql"
|
37
|
+
|
38
|
+
unless system("ls #{module_location}")
|
39
|
+
raise RuntimeError, "Cannot find the #{module_name} module. Was it compiled and installed?"
|
40
|
+
end
|
41
|
+
|
42
|
+
unless system("psql -d #{db_name} -f #{module_location}")
|
43
|
+
raise RuntimeError, "`psql -d #{db_name} -f #{module_location}` cannot complete successfully."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def install_postgres_91_module(module_name)
|
48
|
+
module_location = "#{postgres_share_dir}/extension/#{module_name}.control"
|
49
|
+
|
50
|
+
unless system("ls #{module_location}")
|
51
|
+
raise RuntimeError, "Cannot find the #{module_name} module. Was it compiled and installed?"
|
52
|
+
end
|
53
|
+
|
54
|
+
ActiveRecord::Base.connection.execute("CREATE EXTENSION #{module_name};")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/texticle/searchable.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'texticle'
|
3
|
+
|
4
|
+
namespace :texticle do
|
5
|
+
desc 'Create full text search index migration, give the model for which you want to create the indexes'
|
6
|
+
task :create_index_migration, [:model_name] => :environment do |task, args|
|
7
|
+
raise 'A model name is required' unless args[:model_name]
|
8
|
+
Texticle::FullTextIndexer.new.generate_migration(args[:model_name])
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Install trigram text search module"
|
12
|
+
task :install_trigram => [:environment] do
|
13
|
+
installer = Texticle::PostgresModuleInstaller.new
|
14
|
+
installer.install_module('pg_trgm')
|
15
|
+
|
16
|
+
puts "Trigram text search module successfully installed into '#{installer.db_name}' database."
|
17
|
+
end
|
18
|
+
end
|
File without changes
|
data/spec/spec_helper.rb
CHANGED
@@ -3,7 +3,76 @@ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
|
3
3
|
require 'yaml'
|
4
4
|
require 'texticle'
|
5
5
|
require 'shoulda'
|
6
|
-
require '
|
6
|
+
require 'pry'
|
7
|
+
require 'active_record'
|
8
|
+
require 'texticle'
|
9
|
+
require 'texticle/searchable'
|
7
10
|
|
8
11
|
config = YAML.load_file File.expand_path(File.dirname(__FILE__) + '/config.yml')
|
9
12
|
ActiveRecord::Base.establish_connection config.merge(:adapter => :postgresql)
|
13
|
+
|
14
|
+
class ARStandIn < ActiveRecord::Base;
|
15
|
+
self.abstract_class = true
|
16
|
+
extend Texticle
|
17
|
+
end
|
18
|
+
|
19
|
+
class NotThere < ARStandIn; end
|
20
|
+
|
21
|
+
class TexticleWebComic < ARStandIn;
|
22
|
+
has_many :characters, :foreign_key => :web_comic_id
|
23
|
+
self.table_name = :web_comics
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class WebComic < ActiveRecord::Base
|
28
|
+
# string :name
|
29
|
+
# string :author
|
30
|
+
# integer :id
|
31
|
+
|
32
|
+
has_many :characters
|
33
|
+
end
|
34
|
+
|
35
|
+
class WebComicWithSearchable < WebComic
|
36
|
+
extend Searchable
|
37
|
+
end
|
38
|
+
|
39
|
+
class WebComicWithSearchableName < WebComic
|
40
|
+
extend Searchable(:name)
|
41
|
+
end
|
42
|
+
|
43
|
+
class WebComicWithSearchableNameAndAuthor < WebComic
|
44
|
+
extend Searchable(:name, :author)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class Character < ActiveRecord::Base
|
49
|
+
# string :name
|
50
|
+
# string :description
|
51
|
+
# integer :web_comic_id
|
52
|
+
|
53
|
+
belongs_to :web_comic
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class Game < ActiveRecord::Base
|
58
|
+
# string :system
|
59
|
+
# string :title
|
60
|
+
# text :description
|
61
|
+
end
|
62
|
+
|
63
|
+
class GameExtendedWithTexticle < Game
|
64
|
+
extend Texticle
|
65
|
+
end
|
66
|
+
|
67
|
+
class GameExtendedWithTexticleAndCustomLanguage < GameExtendedWithTexticle
|
68
|
+
def searchable_language
|
69
|
+
'spanish'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
class GameFail < Game; end
|
75
|
+
|
76
|
+
class GameFailExtendedWithTexticle < GameFail
|
77
|
+
extend Texticle
|
78
|
+
end
|
@@ -1,60 +1,91 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'fixtures/webcomic'
|
3
2
|
require 'texticle/searchable'
|
4
3
|
|
5
4
|
class SearchableTest < Test::Unit::TestCase
|
6
5
|
context "when extending an ActiveRecord::Base subclass" do
|
7
|
-
setup do
|
8
|
-
@qcont = WebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
|
9
|
-
@jhony = WebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
|
10
|
-
@ddeeg = WebComic.create :name => "Dominic Deegan", :author => "Mookie"
|
11
|
-
@penny = WebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
|
12
|
-
end
|
13
|
-
|
14
|
-
teardown do
|
15
|
-
WebComic.delete_all
|
16
|
-
#Object.send(:remove_const, :WebComic) if defined?(WebComic)
|
17
|
-
end
|
18
|
-
|
19
6
|
context "with no parameters" do
|
20
7
|
setup do
|
21
|
-
|
8
|
+
@qcont = WebComicWithSearchable.create :name => "Questionable Content", :author => "Jeph Jaques"
|
9
|
+
@jhony = WebComicWithSearchable.create :name => "Johnny Wander", :author => "Ananth & Yuko"
|
10
|
+
@ddeeg = WebComicWithSearchable.create :name => "Dominic Deegan", :author => "Mookie"
|
11
|
+
@penny = WebComicWithSearchable.create :name => "Penny Arcade", :author => "Tycho & Gabe"
|
12
|
+
end
|
13
|
+
|
14
|
+
teardown do
|
15
|
+
WebComicWithSearchable.delete_all
|
22
16
|
end
|
23
17
|
|
24
18
|
should "search across all columns" do
|
25
|
-
assert_equal [@penny],
|
26
|
-
assert_equal [@ddeeg],
|
19
|
+
assert_equal [@penny], WebComicWithSearchable.advanced_search("Penny")
|
20
|
+
assert_equal [@ddeeg], WebComicWithSearchable.advanced_search("Dominic")
|
27
21
|
end
|
28
22
|
end
|
29
23
|
|
30
24
|
context "with one column as parameter" do
|
31
25
|
setup do
|
32
|
-
|
26
|
+
@qcont = WebComicWithSearchableName.create :name => "Questionable Content", :author => "Jeph Jaques"
|
27
|
+
@jhony = WebComicWithSearchableName.create :name => "Johnny Wander", :author => "Ananth & Yuko"
|
28
|
+
@ddeeg = WebComicWithSearchableName.create :name => "Dominic Deegan", :author => "Mookie"
|
29
|
+
@penny = WebComicWithSearchableName.create :name => "Penny Arcade", :author => "Tycho & Gabe"
|
30
|
+
end
|
31
|
+
|
32
|
+
teardown do
|
33
|
+
WebComicWithSearchableName.delete_all
|
33
34
|
end
|
34
35
|
|
35
36
|
should "only search across the given column" do
|
36
|
-
assert_equal [@penny],
|
37
|
-
assert_empty
|
37
|
+
assert_equal [@penny], WebComicWithSearchableName.advanced_search("Penny")
|
38
|
+
assert_empty WebComicWithSearchableName.advanced_search("Tycho")
|
39
|
+
end
|
40
|
+
|
41
|
+
["hello \\", "tebow!" , "food &"].each do |search_term|
|
42
|
+
should "be fine with searching for crazy character #{search_term} with plain search" do
|
43
|
+
# Uses plainto_tsquery
|
44
|
+
assert_equal [], WebComicWithSearchableName.basic_search(search_term)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "be not fine with searching for crazy character #{search_term} with advanced search" do
|
48
|
+
# Uses to_tsquery
|
49
|
+
assert_raise(ActiveRecord::StatementInvalid) do
|
50
|
+
WebComicWithSearchableName.advanced_search(search_term).all
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
should "fuzzy search stuff" do
|
56
|
+
assert_equal [@qcont], WebComicWithSearchableName.fuzzy_search('Questio')
|
38
57
|
end
|
39
58
|
|
40
59
|
should "define :searchable_columns as private" do
|
41
|
-
assert_raise(NoMethodError) {
|
60
|
+
assert_raise(NoMethodError) { WebComicWithSearchableName.searchable_columns }
|
42
61
|
begin
|
43
|
-
|
62
|
+
WebComicWithSearchableName.searchable_columns
|
44
63
|
rescue NoMethodError => error
|
45
64
|
assert_match error.message, /private method/
|
46
65
|
end
|
47
66
|
end
|
67
|
+
|
68
|
+
should "define #indexable_columns which returns a write-proof Enumerable" do
|
69
|
+
assert_equal(Enumerator, WebComicWithSearchableName.indexable_columns.class)
|
70
|
+
assert_raise(NoMethodError) { WebComicWithSearchableName.indexable_columns[0] = 'foo' }
|
71
|
+
end
|
48
72
|
end
|
49
73
|
|
50
74
|
context "with two columns as parameters" do
|
51
75
|
setup do
|
52
|
-
|
76
|
+
@qcont = WebComicWithSearchableNameAndAuthor.create :name => "Questionable Content", :author => "Jeph Jaques"
|
77
|
+
@jhony = WebComicWithSearchableNameAndAuthor.create :name => "Johnny Wander", :author => "Ananth & Yuko"
|
78
|
+
@ddeeg = WebComicWithSearchableNameAndAuthor.create :name => "Dominic Deegan", :author => "Mookie"
|
79
|
+
@penny = WebComicWithSearchableNameAndAuthor.create :name => "Penny Arcade", :author => "Tycho & Gabe"
|
80
|
+
end
|
81
|
+
|
82
|
+
teardown do
|
83
|
+
WebComicWithSearchableNameAndAuthor.delete_all
|
53
84
|
end
|
54
85
|
|
55
86
|
should "only search across the given column" do
|
56
|
-
assert_equal [@penny],
|
57
|
-
assert_equal [@penny],
|
87
|
+
assert_equal [@penny], WebComicWithSearchableNameAndAuthor.advanced_search("Penny")
|
88
|
+
assert_equal [@penny], WebComicWithSearchableNameAndAuthor.advanced_search("Tycho")
|
58
89
|
end
|
59
90
|
end
|
60
91
|
end
|
data/spec/texticle_spec.rb
CHANGED
@@ -1,18 +1,11 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
require 'spec_helper'
|
3
|
-
require 'fixtures/webcomic'
|
4
|
-
require 'fixtures/character'
|
5
|
-
require 'fixtures/game'
|
6
3
|
|
7
4
|
class TexticleTest < Test::Unit::TestCase
|
8
5
|
context "after extending ActiveRecord::Base" do
|
9
|
-
# before(:all)
|
10
|
-
ActiveRecord::Base.extend(Texticle)
|
11
|
-
class NotThere < ActiveRecord::Base; end
|
12
|
-
|
13
6
|
should "not break #respond_to?" do
|
14
7
|
assert_nothing_raised do
|
15
|
-
|
8
|
+
ARStandIn.respond_to? :abstract_class?
|
16
9
|
end
|
17
10
|
end
|
18
11
|
|
@@ -24,9 +17,9 @@ class TexticleTest < Test::Unit::TestCase
|
|
24
17
|
end
|
25
18
|
|
26
19
|
should "not break #method_missing" do
|
27
|
-
assert_raise(NoMethodError) {
|
20
|
+
assert_raise(NoMethodError) { ARStandIn.random }
|
28
21
|
begin
|
29
|
-
|
22
|
+
ARStandIn.random
|
30
23
|
rescue NoMethodError => error
|
31
24
|
assert_match error.message, /undefined method `random'/
|
32
25
|
end
|
@@ -44,9 +37,9 @@ class TexticleTest < Test::Unit::TestCase
|
|
44
37
|
|
45
38
|
context "when finding models based on searching a related model" do
|
46
39
|
setup do
|
47
|
-
@qc =
|
48
|
-
@jw =
|
49
|
-
@pa =
|
40
|
+
@qc = TexticleWebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
|
41
|
+
@jw = TexticleWebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
|
42
|
+
@pa = TexticleWebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
|
50
43
|
|
51
44
|
@gabe = @pa.characters.create :name => 'Gabe', :description => 'the simple one'
|
52
45
|
@tycho = @pa.characters.create :name => 'Tycho', :description => 'the wordy one'
|
@@ -63,169 +56,161 @@ class TexticleTest < Test::Unit::TestCase
|
|
63
56
|
end
|
64
57
|
|
65
58
|
teardown do
|
66
|
-
|
59
|
+
TexticleWebComic.delete_all
|
67
60
|
Character.delete_all
|
68
61
|
end
|
69
62
|
|
70
63
|
should "look in the related model with nested searching syntax" do
|
71
|
-
assert_equal [@jw],
|
72
|
-
assert_equal [@pa, @jw, @qc].sort,
|
73
|
-
assert_equal [@pa, @qc].sort,
|
64
|
+
assert_equal [@jw], TexticleWebComic.joins(:characters).advanced_search(:characters => {:description => 'tall'})
|
65
|
+
assert_equal [@pa, @jw, @qc].sort, TexticleWebComic.joins(:characters).advanced_search(:characters => {:description => 'anger'}).sort
|
66
|
+
assert_equal [@pa, @qc].sort, TexticleWebComic.joins(:characters).advanced_search(:characters => {:description => 'crude'}).sort
|
74
67
|
end
|
75
68
|
end
|
76
69
|
end
|
77
70
|
|
78
71
|
context "after extending an ActiveRecord::Base subclass" do
|
79
|
-
# before(:all)
|
80
|
-
class ::GameFail < Game; end
|
81
|
-
|
82
72
|
setup do
|
83
|
-
@zelda =
|
84
|
-
@mario =
|
85
|
-
@sonic =
|
86
|
-
@dkong =
|
87
|
-
@megam =
|
88
|
-
@sfnes =
|
89
|
-
@sfgen =
|
90
|
-
@takun =
|
73
|
+
@zelda = GameExtendedWithTexticle.create :system => "NES", :title => "Legend of Zelda", :description => "A Link to the Past."
|
74
|
+
@mario = GameExtendedWithTexticle.create :system => "NES", :title => "Super Mario Bros.", :description => "The original platformer."
|
75
|
+
@sonic = GameExtendedWithTexticle.create :system => "Genesis", :title => "Sonic the Hedgehog", :description => "Spiky."
|
76
|
+
@dkong = GameExtendedWithTexticle.create :system => "SNES", :title => "Diddy's Kong Quest", :description => "Donkey Kong Country 2"
|
77
|
+
@megam = GameExtendedWithTexticle.create :system => nil, :title => "Mega Man", :description => "Beware Dr. Brain"
|
78
|
+
@sfnes = GameExtendedWithTexticle.create :system => "SNES", :title => "Street Fighter 2", :description => "Yoga Flame!"
|
79
|
+
@sfgen = GameExtendedWithTexticle.create :system => "Genesis", :title => "Street Fighter 2", :description => "Yoga Flame!"
|
80
|
+
@takun = GameExtendedWithTexticle.create :system => "Saturn", :title => "Magical Tarurūto-kun", :description => "カッコイイ!"
|
91
81
|
end
|
92
82
|
|
93
83
|
teardown do
|
94
|
-
|
84
|
+
GameExtendedWithTexticle.delete_all
|
95
85
|
end
|
96
86
|
|
97
87
|
should "not break respond_to? when connection is unavailable" do
|
98
|
-
|
88
|
+
GameFailExtendedWithTexticle.establish_connection({:adapter => :postgresql, :database =>'unavailable', :username=>'bad', :pool=>5, :timeout=>5000}) rescue nil
|
99
89
|
|
100
90
|
assert_nothing_raised do
|
101
|
-
|
91
|
+
GameFailExtendedWithTexticle.respond_to?(:advanced_search)
|
102
92
|
end
|
103
|
-
|
104
93
|
end
|
105
94
|
|
106
95
|
should "define a #search method" do
|
107
|
-
assert
|
96
|
+
assert GameExtendedWithTexticle.respond_to?(:search)
|
108
97
|
end
|
109
98
|
|
110
99
|
context "when searching with a String argument" do
|
111
100
|
should "search across all :string columns if no indexes have been specified" do
|
112
|
-
assert_equal [@mario],
|
113
|
-
assert_equal Set.new([@mario, @zelda]),
|
101
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search("Mario")
|
102
|
+
assert_equal Set.new([@mario, @zelda]), GameExtendedWithTexticle.advanced_search("NES").to_set
|
114
103
|
end
|
115
104
|
|
116
105
|
should "work if the query contains an apostrophe" do
|
117
|
-
assert_equal [@dkong],
|
106
|
+
assert_equal [@dkong], GameExtendedWithTexticle.advanced_search("Diddy's")
|
118
107
|
end
|
119
108
|
|
120
109
|
should "work if the query contains whitespace" do
|
121
|
-
assert_equal [@megam],
|
110
|
+
assert_equal [@megam], GameExtendedWithTexticle.advanced_search("Mega Man")
|
122
111
|
end
|
123
112
|
|
124
113
|
should "work if the query contains an accent" do
|
125
|
-
assert_equal [@takun],
|
114
|
+
assert_equal [@takun], GameExtendedWithTexticle.advanced_search("Tarurūto-kun")
|
126
115
|
end
|
127
116
|
|
128
117
|
should "search across records with NULL values" do
|
129
|
-
assert_equal [@megam],
|
118
|
+
assert_equal [@megam], GameExtendedWithTexticle.advanced_search("Mega")
|
130
119
|
end
|
131
120
|
|
132
121
|
should "scope consecutively" do
|
133
|
-
assert_equal [@sfgen],
|
122
|
+
assert_equal [@sfgen], GameExtendedWithTexticle.advanced_search("Genesis").advanced_search("Street Fighter")
|
134
123
|
end
|
135
124
|
end
|
136
125
|
|
137
126
|
context "when searching with a Hash argument" do
|
138
127
|
should "search across the given columns" do
|
139
|
-
assert_empty
|
140
|
-
assert_empty
|
141
|
-
assert_empty
|
128
|
+
assert_empty GameExtendedWithTexticle.advanced_search(:title => "NES")
|
129
|
+
assert_empty GameExtendedWithTexticle.advanced_search(:system => "Mario")
|
130
|
+
assert_empty GameExtendedWithTexticle.advanced_search(:system => "NES", :title => "Sonic")
|
142
131
|
|
143
|
-
assert_equal [@mario],
|
132
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search(:title => "Mario")
|
144
133
|
|
145
|
-
assert_equal 2,
|
134
|
+
assert_equal 2, GameExtendedWithTexticle.advanced_search(:system => "NES").count
|
146
135
|
|
147
|
-
assert_equal [@zelda],
|
148
|
-
assert_equal [@megam],
|
136
|
+
assert_equal [@zelda], GameExtendedWithTexticle.advanced_search(:system => "NES", :title => "Zelda")
|
137
|
+
assert_equal [@megam], GameExtendedWithTexticle.advanced_search(:title => "Mega")
|
149
138
|
end
|
150
139
|
|
151
140
|
should "scope consecutively" do
|
152
|
-
assert_equal [@sfgen],
|
141
|
+
assert_equal [@sfgen], GameExtendedWithTexticle.advanced_search(:system => "Genesis").advanced_search(:title => "Street Fighter")
|
153
142
|
end
|
154
143
|
|
155
144
|
should "cast non-:string columns as text" do
|
156
|
-
assert_equal [@mario],
|
145
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search(:id => @mario.id)
|
157
146
|
end
|
158
147
|
end
|
159
148
|
|
160
149
|
context "when using dynamic search methods" do
|
161
150
|
should "generate methods for each :string column" do
|
162
|
-
assert_equal [@mario],
|
163
|
-
assert_equal [@takun],
|
151
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_title("Mario")
|
152
|
+
assert_equal [@takun], GameExtendedWithTexticle.advanced_search_by_system("Saturn")
|
164
153
|
end
|
165
154
|
|
166
155
|
should "generate methods for each :text column" do
|
167
|
-
assert_equal [@mario],
|
156
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_description("platform")
|
168
157
|
end
|
169
158
|
|
170
159
|
should "generate methods for any combination of :string and :text columns" do
|
171
|
-
assert_equal [@mario],
|
172
|
-
assert_equal [@sonic],
|
173
|
-
assert_equal [@mario],
|
174
|
-
assert_equal [@megam],
|
160
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_title_and_system("Mario", "NES")
|
161
|
+
assert_equal [@sonic], GameExtendedWithTexticle.advanced_search_by_system_and_title("Genesis", "Sonic")
|
162
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_title_and_title("Mario", "Mario")
|
163
|
+
assert_equal [@megam], GameExtendedWithTexticle.advanced_search_by_title_and_description("Man", "Brain")
|
175
164
|
end
|
176
165
|
|
177
166
|
should "generate methods for inclusive searches" do
|
178
|
-
assert_equal Set.new([@megam, @takun]),
|
167
|
+
assert_equal Set.new([@megam, @takun]), GameExtendedWithTexticle.advanced_search_by_system_or_title("Saturn", "Mega Man").to_set
|
179
168
|
end
|
180
169
|
|
181
170
|
should "scope consecutively" do
|
182
|
-
assert_equal [@sfgen],
|
171
|
+
assert_equal [@sfgen], GameExtendedWithTexticle.advanced_search_by_system("Genesis").advanced_search_by_title("Street Fighter")
|
183
172
|
end
|
184
173
|
|
185
174
|
should "generate methods for non-:string columns" do
|
186
|
-
assert_equal [@mario],
|
175
|
+
assert_equal [@mario], GameExtendedWithTexticle.advanced_search_by_id(@mario.id)
|
187
176
|
end
|
188
177
|
|
189
178
|
should "work with #respond_to?" do
|
190
|
-
assert
|
191
|
-
assert
|
192
|
-
assert
|
193
|
-
assert
|
194
|
-
assert
|
195
|
-
assert
|
179
|
+
assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_system)
|
180
|
+
assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_title)
|
181
|
+
assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_system_and_title)
|
182
|
+
assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_system_or_title)
|
183
|
+
assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_title_and_title_and_title)
|
184
|
+
assert GameExtendedWithTexticle.respond_to?(:advanced_search_by_id)
|
196
185
|
|
197
|
-
assert !
|
186
|
+
assert !GameExtendedWithTexticle.respond_to?(:advanced_search_by_title_and_title_or_title)
|
198
187
|
end
|
199
188
|
|
200
189
|
should "allow for 2 arguments to #respond_to?" do
|
201
|
-
assert
|
190
|
+
assert GameExtendedWithTexticle.respond_to?(:normalize, true)
|
202
191
|
end
|
203
192
|
end
|
204
193
|
|
205
194
|
context "when searching after selecting columns to return" do
|
206
195
|
should "not fetch extra columns" do
|
207
196
|
assert_raise(ActiveModel::MissingAttributeError) do
|
208
|
-
|
197
|
+
GameExtendedWithTexticle.select(:title).advanced_search("Mario").first.system
|
209
198
|
end
|
210
199
|
end
|
211
200
|
end
|
212
|
-
end
|
213
|
-
|
214
|
-
context "when setting a custom search language" do
|
215
|
-
def Game.searchable_language
|
216
|
-
'spanish'
|
217
|
-
end
|
218
201
|
|
219
|
-
|
220
|
-
|
221
|
-
|
202
|
+
context "when setting a custom search language" do
|
203
|
+
setup do
|
204
|
+
GameExtendedWithTexticleAndCustomLanguage.create :system => "PS3", :title => "Harry Potter & the Deathly Hallows"
|
205
|
+
end
|
222
206
|
|
223
|
-
|
224
|
-
|
225
|
-
|
207
|
+
teardown do
|
208
|
+
GameExtendedWithTexticleAndCustomLanguage.delete_all
|
209
|
+
end
|
226
210
|
|
227
|
-
|
228
|
-
|
211
|
+
should "still find results" do
|
212
|
+
assert_not_empty GameExtendedWithTexticleAndCustomLanguage.advanced_search_by_title("harry")
|
213
|
+
end
|
229
214
|
end
|
230
215
|
end
|
231
216
|
end
|
metadata
CHANGED
@@ -1,20 +1,21 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: texticle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
+
- Ben Hamill
|
8
9
|
- ecin
|
9
10
|
- Aaron Patterson
|
10
11
|
autorequire:
|
11
12
|
bindir: bin
|
12
13
|
cert_chain: []
|
13
|
-
date:
|
14
|
+
date: 2013-01-12 00:00:00.000000000 Z
|
14
15
|
dependencies:
|
15
16
|
- !ruby/object:Gem::Dependency
|
16
17
|
name: pg
|
17
|
-
requirement:
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
18
19
|
none: false
|
19
20
|
requirements:
|
20
21
|
- - ~>
|
@@ -22,10 +23,15 @@ dependencies:
|
|
22
23
|
version: 0.11.0
|
23
24
|
type: :development
|
24
25
|
prerelease: false
|
25
|
-
version_requirements:
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 0.11.0
|
26
32
|
- !ruby/object:Gem::Dependency
|
27
33
|
name: shoulda
|
28
|
-
requirement:
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
29
35
|
none: false
|
30
36
|
requirements:
|
31
37
|
- - ~>
|
@@ -33,32 +39,63 @@ dependencies:
|
|
33
39
|
version: 2.11.3
|
34
40
|
type: :development
|
35
41
|
prerelease: false
|
36
|
-
version_requirements:
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.11.3
|
37
48
|
- !ruby/object:Gem::Dependency
|
38
49
|
name: rake
|
39
|
-
requirement:
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
40
51
|
none: false
|
41
52
|
requirements:
|
42
53
|
- - ~>
|
43
54
|
- !ruby/object:Gem::Version
|
44
|
-
version: 0.
|
55
|
+
version: 0.9.0
|
45
56
|
type: :development
|
46
57
|
prerelease: false
|
47
|
-
version_requirements:
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: ruby-debug19
|
50
|
-
requirement: &70337748618760 !ruby/object:Gem::Requirement
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
59
|
none: false
|
52
60
|
requirements:
|
53
61
|
- - ~>
|
54
62
|
- !ruby/object:Gem::Version
|
55
|
-
version: 0.
|
63
|
+
version: 0.9.0
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: pry
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
56
72
|
type: :development
|
57
73
|
prerelease: false
|
58
|
-
version_requirements:
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: pry-doc
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
59
96
|
- !ruby/object:Gem::Dependency
|
60
97
|
name: activerecord
|
61
|
-
requirement:
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
62
99
|
none: false
|
63
100
|
requirements:
|
64
101
|
- - ~>
|
@@ -66,10 +103,16 @@ dependencies:
|
|
66
103
|
version: '3.0'
|
67
104
|
type: :runtime
|
68
105
|
prerelease: false
|
69
|
-
version_requirements:
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ~>
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '3.0'
|
70
112
|
description: ! "Texticle exposes full text search capabilities from PostgreSQL, extending\n
|
71
113
|
\ ActiveRecord with scopes making search easy and fun!"
|
72
114
|
email:
|
115
|
+
- git-commits@benhamill.com
|
73
116
|
- ecin@copypastel.com
|
74
117
|
executables: []
|
75
118
|
extensions: []
|
@@ -79,18 +122,26 @@ extra_rdoc_files:
|
|
79
122
|
- README.rdoc
|
80
123
|
files:
|
81
124
|
- CHANGELOG.rdoc
|
125
|
+
- Gemfile
|
82
126
|
- Manifest.txt
|
83
127
|
- README.rdoc
|
84
128
|
- Rakefile
|
85
129
|
- lib/texticle.rb
|
86
|
-
- lib/texticle/
|
130
|
+
- lib/texticle/full_text_indexer.rb
|
87
131
|
- lib/texticle/rails.rb
|
132
|
+
- lib/texticle/searchable.rb
|
133
|
+
- lib/texticle/tasks.rb
|
134
|
+
- lib/texticle/postgres_module_installer.rb
|
135
|
+
- spec/config.yml.example
|
136
|
+
- spec/fixtures/character.rb
|
137
|
+
- spec/fixtures/game.rb
|
138
|
+
- spec/fixtures/webcomic.rb
|
88
139
|
- spec/spec_helper.rb
|
89
|
-
- spec/texticle_spec.rb
|
90
140
|
- spec/texticle/searchable_spec.rb
|
91
|
-
- spec/
|
92
|
-
homepage: http://
|
93
|
-
licenses:
|
141
|
+
- spec/texticle_spec.rb
|
142
|
+
homepage: http://texticle.github.com/texticle
|
143
|
+
licenses:
|
144
|
+
- MIT
|
94
145
|
post_install_message:
|
95
146
|
rdoc_options:
|
96
147
|
- --main
|
@@ -110,12 +161,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
161
|
- !ruby/object:Gem::Version
|
111
162
|
version: '0'
|
112
163
|
requirements: []
|
113
|
-
rubyforge_project:
|
114
|
-
rubygems_version: 1.8.
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 1.8.23
|
115
166
|
signing_key:
|
116
167
|
specification_version: 3
|
117
168
|
summary: Texticle exposes full text search capabilities from PostgreSQL
|
118
169
|
test_files:
|
170
|
+
- spec/config.yml.example
|
171
|
+
- spec/fixtures/character.rb
|
172
|
+
- spec/fixtures/game.rb
|
173
|
+
- spec/fixtures/webcomic.rb
|
119
174
|
- spec/spec_helper.rb
|
175
|
+
- spec/texticle/searchable_spec.rb
|
120
176
|
- spec/texticle_spec.rb
|
121
|
-
|
177
|
+
has_rdoc:
|