texticle 2.0.2 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,22 @@
1
+ == 2.0.3
2
+
3
+ * 1 new feature
4
+
5
+ * Allow searching through relations. Model.join(:relation).search(:relation => {:column => "query"})
6
+ works, and reduces the need for multi-model tables. Huge thanks to Ben Hamill for the pull request.
7
+ * Allow searching through all model columns irrespective of the column's type; we cast all columns to text
8
+ in the search query. Performance may degrade when searching through anything but a string column.
9
+
10
+ * 2 bugfixes
11
+
12
+ * Fix exceptions when adding Texticle to a table-less model.
13
+ * Column names in a search query are now scoped to the current table.
14
+
15
+ * 1 dev improvement
16
+
17
+ * Running `rake` from the project root will setup the test environment by creating a test database
18
+ and running the necessary migrations. `rake` can also be used to run all the project tests.
19
+
1
20
  == 2.0.2
2
21
 
3
22
  * 1 bugfix
@@ -25,7 +44,7 @@
25
44
 
26
45
  require 'texticle/searchable'
27
46
  class Game
28
- include Searchable(:title)
47
+ extend Searchable(:title)
29
48
  end
30
49
 
31
50
  This also allows Texticle use in Rails without having #search available to all models:
@@ -38,6 +38,19 @@ 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
+ === Creating Indexes for Super Speed
42
+ You can have Postgresql use an index for the full-text search. To declare a full-text index, in a
43
+ migration add code like the following:
44
+
45
+ execute "
46
+ create index on email_logs using gin(to_tsvector('english', subject));
47
+ create index on email_logs using gin(to_tsvector('english', email_address));"
48
+
49
+ In the above example, the table email_logs has two text columns that we search against, subject and email_address.
50
+ You will need to add an index for every text/string column you query against, or else Postgresql will revert to a
51
+ full table scan instead of using the indexes.
52
+
53
+
41
54
  == REQUIREMENTS:
42
55
 
43
56
  * ActiveRecord
data/Rakefile CHANGED
@@ -5,27 +5,58 @@ require 'pg'
5
5
  require 'active_record'
6
6
  require 'benchmark'
7
7
 
8
- require File.expand_path(File.dirname(__FILE__) + '/spec/spec_helper')
8
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/spec')
9
+
10
+ task :default do
11
+ config = File.open(File.expand_path(File.dirname(__FILE__) + '/spec/config.yml')).read
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
22
+ Rake::Task["test"].invoke
23
+ end
24
+
25
+ task :test do
26
+ require 'texticle_spec'
27
+ require 'texticle/searchable_spec'
28
+ end
9
29
 
10
30
  namespace :db do
11
31
  desc 'Run migrations for test database'
12
32
  task :migrate do
33
+ require 'spec_helper'
13
34
  ActiveRecord::Migration.instance_eval do
14
35
  create_table :games do |table|
15
36
  table.string :system
16
37
  table.string :title
38
+ table.text :description
17
39
  end
18
40
  create_table :web_comics do |table|
19
41
  table.string :name
20
42
  table.string :author
43
+ table.text :review
44
+ table.integer :id
45
+ end
46
+ create_table :characters do |table|
47
+ table.string :name
48
+ table.string :description
49
+ table.integer :web_comic_id
21
50
  end
22
51
  end
23
52
  end
24
53
  desc 'Drop tables from test database'
25
54
  task :drop do
55
+ require 'spec_helper'
26
56
  ActiveRecord::Migration.instance_eval do
27
57
  drop_table :games
28
58
  drop_table :web_comics
59
+ drop_table :characters
29
60
  end
30
61
  end
31
62
  end
@@ -1,9 +1,9 @@
1
1
  require 'active_record'
2
2
 
3
3
  module Texticle
4
-
5
4
  def search(query = "", exclusive = true)
6
- language = connection.quote(searchable_language)
5
+ @similarities = []
6
+ @conditions = []
7
7
 
8
8
  unless query.is_a?(Hash)
9
9
  exclusive = false
@@ -12,20 +12,12 @@ module Texticle
12
12
  end
13
13
  end
14
14
 
15
- similarities = []
16
- conditions = []
17
-
18
- query.each do |column, search_term|
19
- column = connection.quote_column_name(column)
20
- search_term = connection.quote normalize(Helper.normalize(search_term))
21
- similarities << "ts_rank(to_tsvector(#{language}, #{quoted_table_name}.#{column}), to_tsquery(#{language}, #{search_term}))"
22
- conditions << "to_tsvector(#{language}, #{column}) @@ to_tsquery(#{language}, #{search_term})"
23
- end
15
+ parse_query_hash(query)
24
16
 
25
17
  rank = connection.quote_column_name('rank' + rand.to_s)
26
18
 
27
- select("#{quoted_table_name + '.*,' if scoped.select_values.empty?} #{similarities.join(" + ")} AS #{rank}").
28
- where(conditions.join(exclusive ? " AND " : " OR ")).
19
+ select("#{quoted_table_name + '.*,' if scoped.select_values.empty?} #{@similarities.join(" + ")} AS #{rank}").
20
+ where(@conditions.join(exclusive ? " AND " : " OR ")).
29
21
  order("#{rank} DESC")
30
22
  end
31
23
 
@@ -45,23 +37,41 @@ module Texticle
45
37
  else
46
38
  super
47
39
  end
40
+ rescue ActiveRecord::StatementInvalid
41
+ super
48
42
  end
49
43
 
50
44
  def respond_to?(method, include_private = false)
51
45
  return super if self == ActiveRecord::Base
52
46
  Helper.dynamic_search_method?(method, self.columns) or super
53
- rescue ActiveRecord::StatementInvalid
47
+ rescue StandardError
54
48
  super
55
49
  end
56
50
 
57
51
  private
58
52
 
53
+ def parse_query_hash(query, table_name = quoted_table_name)
54
+ language = connection.quote(searchable_language)
55
+ table_name = connection.quote_table_name(table_name)
56
+
57
+ query.each do |column_or_table, search_term|
58
+ if search_term.is_a?(Hash)
59
+ parse_query_hash(search_term, column_or_table)
60
+ else
61
+ column = connection.quote_column_name(column_or_table)
62
+ search_term = connection.quote normalize(Helper.normalize(search_term))
63
+ @similarities << "ts_rank(to_tsvector(#{language}, #{table_name}.#{column}::text), to_tsquery(#{language}, #{search_term}::text))"
64
+ @conditions << "to_tsvector(#{language}, #{table_name}.#{column}::text) @@ to_tsquery(#{language}, #{search_term}::text)"
65
+ end
66
+ end
67
+ end
68
+
59
69
  def normalize(query)
60
70
  query
61
71
  end
62
72
 
63
73
  def searchable_columns
64
- columns.select {|column| column.type == :string }.map(&:name)
74
+ columns.select {|column| [:string, :text].include? column.type }.map(&:name)
65
75
  end
66
76
 
67
77
  def searchable_language
@@ -91,7 +101,7 @@ module Texticle
91
101
  end
92
102
 
93
103
  def exclusive_dynamic_search_method?(method, class_columns)
94
- string_columns = class_columns.select {|column| column.type == :string }.map(&:name)
104
+ string_columns = class_columns.map(&:name)
95
105
  columns = exclusive_dynamic_search_columns(method)
96
106
  unless columns.empty?
97
107
  columns.all? {|column| string_columns.include?(column) }
@@ -101,7 +111,7 @@ module Texticle
101
111
  end
102
112
 
103
113
  def inclusive_dynamic_search_method?(method, class_columns)
104
- string_columns = class_columns.select {|column| column.type == :string }.map(&:name)
114
+ string_columns = class_columns.map(&:name)
105
115
  columns = inclusive_dynamic_search_columns(method)
106
116
  unless columns.empty?
107
117
  columns.all? {|column| string_columns.include?(column) }
@@ -116,5 +126,4 @@ module Texticle
116
126
  end
117
127
  end
118
128
  end
119
-
120
129
  end
@@ -5,11 +5,11 @@ def Searchable(*searchable_columns)
5
5
 
6
6
  include Texticle
7
7
 
8
- private
9
-
10
8
  define_method(:searchable_columns) do
11
9
  searchable_columns.map(&:to_s)
12
10
  end
11
+
12
+ private :searchable_columns
13
13
  end
14
14
  end
15
15
 
@@ -1,16 +1,11 @@
1
1
  require 'spec_helper'
2
+ require 'fixtures/webcomic'
2
3
  require 'texticle/searchable'
3
4
 
4
- class WebComic < ActiveRecord::Base
5
- # string :name
6
- # string :author
7
- end
8
-
9
5
  class SearchableTest < Test::Unit::TestCase
10
-
11
6
  context "when extending an ActiveRecord::Base subclass" do
12
7
  setup do
13
- @qcont = WebComic.create :name => "Questionable Content", :author => "Jeff Jaques"
8
+ @qcont = WebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
14
9
  @jhony = WebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
15
10
  @ddeeg = WebComic.create :name => "Dominic Deegan", :author => "Mookie"
16
11
  @penny = WebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
@@ -18,11 +13,12 @@ class SearchableTest < Test::Unit::TestCase
18
13
 
19
14
  teardown do
20
15
  WebComic.delete_all
16
+ #Object.send(:remove_const, :WebComic) if defined?(WebComic)
21
17
  end
22
18
 
23
- context "with no paramters" do
19
+ context "with no parameters" do
24
20
  setup do
25
- WebComic.extend(Searchable)
21
+ WebComic.extend Searchable
26
22
  end
27
23
 
28
24
  should "search across all columns" do
@@ -33,18 +29,27 @@ class SearchableTest < Test::Unit::TestCase
33
29
 
34
30
  context "with one column as parameter" do
35
31
  setup do
36
- WebComic.extend(Searchable(:name))
32
+ WebComic.extend Searchable(:name)
37
33
  end
38
34
 
39
35
  should "only search across the given column" do
40
36
  assert_equal [@penny], WebComic.search("Penny")
41
37
  assert_empty WebComic.search("Tycho")
42
38
  end
39
+
40
+ should "define :searchable_columns as private" do
41
+ assert_raise(NoMethodError) { WebComic.searchable_columns }
42
+ begin
43
+ WebComic.searchable_columns
44
+ rescue NoMethodError => error
45
+ assert_match error.message, /private method/
46
+ end
47
+ end
43
48
  end
44
49
 
45
50
  context "with two columns as parameters" do
46
51
  setup do
47
- WebComic.extend(Searchable(:name, :author))
52
+ WebComic.extend Searchable(:name, :author)
48
53
  end
49
54
 
50
55
  should "only search across the given column" do
@@ -53,5 +58,4 @@ class SearchableTest < Test::Unit::TestCase
53
58
  end
54
59
  end
55
60
  end
56
-
57
61
  end
@@ -1,25 +1,14 @@
1
1
  # coding: utf-8
2
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 NotThere < ActiveRecord::Base
14
-
15
- end
3
+ require 'fixtures/webcomic'
4
+ require 'fixtures/character'
5
+ require 'fixtures/game'
16
6
 
17
7
  class TexticleTest < Test::Unit::TestCase
18
-
19
8
  context "after extending ActiveRecord::Base" do
20
- setup do
21
- ActiveRecord::Base.extend(Texticle)
22
- end
9
+ # before(:all)
10
+ ActiveRecord::Base.extend(Texticle)
11
+ class NotThere < ActiveRecord::Base; end
23
12
 
24
13
  should "not break #respond_to?" do
