textacular 5.1.0 → 5.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3bd98a4fec2d72714909b49059a7f8b50298d734
4
- data.tar.gz: 9b3fc608d5192296392da8323048c65440146cbd
2
+ SHA256:
3
+ metadata.gz: 974b8a8209d44a134f421db20f9ae6338c03057d8bcf26ce1e0c40778a29d6bf
4
+ data.tar.gz: 422c096f881919e53eaaab5f530e26e0e5aeffe33a4c8646bcb7f3b367dc9084
5
5
  SHA512:
6
- metadata.gz: 7e1cd1fdce05d21df1e9803ca0bd1db66fc18ba371a4980175824ad5c46df5eb1e4cfd1aed857e5cea384887734f7f563ce498cc860c8c8827873f669a9ca584
7
- data.tar.gz: 6237664f59c95a81b48283b3f761289a8c63199c908fba347e3931be9f93c59f5be897c971434266a7d1a203dfc3aa11af217736ca2e3313cea180c73c0d16c3
6
+ metadata.gz: ab93dff1128025cc4615d29c819d104a9300bdbb0a88d9cc588940a1f630922a201b9f11449d610ba5cadc5ae85c2e761c0ad3ff5b8cb7a85f1fd3dd15acf744
7
+ data.tar.gz: 2102ea9936a087f18ed4945ddb4213e93acb70dc4bc8d3281489664fda2968b635e024636f0f2112f0fa8fc7a13b5d2b7e728aa200ac7663b681ae287b7e6b5d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 5.5.0
6
+
7
+ * ActiveRecord 7.0 compatibility
8
+
9
+ ## 5.4.0
10
+
11
+ * ActiveRecord 6.1 compatibility
12
+
13
+ ## 5.3.0
14
+
15
+ * Add `#web_search` method to use Postgres' 11+ `websearch_to_tsquery`
16
+
17
+ ## 5.2.0
18
+
19
+ * Active Record 6.0 compatibility
20
+
5
21
  ## 5.1.0
6
22
 
7
23
  * ActiveRecord 5.2 compatibility by wrapping string queries with `Arel.sql()`
data/Gemfile CHANGED
@@ -2,4 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'activerecord', '~> 5.2.x'
5
+ git 'git://github.com/rails/rails.git', branch: 'main' do
6
+ gem 'activerecord'
7
+ end
8
+
9
+ gem "pg", "~> 1.1"
data/README.md CHANGED
@@ -25,20 +25,18 @@ extending ActiveRecord with scopes making search easy and fun!
25
25
 
26
26
  ### Quick Start
27
27
 
28
- #### Rails 3, Rails 4
29
-
30
28
  In the project's Gemfile add
31
29
 
32
30
  ```ruby
33
- gem 'textacular', '~> 4.0'
31
+ gem 'textacular', '~> 5.0'
34
32
  ```
35
33
 
36
- #### Rails 5.0 and Rails 5.1!
34
+ #### Rails 3, Rails 4
37
35
 
38
36
  In the project's Gemfile add
39
37
 
40
38
  ```ruby
41
- gem 'textacular', '~> 5.0'
39
+ gem 'textacular', '~> 4.0'
42
40
  ```
43
41
 
44
42
  #### ActiveRecord outside of Rails
@@ -75,6 +73,20 @@ Game.advanced_search(title: 'Street|Fantasy')
75
73
  Game.advanced_search(system: '!PS2')
76
74
  ```
77
75
 
76
+ The `#web_search` method lets you use Postgres' 11+ `websearch_to_tsquery` function
77
+ supporting websearch like syntax:
78
+
79
+ - unquoted text: text not inside quote marks will be converted to terms separated by & operators, as if processed by plainto_tsquery.
80
+ - "quoted text": text inside quote marks will be converted to terms separated by <-> operators, as if processed by phraseto_tsquery.
81
+ - OR: logical or will be converted to the | operator.
82
+ - -: the logical not operator, converted to the the ! operator.
83
+
84
+ ```ruby
85
+ Game.web_search(title: '"Street Fantasy"')
86
+ Game.web_search(title: 'Street OR Fantasy')
87
+ Game.web_search(system: '-PS2')
88
+ ```
89
+
78
90
  Finally, the `#fuzzy_search` method lets you use Postgres's trigram search
79
91
  functionality.
80
92
 
@@ -150,29 +162,20 @@ migration add code like the following:
150
162
 
151
163
  #### For basic_search
152
164
  ```ruby
153
- execute "
154
- create index on email_logs using gin(to_tsvector('english', subject));
155
- create index on email_logs using gin(to_tsvector('english', email_address));"
165
+ add_index :email_logs, %{to_tsvector('english', subject)}, using: :gin
166
+ add_index :email_logs, %{to_tsvector('english', email_address)}, using: :gin
156
167
  ```
