pg_fulltext 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/pg_fulltext/active_record.rb +76 -0
- data/lib/pg_fulltext/query.rb +66 -0
- data/lib/pg_fulltext.rb +4 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 49c8f3b3250945a7e3e600a100951abffe80a0537e14fcf392d96778d9648777
|
4
|
+
data.tar.gz: efef8f9be9540e961015472dc3f498203c62a37856ef4f7c66f00fad30e7e5a2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2c324ce42e53dd725bec4ee7e11a8763927fe8080059a504ba31e5546b8d528ee76c040ae4a93c6b272a744d413f695583d413d991d1db6afb846f2e8750162e
|
7
|
+
data.tar.gz: 90e7cd23c65e9ee883c4c65c8fb5c21d08078eedf134f9d42cf989faf8928e2edc05a40ab008c07b367191326873773704de5bc458588aa5c0bcff7edce2ae11
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module PgFulltext
|
4
|
+
module ActiveRecord
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Class method to add a scope to the class, searching against `tsv` column by default
|
12
|
+
def add_search_scope(name = :search, **options)
|
13
|
+
self.scope name, -> (query) {
|
14
|
+
apply_search_to_relation(self, query, **options)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply_search_to_relation(
|
19
|
+
relation,
|
20
|
+
query,
|
21
|
+
tsvector_column: :tsv,
|
22
|
+
search_type: :simple,
|
23
|
+
order: true,
|
24
|
+
reorder: false,
|
25
|
+
any_word: false,
|
26
|
+
ignore_accents: false
|
27
|
+
)
|
28
|
+
serial = SecureRandom.hex(4)
|
29
|
+
table_quoted = connection.quote_table_name(table_name)
|
30
|
+
pk_quoted = "#{table_quoted}.#{connection.quote_column_name(primary_key)}"
|
31
|
+
fulltext_join_name = "pg_fulltext_#{serial}"
|
32
|
+
|
33
|
+
# Build the search relation to join on
|
34
|
+
search_relation = get_search_relation(
|
35
|
+
relation,
|
36
|
+
query,
|
37
|
+
tsvector_column: tsvector_column,
|
38
|
+
search_type: search_type,
|
39
|
+
any_word: any_word,
|
40
|
+
ignore_accents: ignore_accents,
|
41
|
+
)
|
42
|
+
|
43
|
+
# Join the search relation
|
44
|
+
relation = relation.joins("INNER JOIN (#{search_relation.to_sql}) AS #{fulltext_join_name} ON #{fulltext_join_name}.id = #{pk_quoted}")
|
45
|
+
|
46
|
+
# Order/reorder against the search rank
|
47
|
+
if order || reorder
|
48
|
+
relation = relation.send(reorder ? :reorder : :order, "#{fulltext_join_name}.rank DESC")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return the model relation
|
52
|
+
relation
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_search_relation(
|
56
|
+
relation,
|
57
|
+
query,
|
58
|
+
tsvector_column: :tsv,
|
59
|
+
search_type: nil,
|
60
|
+
any_word: false,
|
61
|
+
ignore_accents: false
|
62
|
+
)
|
63
|
+
tsquery_string_quoted = connection.quote(PgFulltext::Query.to_tsquery_string(query, operator: any_word ? '|' : '&'))
|
64
|
+
tsquery_string_quoted = "unaccent(#{tsquery_string_quoted})" if ignore_accents
|
65
|
+
table_quoted = connection.quote_table_name(table_name)
|
66
|
+
column_quoted = connection.quote_column_name(tsvector_column)
|
67
|
+
fqc_quoted = "#{table_quoted}.#{column_quoted}"
|
68
|
+
tsquery = "to_tsquery(#{"#{connection.quote search_type}, " if search_type.present?}#{tsquery_string_quoted})"
|
69
|
+
|
70
|
+
relation
|
71
|
+
.select(:id, "ts_rank_cd(#{fqc_quoted}, #{tsquery}) AS rank")
|
72
|
+
.where("#{fqc_quoted} @@ #{tsquery}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module PgFulltext
|
2
|
+
module Query
|
3
|
+
def self.to_tsquery_string(query, prefix: true, operator: '&')
|
4
|
+
|
5
|
+
# Parse out all [unicode] non-word and non-quote characters
|
6
|
+
query.gsub!(/[^\s\p{L}"!]/, '')
|
7
|
+
query.gsub!(/"+/, '"')
|
8
|
+
query.gsub!(/\s+/, ' ')
|
9
|
+
|
10
|
+
# Collect terms
|
11
|
+
terms = []
|
12
|
+
|
13
|
+
# Phrase mode
|
14
|
+
if query.count('"') > 0 && query.count('"') % 2 == 0
|
15
|
+
phrase_terms = []
|
16
|
+
negate_phrase = false
|
17
|
+
|
18
|
+
query_parts = query.split(' ')
|
19
|
+
query_parts.each do |term|
|
20
|
+
|
21
|
+
# Skip if completely comprised of non-unicode word characters
|
22
|
+
next if term.gsub(/[^\s\p{L}]/, '') == ''
|
23
|
+
|
24
|
+
if term.start_with?('!"') && !term.end_with?('"')
|
25
|
+
phrase_terms << format_term(term[2..-1], prefix: true)
|
26
|
+
negate_phrase = true
|
27
|
+
elsif term.start_with?('"') && !term.end_with?('"')
|
28
|
+
phrase_terms << format_term(term[1..-1], prefix: true)
|
29
|
+
elsif phrase_terms.length > 0
|
30
|
+
if term.end_with?('"')
|
31
|
+
phrase_terms << format_term(term[0..-2], prefix: prefix)
|
32
|
+
terms << "#{'!' if negate_phrase}(#{reject_falsy(phrase_terms, prefix: prefix).join(' <-> ')})"
|
33
|
+
phrase_terms = []
|
34
|
+
negate_phrase = false
|
35
|
+
else
|
36
|
+
phrase_terms << format_term(term, prefix: prefix)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
terms << format_term(term, prefix: prefix)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
else
|
43
|
+
query.gsub! /["]/, ''
|
44
|
+
terms = reject_falsy(query.split(' ').map { |v| format_term(v, prefix: prefix) }, prefix: prefix)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Join terms with operator
|
48
|
+
terms.join(" #{operator} ")
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def self.format_term(term, prefix: true)
|
54
|
+
# Remove any ! that's not at the beginning of the term, as it will break the query
|
55
|
+
term.gsub!(/(?<!^)!/, '')
|
56
|
+
|
57
|
+
# Add the prefix if prefix is set
|
58
|
+
"#{term}#{':*' if prefix}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.reject_falsy(terms, prefix: true)
|
62
|
+
false_values = [nil, '', '"', '!', ':*', '":*', '!:*']
|
63
|
+
terms.reject { |v| false_values.include?(v) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/pg_fulltext.rb
ADDED
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_fulltext
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Robertson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Allows simple searching with a variety of options
|
14
|
+
email: adam@arcreative.net
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/pg_fulltext.rb
|
20
|
+
- lib/pg_fulltext/active_record.rb
|
21
|
+
- lib/pg_fulltext/query.rb
|
22
|
+
homepage: https://github.com/arcreative/pg_fulltext
|
23
|
+
licenses:
|
24
|
+
- MIT
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubygems_version: 3.1.4
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: PostgreSQL fulltext search
|
45
|
+
test_files: []
|