25
14
  assert_nothing_raised do
@@ -35,31 +24,85 @@ class TexticleTest < Test::Unit::TestCase
35
24
  end
36
25
 
37
26
  should "not break #method_missing" do
27
+ assert_raise(NoMethodError) { ActiveRecord::Base.random }
38
28
  begin
39
29
  ActiveRecord::Base.random
40
30
  rescue NoMethodError => error
41
31
  assert_match error.message, /undefined method `random'/
42
32
  end
43
33
  end
34
+
35
+ should "not break #method_missing for table-less classes" do
36
+ assert !NotThere.table_exists?
37
+ assert_raise(NoMethodError) { NotThere.random }
38
+ begin
39
+ NotThere.random
40
+ rescue NoMethodError => error
41
+ assert_match error.message, /undefined method `random'/
42
+ end
43
+ end
44
+
45
+ context "when finding models based on searching a related model" do
46
+ setup do
47
+ @qc = WebComic.create :name => "Questionable Content", :author => "Jeph Jaques"
48
+ @jw = WebComic.create :name => "Johnny Wander", :author => "Ananth & Yuko"
49
+ @pa = WebComic.create :name => "Penny Arcade", :author => "Tycho & Gabe"
50
+
51
+ @gabe = @pa.characters.create :name => 'Gabe', :description => 'the simple one'
52
+ @tycho = @pa.characters.create :name => 'Tycho', :description => 'the wordy one'
53
+ @div = @pa.characters.create :name => 'Div', :description => 'a crude divx player with anger management issues'
54
+
55
+ @martin = @qc.characters.create :name => 'Martin', :description => 'the insecure protagonist'
56
+ @faye = @qc.characters.create :name => 'Faye', :description => 'a sarcastic barrista with anger management issues'
57
+ @pintsize = @qc.characters.create :name => 'Pintsize', :description => 'a crude AnthroPC'
58
+
59
+ @ananth = @jw.characters.create :name => 'Ananth', :description => 'Stubble! What is under that hat?!?'
60
+ @yuko = @jw.characters.create :name => 'Yuko', :description => 'So... small. Carl Sagan haircut.'
61
+ @john = @jw.characters.create :name => 'John', :description => 'Tall. Anger issues?'
62
+ @cricket = @jw.characters.create :name => 'Cricket', :description => 'Chirrup!'
63
+ end
64
+
65
+ teardown do
66
+ WebComic.delete_all
67
+ Character.delete_all
68
+ end
69
+
70
+ should "look in the related model with nested searching syntax" do
71
+ assert_equal [@jw], WebComic.joins(:characters).search(:characters => {:description => 'tall'})
72
+ assert_equal [@pa, @jw, @qc].sort, WebComic.joins(:characters).search(:characters => {:description => 'anger'}).sort
73
+ assert_equal [@pa, @qc].sort, WebComic.joins(:characters).search(:characters => {:description => 'crude'}).sort
74
+ end
75
+ end
44
76
  end
45
77
 
46
78
  context "after extending an ActiveRecord::Base subclass" do
79
+ # before(:all)
80
+ class ::GameFail < Game; end
81
+
47
82
  setup do
48
- Game.extend(Texticle)
49
- @zelda = Game.create :system => "NES", :title => "Legend of Zelda"
50
- @mario = Game.create :system => "NES", :title => "Super Mario Bros."
51
- @sonic = Game.create :system => "Genesis", :title => "Sonic the Hedgehog"
52
- @dkong = Game.create :system => "SNES", :title => "Diddy's Kong Quest"
53
- @megam = Game.create :system => nil, :title => "Mega Man"
54
- @sfnes = Game.create :system => "SNES", :title => "Street Fighter 2"
55
- @sfgen = Game.create :system => "Genesis", :title => "Street Fighter 2"
56
- @takun = Game.create :system => "Saturn", :title => "Magical Tarurūto-kun"
83
+ @zelda = Game.create :system => "NES", :title => "Legend of Zelda", :description => "A Link to the Past."
84
+ @mario = Game.create :system => "NES", :title => "Super Mario Bros.", :description => "The original platformer."
85
+ @sonic = Game.create :system => "Genesis", :title => "Sonic the Hedgehog", :description => "Spiky."
86
+ @dkong = Game.create :system => "SNES", :title => "Diddy's Kong Quest", :description => "Donkey Kong Country 2"
87
+ @megam = Game.create :system => nil, :title => "Mega Man", :description => "Beware Dr. Brain"
88
+ @sfnes = Game.create :system => "SNES", :title => "Street Fighter 2", :description => "Yoga Flame!"
89
+ @sfgen = Game.create :system => "Genesis", :title => "Street Fighter 2", :description => "Yoga Flame!"
90
+ @takun = Game.create :system => "Saturn", :title => "Magical Tarurūto-kun", :description => "カッコイイ!"
57
91
  end
58
92
 
59
93
  teardown do
60
94
  Game.delete_all
61
95
  end
62
96
 
97
+ should "not break respond_to? when connection is unavailable" do
98
+ GameFail.establish_connection({:adapter => :postgresql, :database =>'unavailable', :username=>'bad', :pool=>5, :timeout=>5000}) rescue nil
99
+
100
+ assert_nothing_raised do
101
+ GameFail.respond_to?(:search)
102
+ end
103
+
104
+ end
105
+
63
106
  should "define a #search method" do
64
107
  assert Game.respond_to?(:search)
65
108
  end
@@ -108,6 +151,10 @@ class TexticleTest < Test::Unit::TestCase
108
151
  should "scope consecutively" do
109
152
  assert_equal [@sfgen], Game.search(:system => "Genesis").search(:title => "Street Fighter")
110
153
  end
154
+
155
+ should "cast non-:string columns as text" do
156
+ assert_equal [@mario], Game.search(:id => @mario.id)
157
+ end
111
158
  end
112
159
 
113
160
  context "when using dynamic search methods" do
@@ -116,10 +163,15 @@ class TexticleTest < Test::Unit::TestCase
116
163
  assert_equal [@takun], Game.search_by_system("Saturn")
117
164
  end
118
165
 
119
- should "generate methods for any combination of :string columns" do
166
+ should "generate methods for each :text column" do
167
+ assert_equal [@mario], Game.search_by_description("platform")
168
+ end
169
+
170
+ should "generate methods for any combination of :string and :text columns" do
120
171
  assert_equal [@mario], Game.search_by_title_and_system("Mario", "NES")
121
172
  assert_equal [@sonic], Game.search_by_system_and_title("Genesis", "Sonic")
122
173
  assert_equal [@mario], Game.search_by_title_and_title("Mario", "Mario")
174
+ assert_equal [@megam], Game.search_by_title_and_description("Man", "Brain")
123
175
  end
124
176
 
125
177
  should "generate methods for inclusive searches" do
@@ -130,8 +182,8 @@ class TexticleTest < Test::Unit::TestCase
130
182
  assert_equal [@sfgen], Game.search_by_system("Genesis").search_by_title("Street Fighter")
131
183
  end
132
184
 
133
- should "not generate methods for non-:string columns" do
134
- assert_raise(NoMethodError) { Game.search_by_id }
185
+ should "generate methods for non-:string columns" do
186
+ assert_equal [@mario], Game.search_by_id(@mario.id)
135
187
  end
136
188
 
137
189
  should "work with #respond_to?" do
@@ -140,8 +192,8 @@ class TexticleTest < Test::Unit::TestCase
140
192
  assert Game.respond_to?(:search_by_system_and_title)
141
193
  assert Game.respond_to?(:search_by_system_or_title)
142
194
  assert Game.respond_to?(:search_by_title_and_title_and_title)
195
+ assert Game.respond_to?(:search_by_id)
143
196
 
