pg_fulltext 0.1.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 +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: []
|