textacular 4.0.1 → 5.3.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: 496462806ce43763aaeefe0d72e47bf1ed300a4d
4
- data.tar.gz: 1ce89e8864290e04eccbf312aac17b65dac96211
2
+ SHA256:
3
+ metadata.gz: 39d5e5ef4609c4ad498b48ba0b2553b5e211cca399fd6df820db27b2925114d6
4
+ data.tar.gz: b0b76e8c23b19fa21f6e283c449099d5f6e8978d50e788f49a4cf80bc0664ea9
5
5
  SHA512:
6
- metadata.gz: aec40e799a52f138444950693b6606446c50d011fb17fabb3d96218c2831693498ba89584f474c0539a2dd48b3eb45596f9c443362812696fd85419069889138
7
- data.tar.gz: d3f7dffb02381e1364474c1df66c0a464d9927794c354569de0905e434d58e5e449d012c5bd656c3c74fc2e034a99e87ddb40f7a3e11f5d3ec24224bd668935f
6
+ metadata.gz: 10bf037b59c151203a7f2dd962f7a4acb9abb0638f09fea15eba6f887c34635a28cc4aaa931df5a052da712b858e65080449a04e320b97e88e3b3cf997728182
7
+ data.tar.gz: e807fa30b6bf512418a06e97833fd48577be25aadd34d26d289df1005f83b0b9f1980e296ad818e05ee7a72357ae0453c2f8cd5eb2aed73161c85fac77e7ec05
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 5.3.0
6
+
7
+ * Add `#web_search` method to use Postgres' 11+ `websearch_to_tsquery`
8
+
9
+ ## 5.2.0
10
+
11
+ * Active Record 6.0 compatibility
12
+
13
+ ## 5.1.0
14
+
15
+ * ActiveRecord 5.2 compatibility by wrapping string queries with `Arel.sql()`
16
+ * Adds latest Ruby, Rails and Postgres-dependencies to Travis.
17
+ * Rewrite development migration code to support Rails 5.0-5.2.
18
+
19
+ ## 5.0.0
20
+
21
+ * ActiveRecord 5.1 compatibility
22
+ * drop support for ActiveRecord older than 5.0 and Ruby version lower than 2.2.5
23
+
5
24
  ## 4.0.1
6
25
 
7
26
  * ActiveRecord 5 compatibility above 5.0.0
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ git 'git://github.com/rails/rails.git', branch: 'master' do
6
+ gem 'activerecord'
7
+ end
data/README.md CHANGED
@@ -25,12 +25,20 @@ extending ActiveRecord with scopes making search easy and fun!
25
25
 
26
26
  ### Quick Start
27
27
 
28
- #### Rails 3, Rails 4 and Rails 5!
28
+ #### Rails 3, Rails 4
29
29
 
30
30
  In the project's Gemfile add
31
31
 
32
32
  ```ruby
33
- gem 'textacular', '~> 3.0'
33
+ gem 'textacular', '~> 4.0'
34
+ ```
35
+
36
+ #### Rails > 5.0
37
+
38
+ In the project's Gemfile add
39
+
40
+ ```ruby
41
+ gem 'textacular', '~> 5.0'
34
42
  ```
35
43
 
36
44
  #### ActiveRecord outside of Rails
@@ -67,6 +75,20 @@ Game.advanced_search(title: 'Street|Fantasy')
67
75
  Game.advanced_search(system: '!PS2')
68
76
  ```
69
77
 
78
+ The `#web_search` method lets you use Postgres' 11+ `websearch_to_tsquery` function
79
+ supporting websearch like syntax:
80
+
81
+ - unquoted text: text not inside quote marks will be converted to terms separated by & operators, as if processed by plainto_tsquery.
82
+ - "quoted text": text inside quote marks will be converted to terms separated by <-> operators, as if processed by phraseto_tsquery.
83
+ - OR: logical or will be converted to the | operator.
84
+ - -: the logical not operator, converted to the the ! operator.
85
+
86
+ ```ruby
87
+ Game.web_search(title: '"Street Fantasy"')
88
+ Game.web_search(title: 'Street OR Fantasy')
89
+ Game.web_search(system: '-PS2')
90
+ ```
91
+
70
92
  Finally, the `#fuzzy_search` method lets you use Postgres's trigram search
71
93
  functionality.
72
94
 
@@ -140,12 +162,20 @@ end
140
162
  You can have Postgresql use an index for the full-text search. To declare a full-text index, in a
141
163
  migration add code like the following:
142
164
 
165
+ #### For basic_search
143
166
  ```ruby
144
167
  execute "
145
168
  create index on email_logs using gin(to_tsvector('english', subject));
146
169
  create index on email_logs using gin(to_tsvector('english', email_address));"
147
170
  ```
148
171
 
172
+ #### For fuzzy_search
173
+ ```ruby
174
+ execute "
175
+ CREATE INDEX trgm_subject_indx ON users USING gist (subject gist_trgm_ops);
176
+ CREATE INDEX trgm_email_address_indx ON users USING gist (email_address gist_trgm_ops);
177
+ ```
178
+
149
179
  In the above example, the table email_logs has two text columns that we search against, subject and email_address.
150
180
  You will need to add an index for every text/string column you query against, or else Postgresql will revert to a
151
181
  full table scan instead of using the indexes.
data/Rakefile CHANGED
@@ -43,12 +43,28 @@ namespace :db do
43
43
 
44
44
  desc 'Run the test database migrations'
45
45
  task :up => :'db:connect' do
46
- ActiveRecord::Migrator.up 'db/migrate'
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
53
+ else
54
+ migrations = ActiveRecord::Migrator.migrations('db/migrate')
55
+ schema_migration = nil
56
+ end
57
+ ActiveRecord::Migrator.new(:up, migrations, schema_migration).migrate
47
58
  end
48
59
 
49
60
  desc 'Reverse the test database migrations'
50
61
  task :down => :'db:connect' do
51
- ActiveRecord::Migrator.down 'db/migrate'
62
+ migrations = if ActiveRecord.version.version >= '5.2'
63
+ ActiveRecord::Migration.new.migration_context.migrations
64
+ else
65
+ ActiveRecord::Migrator.migrations('db/migrate')
66
+ end
67
+ ActiveRecord::Migrator.new(:down, migrations, nil).migrate
52
68
  end
53
69
  end
54
70
  task :migrate => :'migrate:up'
@@ -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,19 +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
+ "(#{table_name}.#{column}::text % #{search_term})"
128
+ end
129
+
130
+
131
+ def web_similarities_and_conditions(parsed_query_hash)
132
+ parsed_query_hash.inject([[], []]) do |(similarities, conditions), query_args|
133
+ similarities << web_similarity_string(*query_args)
134
+ conditions << web_condition_string(*query_args)
135
+
136
+ [similarities, conditions]
137
+ end
138
+ end
139
+
140
+ def web_similarity_string(table_name, column, search_term)
141
+ "COALESCE(ts_rank(to_tsvector(#{quoted_language}, #{table_name}.#{column}::text), websearch_to_tsquery(#{quoted_language}, #{search_term}::text)), 0)"
142
+ end
143
+
144
+ def web_condition_string(table_name, column, search_term)
145
+ "to_tsvector(#{quoted_language}, #{table_name}.#{column}::text) @@ websearch_to_tsquery(#{quoted_language}, #{search_term}::text)"
121
146
  end
122
147
 
123
- def assemble_query(similarities, conditions, exclusive)
124
- rank = connection.quote_column_name('rank' + rand(100000000000000000).to_s)
148
+ def assemble_query(similarities, conditions, exclusive, rank_alias)
149
+ rank_alias ||= 'rank' + rand(100000000000000000).to_s
150
+ rank = connection.quote_column_name(rank_alias)
125
151
 
126
- select("#{quoted_table_name + '.*,' if select_values.empty?} #{similarities.join(" + ")} AS #{rank}").
152
+ select(Arel.sql("#{quoted_table_name + '.*,' if select_values.empty?} #{similarities.join(" + ")} AS #{rank}")).
127
153
  where(conditions.join(exclusive ? " AND " : " OR ")).
128
- order("#{rank} DESC")
154
+ order(Arel.sql("#{rank} DESC"))
129
155
  end
130
156
 
131
157
  def select_values
@@ -151,7 +177,7 @@ module Textacular
151
177
  module Helper
152
178
  class << self
153
179
  def normalize(query)
154
- query.to_s.gsub(/\s(?![\&|\!|\|])/, '\\\\ ')
180
+ query.to_s.gsub(/\s(?![\&\!\|])/, '\\\\ ')
155
181
  end
156
182
  end
157
183
  end
@@ -16,7 +16,7 @@ class #{model_name}FullTextSearch < ActiveRecord::Migration
16
16
  end
17
17
  MIGRATION
18
18
  filename = "#{model_name.underscore}_full_text_search"
19
- generator = Textacular::MigrationGenerator.new(content, filename)
19
+ generator = Textacular::MigrationGenerator.new(filename, content)
20
20
  generator.generate_migration
21
21
  end
22
22
 
@@ -52,7 +52,7 @@ MIGRATION
52
52
  <<-SQL
