autosuggest 0.1.2 → 0.2.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
2
  SHA256:
3
- metadata.gz: '083927cb6e763a26bad61351189dd1a1c1e66d31da0a4844635ec5440f4620d0'
4
- data.tar.gz: 94245fc7eaeb98f54669b561311d9dee4ac43788f88cef3768a33891c8b530ad
3
+ metadata.gz: fe418b5cfaa006d454a8e061ae08b63821de147677175054be5e28ad254399c6
4
+ data.tar.gz: 10c28b44de11f53fccd66a3e6f547a7f97282e23b529452a1db827b3d2dd0624
5
5
  SHA512:
6
- metadata.gz: f20b852dfcb4cf4249b4c4b2a72e7f58c2c2b642a11b14b33e632ee934736c721d70bcd87f7b6e20659ba5e82f05ce679b69d0e5e72f3b4c64d8187078d2b8c3
7
- data.tar.gz: 6e99a02d77dc1ea2cf06e0903d6adebbbe73579496de092efff471369226dfb917df9bd31cee7697bc2d0fcd9392cbe6393186a6c4a747b49a6b03cee348108a
6
+ metadata.gz: 236b65f1939693fd076445ebddff62535d1d1197e44561ade442e2c381f2d3c4bd06572daacc29718dbfb087abc1cf3c001d0152f21c9198d059d8c1d933985c
7
+ data.tar.gz: 1ca979179a176b6a7eca3ee8b9593e6479680ea4706e8ad1c004d8c88efe0a1f1a82b779dee050cb1aa3c6af749323a87fc9f93c9c0c4d8eeb530a9e6a9f11cd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.2.0 (2023-01-29)
2
+
3
+ - Added `language` option
4
+ - Changed `suggestions` method to filter by default
5
+ - Changed `filter: true` to only return query and score
6
+ - Removed `blacklist_words` method
7
+ - Dropped support for Ruby < 2.7
8
+
9
+ ## 0.1.3 (2021-11-23)
10
+
11
+ - Added model generator
12
+
1
13
  ## 0.1.2 (2021-11-22)
2
14
 
3
15
  - Added `filter` option to `suggestions` method
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015-2021 Andrew Kane
1
+ Copyright (c) 2015-2023 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -4,6 +4,8 @@ Generate autocomplete suggestions based on what your users search
4
4
 
5
5
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
6
6
 
7
+ Autosuggest 0.2 was recently released! See [how to upgrade](#upgrading)
8
+
7
9
  [![Build Status](https://github.com/ankane/autosuggest/workflows/build/badge.svg?branch=master)](https://github.com/ankane/autosuggest/actions)
8
10
 
9
11
  ## Installation
@@ -11,7 +13,7 @@ Generate autocomplete suggestions based on what your users search
11
13
  Add this line to your application’s Gemfile:
12
14
 
13
15
  ```ruby
14
- gem 'autosuggest'
16
+ gem "autosuggest"
15
17
  ```
16
18
 
17
19
  ## Getting Started
@@ -38,14 +40,20 @@ top_queries = Searchjoy::Search.group(:normalized_query)
38
40
  Then pass them to Autosuggest.
39
41
 
40
42
  ```ruby
41
- autosuggest = Autosuggest.new(top_queries)
43
+ autosuggest = Autosuggest::Generator.new(top_queries)
42
44
  ```
43
45
 
44
46
  #### Filter duplicates
45
47
 
46
48
  [Stemming](https://en.wikipedia.org/wiki/Stemming) is used to detect duplicates like `apple` and `apples`.
47
49
 
48
- The most popular query is preferred by default. To override this, use:
50
+ Specify the stemming language (defaults to `english`) with:
51
+
52
+ ```ruby
53
+ autosuggest = Autosuggest::Generator.new(top_queries, language: "spanish")
54
+ ```
55
+
56
+ The most popular query is preferred by default. To override this, use:
49
57
 
50
58
  ```ruby
51
59
  autosuggest.prefer ["apples"]
@@ -85,17 +93,90 @@ There are two ways to build the corpus, which can be used together.
85
93
  autosuggest.block_words ["boom"]
86
94
  ```
87
95
 
88
- #### Profit
96
+ #### Generate suggestions
97
+
98
+ Generate suggestions with:
99
+
100
+ ```ruby
101
+ suggestions = autosuggest.suggestions
102
+ ```
103
+
104
+ #### Save suggestions
105
+
106
+ Save suggestions in your database or another data store.
107
+
108
+ With Rails, you can generate a simple model with:
109
+
110
+ ```sh
111
+ rails generate autosuggest:suggestions
112
+ rails db:migrate
113
+ ```
114
+
115
+ And update suggestions with:
116
+
117
+ ```ruby
118
+ now = Time.now
119
+ records = suggestions.map { |s| s.slice(:query, :score).merge(updated_at: now) }
120
+ Autosuggest::Suggestion.transaction do
121
+ Autosuggest::Suggestion.upsert_all(records, unique_by: :query)
122
+ Autosuggest::Suggestion.where("updated_at < ?", now).delete_all
123
+ end
124
+ ```
125
+
126
+ Leave out `unique_by` for MySQL, and use [activerecord-import](https://github.com/zdennis/activerecord-import) for upserts with Rails < 6.
127
+
128
+ #### Show suggestions
129
+
130
+ Use a JavaScript autocomplete library like [typeahead.js](https://github.com/twitter/typeahead.js) to show suggestions in the UI.
131
+
132
+ If you only have a few thousand suggestions, it’s much faster to load them all at once instead of as a user types (eliminates network requests).
133
+
134
+ With Rails, you can load all suggestions with:
135
+
136
+ ```ruby
137
+ Autosuggest::Suggestion.order(score: :desc).pluck(:query)
138
+ ```
139
+
140
+ And suggestions matching user input with:
141
+
142
+ ```ruby
143
+ input = params[:query]
144
+ Autosuggest::Suggestion
145
+ .order(score: :desc)
146
+ .where("query LIKE ?", "%#{Autosuggest::Suggestion.sanitize_sql_like(input.downcase)}%")
147
+ .pluck(:query)
148
+ ```
149
+
150
+ You can also cache suggestions for performance.
151
+
152
+ ```ruby
153
+ Rails.cache.fetch("suggestions", expires_in: 5.minutes) do
154
+ Autosuggest::Suggestion.order(score: :desc).pluck(:query)
155
+ end
156
+ ```
157
+
158
+ #### Additional considerations
89
159
 
90
- Get suggestions with:
160
+ You may want to have someone manually approve suggestions:
91
161
 
92
162
  ```ruby
93
- autosuggest.suggestions(filter: true)
163
+ Autosuggest::Suggestion.where(status: "approved")
94
164
  ```
95
165
 
96
- Filter queries without results and you’re set. We also prefer to have someone manually approve them by hand.
166
+ Or filter suggestions without results:
97
167
 
98
- ## Full Example
168
+ ```ruby
169
+ Autosuggest::Suggestion.find_each do |suggestion|
170
+ suggestion.results_count = Product.search(suggestion.query, load: false).count
171
+ suggestion.save! if suggestion.changed?
172
+ end
173
+
174
+ Autosuggest::Suggestion.where("results_count > 0")
175
+ ```
176
+
177
+ You can add additional fields to your model/data store to accomplish this.
178
+
179
+ ## Example
99
180
 
100
181
  ```ruby
101
182
  top_queries = Searchjoy::Search.group(:normalized_query)
@@ -103,16 +184,31 @@ top_queries = Searchjoy::Search.group(:normalized_query)
103
184
  product_names = Product.pluck(:name)
104
185
  brand_names = Brand.pluck(:name)
105
186
 
106
- autosuggest = Autosuggest.new(top_queries)
187
+ autosuggest = Autosuggest::Generator.new(top_queries)
107
188
  autosuggest.parse_words product_names
108
189
  autosuggest.add_concept "brand", brand_names
109
190
  autosuggest.prefer brand_names
110
191
  autosuggest.not_duplicates [["straws", "straus"]]
111
192
  autosuggest.block_words ["boom"]
112
193
 
113
- puts autosuggest.pretty_suggestions
114
- # or
115
- suggestions = autosuggest.suggestions(filter: true)
194
+ suggestions = autosuggest.suggestions
195
+
196
+ now = Time.now
197
+ records = suggestions.map { |s| s.slice(:query, :score).merge(updated_at: now) }
198
+ Autosuggest::Suggestion.transaction do
199
+ Autosuggest::Suggestion.upsert_all(records, unique_by: :query)
200
+ Autosuggest::Suggestion.where("updated_at < ?", now).delete_all
201
+ end
202
+ ```
203
+
204
+ ## Upgrading
205
+
206
+ ### 0.2.0
207
+
208
+ Suggestions are now filtered by default, and only the query and score are returned. To get all queries and fields, use:
209
+
210
+ ```ruby
211
+ autosuggest.suggestions(filter: false)
116
212
  ```
117
213
 
118
214
  ## History
@@ -0,0 +1,226 @@
1
+ module Autosuggest
2
+ class Generator
3
+ def initialize(top_queries, language: "english")
4
+ @top_queries = top_queries
5
+ @concepts = {}
6
+ @words = Set.new
7
+ @non_duplicates = Set.new
8
+ @blocked_words = {}
9
+ @preferred_queries = {}
10
+ @profane_words = {}
11
+ @concept_tree = {}
12
+ begin
13
+ @stemmer = Lingua::Stemmer.new(language: language)
14
+ rescue Lingua::StemmerError
15
+ raise ArgumentError, "Language not available"
16
+ end
17
+ # TODO take language into account for profanity
18
+ add_nodes(@profane_words, Obscenity::Base.blacklist)
19
+ end
20
+
21
+ def add_concept(name, values)
22
+ values = values.compact.uniq
23
+ add_nodes(@concept_tree, values)
24
+ @concepts[name] = Set.new(values.map(&:downcase))
25
+ end
26
+
27
+ def parse_words(phrases, options = {})
28
+ min = options[:min] || 1
29
+
30
+ word_counts = Hash.new(0)
31
+ phrases.each do |phrase|
32
+ words = tokenize(phrase)
33
+ words.each do |word|
34
+ word_counts[word] += 1
35
+ end
36
+ end
37
+
38
+ word_counts.select { |_, c| c >= min }.each do |word, _|
39
+ @words << word
40
+ end
41
+
42
+ word_counts
43
+ end
44
+
45
+ def not_duplicates(pairs)
46
+ pairs.each do |pair|
47
+ @non_duplicates << pair.map(&:downcase).sort
48
+ end
49
+ end
50
+
51
+ def block_words(words)
52
+ add_nodes(@blocked_words, words)
53
+ words
54
+ end
55
+
56
+ def prefer(queries)
57
+ queries.each do |query|
58
+ @preferred_queries[normalize_query(query)] ||= query
59
+ end
60
+ end
61
+
62
+ def suggestions(filter: true)
63
+ stemmed_queries = {}
64
+ added_queries = Set.new
65
+ results = @top_queries.sort_by { |_query, count| -count }.map do |query, count|
66
+ query = query.to_s
67
+
68
+ # TODO do not ignore silently
69
+ next if query.length < 2
70
+
71
+ stemmed_query = normalize_query(query)
72
+
73
+ # get preferred term
74
+ preferred_query = @preferred_queries[stemmed_query]
75
+ if preferred_query && preferred_query != query
76
+ original_query, query = query, preferred_query
77
+ end
78
+
79
+ # exclude duplicates
80
+ duplicate = stemmed_queries[stemmed_query]
81
+ stemmed_queries[stemmed_query] ||= query
82
+
83
+ # also detect possibly misspelled duplicates
84
+ # TODO use top query as duplicate
85
+ if !duplicate && query.length > 4
86
+ edits(query).each do |edited_query|
87
+ if added_queries.include?(edited_query)
88
+ duplicate = edited_query
89
+ break
90
+ end
91
+ end
92
+ end
93
+ if duplicate && @non_duplicates.include?([duplicate, query].sort)
94
+ duplicate = nil
95
+ end
96
+ added_queries << query unless duplicate
97
+
98
+ # find concepts
99
+ concepts = []
100
+ @concepts.each do |name, values|
101
+ concepts << name if values.include?(query)
102
+ end
103
+
104
+ tokens = tokenize(query)
105
+
106
+ # exclude misspellings that are not brands
107
+ misspelling = @words.any? && misspellings?(tokens)
108
+
109
+ profane = blocked?(tokens, @profane_words)
110
+ blocked = blocked?(tokens, @blocked_words)
111
+
112
+ notes = []
113
+ notes << "duplicate of #{duplicate}" if duplicate
114
+ notes.concat(concepts)
115
+ notes << "misspelling" if misspelling
116
+ notes << "profane" if profane
117
+ notes << "blocked" if blocked
118
+ notes << "originally #{original_query}" if original_query
119
+
120
+ {
121
+ query: query,
122
+ original_query: original_query,
123
+ score: count,
124
+ duplicate: duplicate,
125
+ concepts: concepts,
126
+ misspelling: misspelling,
127
+ profane: profane,
128
+ blocked: blocked,
129
+ notes: notes
130
+ }
131
+ end
132
+
133
+ results.compact!
134
+
135
+ if filter
136
+ results.filter_map do |s|
137
+ unless s[:duplicate] || s[:misspelling] || s[:profane] || s[:blocked]
138
+ s.slice(:query, :score)
139
+ end
140
+ end
141
+ else
142
+ results
143
+ end
144
+ end
145
+
146
+ def table
147
+ str = "%-30s %5s %s\n" % %w(Query Score Notes)
148
+ suggestions(filter: false).each do |suggestion|
149
+ str << "%-30s %5d %s\n" % [suggestion[:query], suggestion[:score], suggestion[:notes].join(", ")]
150
+ end
151
+ str
152
+ end
153
+ alias_method :pretty_suggestions, :table
154
+
155
+ protected
156
+
157
+ def misspellings?(tokens)
158
+ pos = [0]
159
+ while i = pos.shift
160
+ return false if i == tokens.size
161
+
162
+ if @words.include?(tokens[i])
163
+ pos << i + 1
164
+ end
165
+
166
+ node = @concept_tree[tokens[i]]
167
+ j = i
168
+ while node
169
+ j += 1
170
+ pos << j if node[:eos]
171
+ break if j == tokens.size
172
+ node = node[tokens[j]]
173
+ end
174
+
175
+ pos.uniq!
176
+ end
177
+ true
178
+ end
179
+
180
+ def blocked?(tokens, blocked_words)
181
+ tokens.each_with_index do |token, i|
182
+ node = blocked_words[token]
183
+ j = i
184
+ while node
185
+ return true if node[:eos]
186
+ j += 1
187
+ break if j == tokens.size
188
+ node = node[tokens[j]]
189
+ end
190
+ end
191
+ false
192
+ end
193
+
194
+ def tokenize(str)
195
+ str.to_s.downcase.split(" ")
196
+ end
197
+
198
+ # from https://blog.lojic.com/2008/09/04/how-to-write-a-spelling-corrector-in-ruby/
199
+ LETTERS = ("a".."z").to_a.join + "'"
200
+ def edits(word)
201
+ n = word.length
202
+ deletion = (0...n).collect { |i| word[0...i] + word[i + 1..-1] }
203
+ transposition = (0...n - 1).collect { |i| word[0...i] + word[i + 1, 1] + word[i, 1] + word[i + 2..-1] }
204
+ alteration = []
205
+ n.times { |i| LETTERS.each_byte { |l| alteration << word[0...i] + l.chr + word[i + 1..-1] } }
206
+ insertion = []
207
+ (n + 1).times { |i| LETTERS.each_byte { |l| insertion << word[0...i] + l.chr + word[i..-1] } }
208
+ deletion + transposition + alteration + insertion
209
+ end
210
+
211
+ def normalize_query(query)
212
+ tokenize(query.to_s.gsub("&", "and")).map { |q| @stemmer.stem(q) }.sort.join
213
+ end
214
+
215
+ def add_nodes(var, words)
216
+ words.each do |word|
217
+ node = var
218
+ tokenize(word).each do |token|
219
+ node = (node[token] ||= {})
220
+ end
221
+ node[:eos] = true
222
+ end
223
+ var
224
+ end
225
+ end
226
+ end
@@ -1,3 +1,3 @@
1
- class Autosuggest
2
- VERSION = "0.1.2"
1
+ module Autosuggest
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/autosuggest.rb CHANGED
@@ -7,226 +7,11 @@ require "lingua/stemmer"
7
7
  require "obscenity"
8
8
 
9
9
  # modules
10
- require "autosuggest/version"
10
+ require_relative "autosuggest/generator"
11
+ require_relative "autosuggest/version"
11
12
 
12
- class Autosuggest
13
- def initialize(top_queries)
14
- @top_queries = top_queries
15
- @concepts = {}
16
- @words = Set.new
17
- @non_duplicates = Set.new
18
- @blocked_words = {}
19
- @blacklisted_words = {}
20
- @preferred_queries = {}
21
- @profane_words = {}
22
- @concept_tree = {}
23
- add_nodes(@profane_words, Obscenity::Base.blacklist)
24
- end
25
-
26
- def add_concept(name, values)
27
- values = values.compact.uniq
28
- add_nodes(@concept_tree, values)
29
- @concepts[name] = Set.new(values.map(&:downcase))
30
- end
31
-
32
- def parse_words(phrases, options = {})
33
- min = options[:min] || 1
34
-
35
- word_counts = Hash.new(0)
36
- phrases.each do |phrase|
37
- words = tokenize(phrase)
38
- words.each do |word|
39
- word_counts[word] += 1
40
- end
41
- end
42
-
43
- word_counts.select { |_, c| c >= min }.each do |word, _|
44
- @words << word
45
- end
46
-
47
- word_counts
48
- end
49
-
50
- def not_duplicates(pairs)
51
- pairs.each do |pair|
52
- @non_duplicates << pair.map(&:downcase).sort
53
- end
54
- end
55
-
56
- def block_words(words)
57
- add_nodes(@blocked_words, words)
58
- words
59
- end
60
-
61
- def blacklist_words(words)
62
- warn "[autosuggest] blacklist_words is deprecated. Use block_words instead."
63
- add_nodes(@blacklisted_words, words)
64
- words
65
- end
66
-
67
- def prefer(queries)
68
- queries.each do |query|
69
- @preferred_queries[normalize_query(query)] ||= query
70
- end
71
- end
72
-
73
- # TODO add queries method for filter: false and make suggestions use filter: true in 0.2.0
74
- def suggestions(filter: false)
75
- stemmed_queries = {}
76
- added_queries = Set.new
77
- results = @top_queries.sort_by { |_query, count| -count }.map do |query, count|
78
- query = query.to_s
79
-
80
- # TODO do not ignore silently
81
- next if query.length < 2
82
-
83
- stemmed_query = normalize_query(query)
84
-
85
- # get preferred term
86
- preferred_query = @preferred_queries[stemmed_query]
87
- if preferred_query && preferred_query != query
88
- original_query, query = query, preferred_query
89
- end
90
-
91
- # exclude duplicates
92
- duplicate = stemmed_queries[stemmed_query]
93
- stemmed_queries[stemmed_query] ||= query
94
-
95
- # also detect possibly misspelled duplicates
96
- # TODO use top query as duplicate
97
- if !duplicate && query.length > 4
98
- edits(query).each do |edited_query|
99
- if added_queries.include?(edited_query)
100
- duplicate = edited_query
101
- break
102
- end
103
- end
104
- end
105
- if duplicate && @non_duplicates.include?([duplicate, query].sort)
106
- duplicate = nil
107
- end
108
- added_queries << query unless duplicate
109
-
110
- # find concepts
111
- concepts = []
112
- @concepts.each do |name, values|
113
- concepts << name if values.include?(query)
114
- end
115
-
116
- tokens = tokenize(query)
117
-
118
- # exclude misspellings that are not brands
119
- misspelling = @words.any? && misspellings?(tokens)
120
-
121
- profane = blocked?(tokens, @profane_words)
122
- blocked = blocked?(tokens, @blocked_words)
123
- blacklisted = blocked?(tokens, @blacklisted_words)
124
-
125
- notes = []
126
- notes << "duplicate of #{duplicate}" if duplicate
127
- notes.concat(concepts)
128
- notes << "misspelling" if misspelling
129
- notes << "profane" if profane
130
- notes << "blocked" if blocked
131
- notes << "blacklisted" if blacklisted
132
- notes << "originally #{original_query}" if original_query
133
-
134
- result = {
135
- query: query,
136
- original_query: original_query,
137
- score: count,
138
- duplicate: duplicate,
139
- concepts: concepts,
140
- misspelling: misspelling,
141
- profane: profane,
142
- blocked: blocked
143
- }
144
- result[:blacklisted] = blacklisted if @blacklisted_words.any?
145
- result[:notes] = notes
146
- result
147
- end
148
- if filter
149
- results.reject! { |s| s[:duplicate] || s[:misspelling] || s[:profane] || s[:blocked] }
150
- end
151
- results
152
- end
153
-
154
- def pretty_suggestions
155
- str = "%-30s %5s %s\n" % %w(Query Score Notes)
156
- suggestions.each do |suggestion|
157
- str << "%-30s %5d %s\n" % [suggestion[:query], suggestion[:score], suggestion[:notes].join(", ")]
158
- end
159
- str
160
- end
161
-
162
- protected
163
-
164
- def misspellings?(tokens)
165
- pos = [0]
166
- while i = pos.shift
167
- return false if i == tokens.size
168
-
169
- if @words.include?(tokens[i])
170
- pos << i + 1
171
- end
172
-
173
- node = @concept_tree[tokens[i]]
174
- j = i
175
- while node
176
- j += 1
177
- pos << j if node[:eos]
178
- break if j == tokens.size
179
- node = node[tokens[j]]
180
- end
181
-
182
- pos.uniq!
183
- end
184
- true
185
- end
186
-
187
- def blocked?(tokens, blocked_words)
188
- tokens.each_with_index do |token, i|
189
- node = blocked_words[token]
190
- j = i
191
- while node
192
- return true if node[:eos]
193
- j += 1
194
- break if j == tokens.size
195
- node = node[tokens[j]]
196
- end
197
- end
198
- false
199
- end
200
-
201
- def tokenize(str)
202
- str.to_s.downcase.split(" ")
203
- end
204
-
205
- # from https://blog.lojic.com/2008/09/04/how-to-write-a-spelling-corrector-in-ruby/
206
- LETTERS = ("a".."z").to_a.join + "'"
207
- def edits(word)
208
- n = word.length
209
- deletion = (0...n).collect { |i| word[0...i] + word[i + 1..-1] }
210
- transposition = (0...n - 1).collect { |i| word[0...i] + word[i + 1, 1] + word[i, 1] + word[i + 2..-1] }
211
- alteration = []
212
- n.times { |i| LETTERS.each_byte { |l| alteration << word[0...i] + l.chr + word[i + 1..-1] } }
213
- insertion = []
214
- (n + 1).times { |i| LETTERS.each_byte { |l| insertion << word[0...i] + l.chr + word[i..-1] } }
215
- deletion + transposition + alteration + insertion
216
- end
217
-
218
- def normalize_query(query)
219
- tokenize(query.to_s.gsub("&", "and")).map { |q| Lingua.stemmer(q) }.sort.join
220
- end
221
-
222
- def add_nodes(var, words)
223
- words.each do |word|
224
- node = var
225
- tokenize(word).each do |token|
226
- node = (node[token] ||= {})
227
- end
228
- node[:eos] = true
229
- end
230
- var
13
+ module Autosuggest
14
+ def self.new(*args, **options)
15
+ Generator.new(*args, **options)
231
16
  end
232
17
  end
@@ -0,0 +1,33 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Autosuggest
4
+ module Generators
5
+ class SuggestionsGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ def copy_templates
10
+ template "model.rb", "app/models/autosuggest/suggestion.rb"
11
+ migration_template "migration.rb", "db/migrate/create_autosuggest_suggestions.rb", migration_version: migration_version
12
+ end
13
+
14
+ def migration_version
15
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
16
+ end
17
+
18
+ def mysql?
19
+ adapter =~ /mysql/i
20
+ end
21
+
22
+ # use connection_config instead of connection.adapter
23
+ # so database connection isn't needed
24
+ def adapter
25
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
26
+ ActiveRecord::Base.connection_db_config.adapter.to_s
27
+ else
28
+ ActiveRecord::Base.connection_config[:adapter].to_s
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :autosuggest_suggestions do |t|
4
+ t.string :query
5
+ t.float :score
6
+ t.datetime :updated_at<%= mysql? ? ", precision: 6" : "" %>
7
+ end
8
+
9
+ add_index :autosuggest_suggestions, :query, unique: true
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ class Autosuggest::Suggestion < ApplicationRecord
2
+ self.table_name = "autosuggest_suggestions"
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: autosuggest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-23 00:00:00.000000000 Z
11
+ date: 2023-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-stemmer
@@ -48,7 +48,11 @@ files:
48
48
  - LICENSE.txt
49
49
  - README.md
50
50
  - lib/autosuggest.rb
51
+ - lib/autosuggest/generator.rb
51
52
  - lib/autosuggest/version.rb
53
+ - lib/generators/autosuggest/suggestions_generator.rb
54
+ - lib/generators/autosuggest/templates/migration.rb.tt
55
+ - lib/generators/autosuggest/templates/model.rb.tt
52
56
  homepage: https://github.com/ankane/autosuggest
53
57
  licenses:
54
58
  - MIT
@@ -61,14 +65,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
61
65
  requirements:
62
66
  - - ">="
63
67
  - !ruby/object:Gem::Version
64
- version: '2.4'
68
+ version: '2.7'
65
69
  required_rubygems_version: !ruby/object:Gem::Requirement
66
70
  requirements:
67
71
  - - ">="
68
72
  - !ruby/object:Gem::Version
69
73
  version: '0'
70
74
  requirements: []
71
- rubygems_version: 3.2.22
75
+ rubygems_version: 3.4.1
72
76
  signing_key:
73
77
  specification_version: 4
74
78
  summary: Generate autocomplete suggestions based on what your users search