144
- assert !Game.respond_to?(:search_by_id)
145
197
  assert !Game.respond_to?(:search_by_title_and_title_or_title)
146
198
  end
147
199
 
@@ -160,24 +212,20 @@ class TexticleTest < Test::Unit::TestCase
160
212
  end
161
213
 
162
214
  context "when setting a custom search language" do
215
+ def Game.searchable_language
216
+ 'spanish'
217
+ end
218
+
163
219
  setup do
164
- def Game.searchable_language
165
- 'spanish'
166
- end
167
220
  Game.create :system => "PS3", :title => "Harry Potter & the Deathly Hallows"
168
221
  end
169
222
 
170
223
  teardown do
171
- def Game.searchable_language
172
- 'english'
173
- end
174
224
  Game.delete_all
175
225
  end
176
226
 
177
227
  should "still find results" do
178
228
  assert_not_empty Game.search_by_title("harry")
179
- p
180
229
  end
181
230
  end
182
-
183
231
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: texticle
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,42 +10,63 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2011-07-10 00:00:00.000000000 -04:00
14
- default_executable:
13
+ date: 2011-08-30 00:00:00.000000000 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: pg
18
- requirement: &2156472340 !ruby/object:Gem::Requirement
17
+ requirement: &70337748621180 !ruby/object:Gem::Requirement
19
18
  none: false
20
19
  requirements:
21
- - - ! '>='
20
+ - - ~>
22
21
  - !ruby/object:Gem::Version
23
22
  version: 0.11.0
24
23
  type: :development
25
24
  prerelease: false
26
- version_requirements: *2156472340
25
+ version_requirements: *70337748621180
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: shoulda
29
- requirement: &2156471820 !ruby/object:Gem::Requirement
28
+ requirement: &70337748620400 !ruby/object:Gem::Requirement
30
29
  none: false
31
30
  requirements:
32
- - - ! '>='
31
+ - - ~>
33
32
  - !ruby/object:Gem::Version
34
33
  version: 2.11.3
35
34
  type: :development
36
35
  prerelease: false
37
- version_requirements: *2156471820
36
+ version_requirements: *70337748620400
37
+ - !ruby/object:Gem::Dependency
38
+ name: rake
39
+ requirement: &70337748619540 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 0.8.0
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *70337748619540
48
+ - !ruby/object:Gem::Dependency
49
+ name: ruby-debug19
50
+ requirement: &70337748618760 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 0.11.6
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *70337748618760
38
59
  - !ruby/object:Gem::Dependency
39
60
  name: activerecord
40
- requirement: &2156471320 !ruby/object:Gem::Requirement
61
+ requirement: &70337748617780 !ruby/object:Gem::Requirement
41
62
  none: false
42
63
  requirements:
43
- - - ! '>='
64
+ - - ~>
44
65
  - !ruby/object:Gem::Version
45
- version: 3.0.0
66
+ version: '3.0'
46
67
  type: :runtime
47
68
  prerelease: false
48
- version_requirements: *2156471320
69
+ version_requirements: *70337748617780
49
70
  description: ! "Texticle exposes full text search capabilities from PostgreSQL, extending\n
50
71
  \ ActiveRecord with scopes making search easy and fun!"
51
72
  email:
@@ -68,7 +89,6 @@ files:
68
89
  - spec/texticle_spec.rb
69
90
  - spec/texticle/searchable_spec.rb
70
91
  - spec/config.yml
71
- has_rdoc: true
72
92
  homepage: http://tenderlove.github.com/texticle
73
93
  licenses: []
74
94
  post_install_message:
@@ -91,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
111
  version: '0'
92
112
  requirements: []
93
113
  rubyforge_project: texticle
94
- rubygems_version: 1.6.2
114
+ rubygems_version: 1.8.10
95
115
  signing_key:
96
116
  specification_version: 3
97
117
  summary: Texticle exposes full text search capabilities from PostgreSQL