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 +5 -5
- data/CHANGELOG.md +16 -0
- data/Gemfile +5 -1
- data/README.md +21 -18
- data/Rakefile +10 -4
- data/lib/textacular/version.rb +1 -1
- data/lib/textacular.rb +42 -12
- data/spec/config.travis.yml +2 -2
- data/spec/textacular/searchable_spec.rb +50 -0
- data/spec/textacular_spec.rb +13 -0
- metadata +26 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 974b8a8209d44a134f421db20f9ae6338c03057d8bcf26ce1e0c40778a29d6bf
|
4
|
+
data.tar.gz: 422c096f881919e53eaaab5f530e26e0e5aeffe33a4c8646bcb7f3b367dc9084
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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', '~>
|
31
|
+
gem 'textacular', '~> 5.0'
|
34
32
|
```
|
35
33
|
|
36
|
-
#### Rails
|
34
|
+
#### Rails 3, Rails 4
|
37
35
|
|
38
36
|
In the project's Gemfile add
|
39
37
|
|
40
38
|
```ruby
|
41
|
-
gem 'textacular', '~>
|
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
|
-
|
154
|
-
|
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
|
-
|
161
|
-
|
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
|
-
|
47
|
-
ActiveRecord::Migration.new.migration_context
|
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,
|
57
|
+
ActiveRecord::Migrator.new(:up, migrations, schema_migration).migrate
|
52
58
|
end
|
53
59
|
|
54
60
|
desc 'Reverse the test database migrations'
|
data/lib/textacular/version.rb
CHANGED
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
|
-
|
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
|
-
|
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 ")).
|
data/spec/config.travis.yml
CHANGED
@@ -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
|
data/spec/textacular_spec.rb
CHANGED
@@ -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.
|
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:
|
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:
|
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:
|
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: '
|
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: '
|
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
|
-
|
184
|
-
|
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:
|