157
168
 
158
169
  #### For fuzzy_search
159
170
  ```ruby
160
- execute "
161
- CREATE INDEX trgm_subject_indx ON users USING gist (subject gist_trgm_ops);
162
- CREATE INDEX trgm_email_address_indx ON users USING gist (email_address gist_trgm_ops);
171
+ add_index :email_logs, :subject, using: :gist, opclass: :gist_trgm_ops
172
+ add_index :email_logs, :email_address, using: :gist, opclass: :gist_trgm_ops
163
173
  ```
164
174
 
165
175
  In the above example, the table email_logs has two text columns that we search against, subject and email_address.
166
176
  You will need to add an index for every text/string column you query against, or else Postgresql will revert to a
167
177
  full table scan instead of using the indexes.
168
178
 
169
- If you create these indexes, you should also switch to sql for your schema_format in `config/application.rb`:
170
-
171
- ```ruby
172
- config.active_record.schema_format = :sql
173
- ```
174
-
175
-
176
179
  ## REQUIREMENTS:
177
180
 
178
181
  * ActiveRecord
data/Rakefile CHANGED
@@ -43,12 +43,18 @@ namespace :db do
43
43
 
44
44
  desc 'Run the test database migrations'
45
45
  task :up => :'db:connect' do
46
- migrations = if ActiveRecord.version.version >= '5.2'
47
- ActiveRecord::Migration.new.migration_context.migrations
46
+ if ActiveRecord.version >= Gem::Version.new('6.0.0')
47
+ context = ActiveRecord::Migration.new.migration_context
48
+ migrations = context.migrations
49
+ schema_migration = context.schema_migration
50
+ elsif ActiveRecord.version >= Gem::Version.new('5.2')
51
+ migrations = ActiveRecord::Migration.new.migration_context.migrations
52
+ schema_migration = nil
48
53
  else
49
- ActiveRecord::Migrator.migrations('db/migrate')
54
+ migrations = ActiveRecord::Migrator.migrations('db/migrate')
55
+ schema_migration = nil
50
56
  end
51
- ActiveRecord::Migrator.new(:up, migrations, nil).migrate
57
+ ActiveRecord::Migrator.new(:up, migrations, schema_migration).migrate
52
58
  end
53
59
 
54
60
  desc 'Reverse the test database migrations'
@@ -1,5 +1,5 @@
1
1
  module Textacular
2
- VERSION = '5.1.0'
2
+ VERSION = '5.5.0'
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/textacular.rb CHANGED
@@ -12,29 +12,36 @@ module Textacular
12
12
  'english'
13
13
  end
14
14
 
15
- def search(query = "", exclusive = true)
16
- basic_search(query, exclusive)
15
+ def search(query = "", exclusive = true, rank_alias = nil)
16
+ basic_search(query, exclusive, rank_alias)
17
17
  end
18
18
 
19
- def basic_search(query = "", exclusive = true)
19
+ def basic_search(query = "", exclusive = true, rank_alias = nil)
20
20
  exclusive, query = munge_exclusive_and_query(exclusive, query)
21
21
  parsed_query_hash = parse_query_hash(query)
22
22
  similarities, conditions = basic_similarities_and_conditions(parsed_query_hash)
23
- assemble_query(similarities, conditions, exclusive)
23
+ assemble_query(similarities, conditions, exclusive, rank_alias)
24
24
  end
25
25
 
26
- def advanced_search(query = "", exclusive = true)
26
+ def advanced_search(query = "", exclusive = true, rank_alias = nil)
27
27
  exclusive, query = munge_exclusive_and_query(exclusive, query)
28
28
  parsed_query_hash = parse_query_hash(query)
29
29
  similarities, conditions = advanced_similarities_and_conditions(parsed_query_hash)
30
- assemble_query(similarities, conditions, exclusive)
30
+ assemble_query(similarities, conditions, exclusive, rank_alias)
31
31
  end
32
32
 
33
- def fuzzy_search(query = '', exclusive = true)
33
+ def fuzzy_search(query = '', exclusive = true, rank_alias = nil)
34
34
  exclusive, query = munge_exclusive_and_query(exclusive, query)
35
35
  parsed_query_hash = parse_query_hash(query)
36
36
  similarities, conditions = fuzzy_similarities_and_conditions(parsed_query_hash)
37
- assemble_query(similarities, conditions, exclusive)
37
+ assemble_query(similarities, conditions, exclusive, rank_alias)
38
+ end
39
+
40
+ def web_search(query = '', exclusive = true, rank_alias = nil)
41
+ exclusive, query = munge_exclusive_and_query(exclusive, query)
42
+ parsed_query_hash = parse_query_hash(query)
43
+ similarities, conditions = web_similarities_and_conditions(parsed_query_hash)
44
+ assemble_query(similarities, conditions, exclusive, rank_alias)
38
45
  end
39
46
 
40
47
  private
@@ -113,15 +120,38 @@ module Textacular
113
120
  end
114
121
 
115
122
  def fuzzy_similarity_string(table_name, column, search_term)
116
- "COALESCE(similarity(#{table_name}.#{column}, #{search_term}), 0)"
123
+ "COALESCE(similarity(#{table_name}.#{column}::text, #{search_term}), 0)"
117
124
  end
118
125
 
119
126
  def fuzzy_condition_string(table_name, column, search_term)
120
- "(#{table_name}.#{column} % #{search_term})"
127
+ # At this point, search_term is already quoted and query ready. Insert % between the quotes and the actual string.
128
+ search_term = search_term.gsub(/^(['"])/, '\1%')
129
+ search_term = search_term.gsub(/(['"])$/, '%\1')
130
+
131
+ "(#{table_name}.#{column}::text ILIKE #{search_term})"
132
+ end
133
+
134
+
135
+ def web_similarities_and_conditions(parsed_query_hash)
136
+ parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
137
+ similarities << web_similarity_string(*query_args)
138
+ conditions << web_condition_string(*query_args)
139
+
140
+ [similarities, conditions]
141
+ end
142
+ end
143
+
144
+ def web_similarity_string(table_name, column, search_term)
145
+ "COALESCE(ts_rank(to_tsvector(#{quoted_language}, #{table_name}.#{column}::text), websearch_to_tsquery(#{quoted_language}, #{search_term}::text)), 0)"
146
+ end
147
+
148
+ def web_condition_string(table_name, column, search_term)
149
+ "to_tsvector(#{quoted_language}, #{table_name}.#{column}::text) @@ websearch_to_tsquery(#{quoted_language}, #{search_term}::text)"
121
150
  end
122
151
 
123
- def assemble_query(similarities, conditions, exclusive)
124
- rank = connection.quote_column_name('rank' + rand(100000000000000000).to_s)
152
+ def assemble_query(similarities, conditions, exclusive, rank_alias)
153
+ rank_alias ||= 'rank' + rand(100000000000000000).to_s
154
+ rank = connection.quote_column_name(rank_alias)
125
155
 
126
156
  select(Arel.sql("#{quoted_table_name + '.*,' if select_values.empty?} #{similarities.join(" + ")} AS #{rank}")).
127
157
  where(conditions.join(exclusive ? " AND " : " OR ")).
@@ -3,6 +3,6 @@ timeout: 5000
3
3
  host: localhost
4
4
  adapter: postgresql
5
5
  username: postgres
6
- password:
6
+ password: password
7
7
  database: textacular_test
8
- min_messages: ERROR
8
+ min_messages: ERROR
@@ -113,6 +113,14 @@ RSpec.describe "Searchable" do
113
113
  end
114
114
  end
115
115
 
116
+ describe "web search" do # Uses websearch_to_tsquery
117
+ ["hello \\", "tebow!" , "food &"].each do |search_term|
118
+ it "works with interesting term \"#{search_term}\"" do
119
+ expect(WebComicWithSearchableName.web_search(search_term)).to be_empty
120
+ end
121
+ end
122
+ end
123
+
116
124
  it "does fuzzy searching" do
117
125
  expect(
118
126
  WebComicWithSearchableName.fuzzy_search('Questio')
@@ -182,6 +190,14 @@ RSpec.describe "Searchable" do
182
190
  expect(
183
191
  WebComicWithSearchableNameAndAuthor.advanced_search("Tycho")
184
192
  ).to eq([penny_arcade])
193
+
194
+ expect(
195
+ WebComicWithSearchableNameAndAuthor.web_search("Penny")
196
+ ).to eq([penny_arcade])
197
+
198
+ expect(
199
+ WebComicWithSearchableNameAndAuthor.web_search("Tycho")
200
+ ).to eq([penny_arcade])
185
201
  end
186
202
 
187
203
  it "allows includes" do
@@ -190,5 +206,39 @@ RSpec.describe "Searchable" do
190
206
  ).to eq([penny_arcade])
191
207
  end
192
208
  end
209
+
210
+ context 'custom rank' do
211
+ let!(:questionable_content) do
212
+ WebComicWithSearchableName.create(
213
+ name: 'Questionable Content',
214
+ author: nil,
215
+ )
216
+ end
217
+
218
+ it "is selected for search" do
219
+ search_result = WebComicWithSearchableNameAndAuthor.search('Questionable Content', true, 'my_rank')
220
+ expect(search_result.first.attributes['my_rank']).to be_truthy
221
+ end
222
+
223
+ it "is selected for basic_search" do
224
+ search_result = WebComicWithSearchableNameAndAuthor.basic_search('Questionable Content', true, 'my_rank')
225
+ expect(search_result.first.attributes['my_rank']).to be_truthy
226
+ end
227
+
228
+ it "is selected for advanced_search" do
229
+ search_result = WebComicWithSearchableNameAndAuthor.advanced_search('Questionable Content', true, 'my_rank')
230
+ expect(search_result.first.attributes['my_rank']).to be_truthy
231
+ end
232
+
233
+ it "is selected for fuzzy_search" do
234
+ search_result = WebComicWithSearchableNameAndAuthor.fuzzy_search('Questionable Content', true, 'my_rank')
235
+ expect(search_result.first.attributes['my_rank']).to be_truthy
236
+ end
237
+
238
+ it "is selected for web_search" do
239
+ search_result = WebComicWithSearchableNameAndAuthor.web_search('Questionable Content', true, 'my_rank')
240
+ expect(search_result.first.attributes['my_rank']).to be_truthy
241
+ end
242
+ end
193
243
  end
194
244
  end
@@ -181,6 +181,13 @@ RSpec.describe Textacular do
181
181
  expect(GameExtendedWithTextacular).to respond_to(:search)
182
182
  end
183
183
 
184
+ describe "#fuzzy_search" do
185
+ it 'searches non-text columns' do
186
+ expect(GameExtendedWithTextacular.fuzzy_search(id: mario.id)
187
+ ).to eq([mario])
188
+ end
189
+ end
190
+
184
191
  describe "#advanced_search" do
185
192
  context "with a String argument" do
186
193
  it "searches across all :string columns (if not indexes have been specified)" do
@@ -282,6 +289,12 @@ RSpec.describe Textacular do
282
289
  end
283
290
  end
284
291
  end
292
+
293
+ describe "#fuzzy_search" do
294
+ it "works if column contains multiple space delimited strings" do
295
+ expect(GameExtendedWithTextacular.fuzzy_search(title: 'mar')).to eq([mario])
296
+ end
297
+ end
285
298
  end
286
299
  end
287
300
  end
metadata CHANGED
@@ -1,32 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textacular
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 5.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Hamill
8
8
  - ecin
9
9
  - Aaron Patterson
10
10
  - Greg Molnar
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2018-05-12 00:00:00.000000000 Z
14
+ date: 2021-12-23 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: pg
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
- - - "~>"
20
+ - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 1.0.0
22
+ version: '0'
23
23
  type: :development
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - "~>"
27
+ - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 1.0.0
29
+ version: '0'
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: rspec
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -97,6 +97,20 @@ dependencies:
97
97
  - - ">="
98
98
  - !ruby/object:Gem::Version
99
99
  version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: byebug
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
100
114
  - !ruby/object:Gem::Dependency
101
115
  name: activerecord
102
116
  requirement: !ruby/object:Gem::Requirement
@@ -106,7 +120,7 @@ dependencies:
106
120
  version: '5.0'
107
121
  - - "<"
108
122
  - !ruby/object:Gem::Version
109
- version: '6.0'
123
+ version: '7.1'
110
124
  type: :runtime
111
125
  prerelease: false
112
126
  version_requirements: !ruby/object:Gem::Requirement
@@ -116,7 +130,7 @@ dependencies:
116
130
  version: '5.0'
117
131
  - - "<"
118
132
  - !ruby/object:Gem::Version
119
- version: '6.0'
133
+ version: '7.1'
120
134
  description: |-
121
135
  Textacular exposes full text search capabilities from PostgreSQL, extending
122
136
  ActiveRecord with scopes making search easy and fun!
@@ -165,7 +179,7 @@ homepage: http://textacular.github.com/textacular
165
179
  licenses:
166
180
  - MIT
167
181
  metadata: {}
168
- post_install_message:
182
+ post_install_message:
169
183
  rdoc_options: []
170
184
  require_paths:
171
185
  - lib
@@ -180,9 +194,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
194
  - !ruby/object:Gem::Version
181
195
  version: '0'
182
196
  requirements: []
183
- rubyforge_project:
184
- rubygems_version: 2.6.8
185
- signing_key:
197
+ rubygems_version: 3.1.4
198
+ signing_key:
186
199
  specification_version: 4
187
200
  summary: Textacular exposes full text search capabilities from PostgreSQL
188
201
  test_files:
@@ -207,4 +220,3 @@ test_files:
207
220
  - spec/textacular/migration_generator_spec.rb
208
221
  - spec/textacular/searchable_spec.rb
209
222
  - spec/textacular/trigram_installer_spec.rb
210
- has_rdoc: