textacular 4.0.1 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: