searchable-by 0.5.9 → 0.6.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 +4 -4
- data/.github/workflows/{ruby.yml → test.yml} +2 -2
- data/Gemfile.lock +23 -30
- data/README.md +4 -2
- data/lib/searchable_by/column.rb +15 -5
- data/lib/searchable_by/concern.rb +6 -4
- data/lib/searchable_by/config.rb +2 -1
- data/lib/searchable_by/util.rb +15 -12
- data/searchable-by.gemspec +1 -3
- data/spec/searchable_by/util_spec.rb +3 -2
- data/spec/searchable_by_spec.rb +12 -5
- data/spec/spec_helper.rb +4 -4
- metadata +4 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 615602791390c9de0ab159cbbd0c81c52c0441f61ad559082dad189787c7606f
|
4
|
+
data.tar.gz: ad765f8a22cd76196f7a04b7a7e16f38b3bbccacda0f1c7bda659e3ab2783f4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae524aa23d507f4339592c4c6a2955d3d0664e7f9829be7071ed1d28a065e1ad77ff9a810a800769cf417db4d315344a487b058530e168325403cb26a1832e12
|
7
|
+
data.tar.gz: 71a1349d3738997571306d5326cf81bef66b101bbd03b9451d3b3d9b5851c35f1d732486d96b29d73e1efce792a0c5c840c42442a2f83bd5be171a47be28a17c
|
data/Gemfile.lock
CHANGED
@@ -1,38 +1,36 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
searchable-by (0.
|
4
|
+
searchable-by (0.6.0)
|
5
5
|
activerecord
|
6
|
-
activesupport
|
7
6
|
|
8
7
|
GEM
|
9
8
|
remote: http://rubygems.org/
|
10
9
|
specs:
|
11
|
-
activemodel (6.1.
|
12
|
-
activesupport (= 6.1.
|
13
|
-
activerecord (6.1.
|
14
|
-
activemodel (= 6.1.
|
15
|
-
activesupport (= 6.1.
|
16
|
-
activesupport (6.1.
|
10
|
+
activemodel (6.1.4)
|
11
|
+
activesupport (= 6.1.4)
|
12
|
+
activerecord (6.1.4)
|
13
|
+
activemodel (= 6.1.4)
|
14
|
+
activesupport (= 6.1.4)
|
15
|
+
activesupport (6.1.4)
|
17
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
17
|
i18n (>= 1.6, < 2)
|
19
18
|
minitest (>= 5.1)
|
20
19
|
tzinfo (~> 2.0)
|
21
20
|
zeitwerk (~> 2.3)
|
22
21
|
ast (2.4.2)
|
23
|
-
concurrent-ruby (1.1.
|
22
|
+
concurrent-ruby (1.1.9)
|
24
23
|
diff-lcs (1.4.4)
|
25
|
-
i18n (1.8.
|
24
|
+
i18n (1.8.10)
|
26
25
|
concurrent-ruby (~> 1.0)
|
27
26
|
minitest (5.14.4)
|
28
27
|
parallel (1.20.1)
|
29
|
-
parser (3.0.
|
28
|
+
parser (3.0.2.0)
|
30
29
|
ast (~> 2.4.1)
|
31
|
-
rack (2.2.3)
|
32
30
|
rainbow (3.0.0)
|
33
|
-
rake (13.0.
|
31
|
+
rake (13.0.6)
|
34
32
|
regexp_parser (2.1.1)
|
35
|
-
rexml (3.2.
|
33
|
+
rexml (3.2.5)
|
36
34
|
rspec (3.10.0)
|
37
35
|
rspec-core (~> 3.10.0)
|
38
36
|
rspec-expectations (~> 3.10.0)
|
@@ -46,33 +44,28 @@ GEM
|
|
46
44
|
diff-lcs (>= 1.2.0, < 2.0)
|
47
45
|
rspec-support (~> 3.10.0)
|
48
46
|
rspec-support (3.10.2)
|
49
|
-
rubocop (1.
|
47
|
+
rubocop (1.18.3)
|
50
48
|
parallel (~> 1.10)
|
51
49
|
parser (>= 3.0.0.0)
|
52
50
|
rainbow (>= 2.2.2, < 4.0)
|
53
51
|
regexp_parser (>= 1.8, < 3.0)
|
54
52
|
rexml
|
55
|
-
rubocop-ast (>= 1.
|
53
|
+
rubocop-ast (>= 1.7.0, < 2.0)
|
56
54
|
ruby-progressbar (~> 1.7)
|
57
55
|
unicode-display_width (>= 1.4.0, < 3.0)
|
58
|
-
rubocop-ast (1.
|
59
|
-
parser (>=
|
60
|
-
rubocop-bsm (0.
|
56
|
+
rubocop-ast (1.7.0)
|
57
|
+
parser (>= 3.0.1.1)
|
58
|
+
rubocop-bsm (0.6.0)
|
61
59
|
rubocop (~> 1.0)
|
62
60
|
rubocop-performance
|
63
|
-
rubocop-rails
|
64
61
|
rubocop-rake
|
65
62
|
rubocop-rspec
|
66
|
-
rubocop-performance (1.
|
67
|
-
rubocop (>=
|
63
|
+
rubocop-performance (1.11.4)
|
64
|
+
rubocop (>= 1.7.0, < 2.0)
|
68
65
|
rubocop-ast (>= 0.4.0)
|
69
|
-
rubocop-
|
70
|
-
|
71
|
-
|
72
|
-
rubocop (>= 0.90.0, < 2.0)
|
73
|
-
rubocop-rake (0.5.1)
|
74
|
-
rubocop
|
75
|
-
rubocop-rspec (2.2.0)
|
66
|
+
rubocop-rake (0.6.0)
|
67
|
+
rubocop (~> 1.0)
|
68
|
+
rubocop-rspec (2.4.0)
|
76
69
|
rubocop (~> 1.0)
|
77
70
|
rubocop-ast (>= 1.1.0)
|
78
71
|
ruby-progressbar (1.11.0)
|
@@ -95,4 +88,4 @@ DEPENDENCIES
|
|
95
88
|
sqlite3
|
96
89
|
|
97
90
|
BUNDLED WITH
|
98
|
-
2.2.
|
91
|
+
2.2.21
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Searchable By
|
2
2
|
|
3
|
+
[](https://github.com/bsm/searchable-by/actions/workflows/test.yml)
|
4
|
+
|
3
5
|
ActiveRecord plugin to quickly create search scopes.
|
4
6
|
|
5
7
|
## Installation
|
@@ -13,12 +15,12 @@ class Post < ActiveRecord::Base
|
|
13
15
|
belongs_to :author
|
14
16
|
|
15
17
|
# Limit the number of terms per query to 3.
|
16
|
-
|
18
|
+
# Ignore search terms shorter than 3 characters (useful for trigram indexes).
|
19
|
+
searchable_by max_terms: 3, min_length: 3 do
|
17
20
|
# Allow to search strings with custom match type.
|
18
21
|
column :title,
|
19
22
|
match: :prefix, # Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
|
20
23
|
match_phrase: :exact, # For phrases use exact match type, e.g. searching for `"My Post"` will query `WHERE LOWER(title) = 'my post'`.
|
21
|
-
min_length: 3 # Return no-match if search term is too short (useful for trigram indexes).
|
22
24
|
|
23
25
|
# ... and integers.
|
24
26
|
column :id, type: :integer
|
data/lib/searchable_by/column.rb
CHANGED
@@ -1,20 +1,25 @@
|
|
1
1
|
module SearchableBy
|
2
2
|
class Column
|
3
|
-
attr_reader :attr, :type, :match, :match_phrase, :
|
3
|
+
attr_reader :attr, :type, :match, :match_phrase, :wildcard
|
4
4
|
attr_accessor :node
|
5
5
|
|
6
|
-
def initialize(attr, type: :string, match: :all, match_phrase: nil,
|
6
|
+
def initialize(attr, type: :string, match: :all, match_phrase: nil, wildcard: nil, **opts) # rubocop:disable Metrics/ParameterLists
|
7
|
+
if opts.key?(:min_length)
|
8
|
+
ActiveSupport::Deprecation.warn(
|
9
|
+
'Setting min_length for individual columns is deprecated and will be removed in the next release.' \
|
10
|
+
'Please pass it as an option to searchable_by instead',
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
7
14
|
@attr = attr
|
8
15
|
@type = type.to_sym
|
9
16
|
@match = match
|
10
17
|
@match_phrase = match_phrase || match
|
11
|
-
@min_length = min_length
|
18
|
+
@min_length = opts[:min_length].to_i
|
12
19
|
@wildcard = wildcard
|
13
20
|
end
|
14
21
|
|
15
22
|
def build_condition(value)
|
16
|
-
return Arel::Nodes::False.new if value.term.length < min_length # no-match
|
17
|
-
|
18
23
|
scope = node.not_eq(nil)
|
19
24
|
|
20
25
|
case type
|
@@ -27,6 +32,11 @@ module SearchableBy
|
|
27
32
|
|
28
33
|
private
|
29
34
|
|
35
|
+
# TODO: remove when removing min_length option from columns
|
36
|
+
def usable?(value)
|
37
|
+
value.term.length >= @min_length
|
38
|
+
end
|
39
|
+
|
30
40
|
def int_condition(scope, value)
|
31
41
|
scope.and(node.eq(Integer(value.term)))
|
32
42
|
rescue ArgumentError
|
@@ -11,9 +11,10 @@ module SearchableBy
|
|
11
11
|
super
|
12
12
|
end
|
13
13
|
|
14
|
-
def searchable_by(max_terms: nil, **options, &block)
|
14
|
+
def searchable_by(max_terms: nil, min_length: 0, **options, &block)
|
15
15
|
_searchable_by_config.instance_eval(&block)
|
16
16
|
_searchable_by_config.max_terms = max_terms if max_terms
|
17
|
+
_searchable_by_config.min_length = min_length
|
17
18
|
_searchable_by_config.options.update(options) unless options.empty?
|
18
19
|
_searchable_by_config
|
19
20
|
end
|
@@ -21,10 +22,11 @@ module SearchableBy
|
|
21
22
|
# @param [String] query the search query
|
22
23
|
# @return [ActiveRecord::Relation] the scoped relation
|
23
24
|
def search_by(query)
|
24
|
-
|
25
|
+
config = _searchable_by_config
|
26
|
+
columns = config.columns
|
25
27
|
return all if columns.empty?
|
26
28
|
|
27
|
-
values = Util.norm_values(query).first(
|
29
|
+
values = Util.norm_values(query, min_length: config.min_length).first(config.max_terms)
|
28
30
|
return all if values.empty?
|
29
31
|
|
30
32
|
columns.each do |col|
|
@@ -33,7 +35,7 @@ module SearchableBy
|
|
33
35
|
clauses = Util.build_clauses(columns, values)
|
34
36
|
return all if clauses.empty?
|
35
37
|
|
36
|
-
scope = instance_exec(&
|
38
|
+
scope = instance_exec(&config.scoping)
|
37
39
|
clauses.each do |clause|
|
38
40
|
scope = scope.where(clause)
|
39
41
|
end
|
data/lib/searchable_by/config.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
module SearchableBy
|
2
2
|
class Config
|
3
3
|
attr_reader :columns, :scoping, :options
|
4
|
-
attr_accessor :max_terms
|
4
|
+
attr_accessor :max_terms, :min_length
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
@columns = []
|
8
8
|
@max_terms = 5
|
9
|
+
@min_length = 0
|
9
10
|
@options = {}
|
10
11
|
scope { all }
|
11
12
|
end
|
data/lib/searchable_by/util.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SearchableBy
|
2
2
|
module Util
|
3
|
-
def self.norm_values(query)
|
3
|
+
def self.norm_values(query, min_length: 0)
|
4
4
|
values = []
|
5
5
|
query = query.to_s.dup
|
6
6
|
|
@@ -10,7 +10,7 @@ module SearchableBy
|
|
10
10
|
term = Regexp.last_match(2)
|
11
11
|
negate = Regexp.last_match(1) == '-'
|
12
12
|
|
13
|
-
values.push Value.new(term, negate, true) unless term.blank?
|
13
|
+
values.push Value.new(term, negate, true) unless term.blank? || term.length < min_length
|
14
14
|
''
|
15
15
|
end
|
16
16
|
|
@@ -20,7 +20,7 @@ module SearchableBy
|
|
20
20
|
negate = term[0] == '-'
|
21
21
|
term.slice!(0) if negate || term[0] == '+'
|
22
22
|
|
23
|
-
values.push Value.new(term, negate, false) unless term.blank?
|
23
|
+
values.push Value.new(term, negate, false) unless term.blank? || term.length < min_length
|
24
24
|
end
|
25
25
|
|
26
26
|
values.uniq!
|
@@ -28,19 +28,22 @@ module SearchableBy
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.build_clauses(columns, values)
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
values.map do |value|
|
32
|
+
# TODO: remove when removing min_length option from columns
|
33
|
+
usable = columns.all? do |column|
|
34
|
+
column.send(:usable?, value)
|
34
35
|
end
|
35
|
-
|
36
|
-
|
36
|
+
next unless usable
|
37
|
+
|
38
|
+
group = columns.map do |column|
|
39
|
+
column.build_condition(value)
|
40
|
+
end.tap(&:compact!)
|
41
|
+
next if group.empty?
|
37
42
|
|
38
|
-
clause =
|
43
|
+
clause = group.inject(&:or)
|
39
44
|
clause = clause.not if value.negate
|
40
45
|
clause
|
41
|
-
end
|
42
|
-
clauses.compact!
|
43
|
-
clauses
|
46
|
+
end.tap(&:compact!)
|
44
47
|
end
|
45
48
|
end
|
46
49
|
end
|
data/searchable-by.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'searchable-by'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.6.0'
|
4
4
|
s.authors = ['Dimitrij Denissenko']
|
5
5
|
s.email = ['dimitrij@blacksquaremedia.com']
|
6
6
|
s.summary = 'Generate search scopes'
|
@@ -14,8 +14,6 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.required_ruby_version = '>= 2.6'
|
15
15
|
|
16
16
|
s.add_dependency 'activerecord'
|
17
|
-
s.add_dependency 'activesupport'
|
18
|
-
|
19
17
|
s.add_development_dependency 'bundler'
|
20
18
|
s.add_development_dependency 'rake'
|
21
19
|
s.add_development_dependency 'rspec'
|
@@ -2,8 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe SearchableBy::Util do
|
4
4
|
context 'with norm_values' do
|
5
|
-
def norm(str)
|
6
|
-
described_class.norm_values(str).each_with_object({}) do |val, acc|
|
5
|
+
def norm(str, **opts)
|
6
|
+
described_class.norm_values(str, **opts).each_with_object({}) do |val, acc|
|
7
7
|
acc[val.term] = val.negate
|
8
8
|
end
|
9
9
|
end
|
@@ -30,6 +30,7 @@ describe SearchableBy::Util do
|
|
30
30
|
expect(norm('+plus "in other term"')).to eq('in other term' => false, 'plus' => false)
|
31
31
|
expect(norm('with_blank \'\'')).to eq('with_blank' => false, '\'\'' => false)
|
32
32
|
expect(norm('with_blank_doubles ""')).to eq('with_blank_doubles' => false)
|
33
|
+
expect(norm('with min length', min_length: 4)).to eq('length' => false, 'with' => false)
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
data/spec/searchable_by_spec.rb
CHANGED
@@ -67,9 +67,16 @@ describe SearchableBy do
|
|
67
67
|
expect(Post.search_by('recip').pluck(:title)).to match_array(%w[ax1 ax2 bx1 bx2 ab1])
|
68
68
|
end
|
69
69
|
|
70
|
-
it 'supports min term length' do
|
71
|
-
|
72
|
-
expect(User.search_by('
|
70
|
+
it 'supports min term length in context' do
|
71
|
+
# values are discarded - too short for bio
|
72
|
+
expect(User.search_by('+be')).to match_array(USERS.values_at(:a, :b))
|
73
|
+
expect(User.search_by('is')).to match_array(USERS.values_at(:a, :b))
|
74
|
+
|
75
|
+
# value is used to scope
|
76
|
+
expect(User.search_by('ear')).to match_array(USERS.values_at(:a))
|
77
|
+
|
78
|
+
# one used, one discarded
|
79
|
+
expect(User.search_by('beard is')).to match_array(USERS.values_at(:a))
|
73
80
|
end
|
74
81
|
|
75
82
|
it 'searches within scopes' do
|
@@ -82,8 +89,8 @@ describe SearchableBy do
|
|
82
89
|
end
|
83
90
|
|
84
91
|
it 'supports wildcard searching' do
|
85
|
-
expect(User.search_by('uni*dom')).to match_array(
|
86
|
-
expect(User.search_by('uni*o')).to match_array(
|
92
|
+
expect(User.search_by('uni*dom')).to match_array(USERS.values_at(:a))
|
93
|
+
expect(User.search_by('uni*o')).to match_array(USERS.values_at(:a, :b))
|
87
94
|
expect(User.search_by('uni*of*dom')).to be_empty
|
88
95
|
end
|
89
96
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -30,8 +30,8 @@ end
|
|
30
30
|
class User < AbstractModel
|
31
31
|
has_many :posts, foreign_key: :author_id
|
32
32
|
|
33
|
-
searchable_by do
|
34
|
-
column :bio
|
33
|
+
searchable_by min_length: 3 do
|
34
|
+
column :bio
|
35
35
|
column :country, wildcard: '*'
|
36
36
|
end
|
37
37
|
end
|
@@ -53,8 +53,8 @@ class Post < AbstractModel
|
|
53
53
|
end
|
54
54
|
|
55
55
|
USERS = {
|
56
|
-
a: User.create!(name: 'Alice', bio: '
|
57
|
-
b: User.create!(name: 'Bob', bio: '
|
56
|
+
a: User.create!(name: 'Alice', bio: "Chuck Norris' beard is immutable.", country: 'United Kingdom'),
|
57
|
+
b: User.create!(name: 'Bob', bio: 'Chuck Norris can divide by zero.', country: 'United States of America'),
|
58
58
|
}.freeze
|
59
59
|
|
60
60
|
POSTS = {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: searchable-by
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitrij Denissenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: activesupport
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: bundler
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -129,7 +115,7 @@ executables: []
|
|
129
115
|
extensions: []
|
130
116
|
extra_rdoc_files: []
|
131
117
|
files:
|
132
|
-
- ".github/workflows/
|
118
|
+
- ".github/workflows/test.yml"
|
133
119
|
- ".gitignore"
|
134
120
|
- ".rubocop.yml"
|
135
121
|
- Gemfile
|
@@ -166,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
152
|
- !ruby/object:Gem::Version
|
167
153
|
version: '0'
|
168
154
|
requirements: []
|
169
|
-
rubygems_version: 3.
|
155
|
+
rubygems_version: 3.2.15
|
170
156
|
signing_key:
|
171
157
|
specification_version: 4
|
172
158
|
summary: Generate search scopes
|