53
53
  CREATE index #{index_name_for(model, column)}
54
54
  ON #{model.table_name}
55
- USING gin(to_tsvector("#{dictionary}", "#{model.table_name}"."#{column}"::text));
55
+ USING gin(to_tsvector('#{dictionary}', "#{model.table_name}"."#{column}"::text));
56
56
  SQL
57
57
  end
58
58
 
@@ -4,6 +4,7 @@ class Textacular::MigrationGenerator
4
4
  def initialize(filename, content)
5
5
  @filename = filename
6
6
  @content = content
7
+ @output_stream = nil
7
8
  end
8
9
 
9
10
  def generate_migration
@@ -1,5 +1,5 @@
1
1
  module Textacular
2
- VERSION = '4.0.1'
2
+ VERSION = '5.3.0'
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -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
@@ -12,7 +12,7 @@ class WebComicWithSearchableNameFullTextSearch < ActiveRecord::Migration
12
12
  DROP index IF EXISTS web_comics_name_fts_idx;
13
13
  CREATE index web_comics_name_fts_idx
14
14
  ON web_comics
15
- USING gin(to_tsvector("english", "web_comics"."name"::text));
15
+ USING gin(to_tsvector('english', "web_comics"."name"::text));
16
16
  SQL
17
17
  end
18
18
 
@@ -25,7 +25,7 @@ end
25
25
  MIGRATION
26
26
 
27
27
  generator = double(:migration_generator)
28
- expect(Textacular::MigrationGenerator).to receive(:new).with(content, file_name).and_return(generator)
28
+ expect(Textacular::MigrationGenerator).to receive(:new).with(file_name, content).and_return(generator)
29
29
  expect(generator).to receive(:generate_migration)
30
30
 
31
31
  Textacular::FullTextIndexer.new.generate_migration('WebComicWithSearchableName')
@@ -42,11 +42,11 @@ class WebComicWithSearchableNameAndAuthorFullTextSearch < ActiveRecord::Migratio
42
42
  DROP index IF EXISTS web_comics_name_fts_idx;
43
43
  CREATE index web_comics_name_fts_idx
44
44
  ON web_comics
45
- USING gin(to_tsvector("english", "web_comics"."name"::text));
45
+ USING gin(to_tsvector('english', "web_comics"."name"::text));
46
46
  DROP index IF EXISTS web_comics_author_fts_idx;
47
47
  CREATE index web_comics_author_fts_idx
48
48
  ON web_comics
49
- USING gin(to_tsvector("english", "web_comics"."author"::text));
49
+ USING gin(to_tsvector('english', "web_comics"."author"::text));
50
50
  SQL
51
51
  end
52
52
 
@@ -60,7 +60,7 @@ end
60
60
  MIGRATION
61
61
 
62
62
  generator = double(:migration_generator)
63
- expect(Textacular::MigrationGenerator).to receive(:new).with(content, file_name).and_return(generator)
63
+ expect(Textacular::MigrationGenerator).to receive(:new).with(file_name, content).and_return(generator)
64
64
  expect(generator).to receive(:generate_migration)
65
65
 
66
66
  Textacular::FullTextIndexer.new.generate_migration('WebComicWithSearchableNameAndAuthor')
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textacular
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 5.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Hamill
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2016-10-18 00:00:00.000000000 Z
14
+ date: 2020-06-10 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: pg
@@ -19,14 +19,14 @@ dependencies:
19
19
  requirements:
20
20
  - - "~>"
21
21
  - !ruby/object:Gem::Version
22
- version: '0.14'
22
+ version: 1.0.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: '0.14'
29
+ version: 1.0.0
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: rspec
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -97,26 +97,40 @@ 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
103
117
  requirements:
104
118
  - - ">="
105
119
  - !ruby/object:Gem::Version
106
- version: '3.0'
120
+ version: '5.0'
107
121
  - - "<"
108
122
  - !ruby/object:Gem::Version
109
- version: '5.1'
123
+ version: '6.1'
110
124
  type: :runtime
111
125
  prerelease: false
112
126
  version_requirements: !ruby/object:Gem::Requirement
113
127
  requirements:
114
128
  - - ">="
115
129
  - !ruby/object:Gem::Version
116
- version: '3.0'
130
+ version: '5.0'
117
131
  - - "<"
118
132
  - !ruby/object:Gem::Version
119
- version: '5.1'
133
+ version: '6.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!
@@ -180,8 +194,7 @@ 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.2.2
197
+ rubygems_version: 3.0.3
185
198
  signing_key:
186
199
  specification_version: 4
187
200
  summary: Textacular exposes full text search capabilities from PostgreSQL
@@ -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: