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 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
@@ -0,0 +1,4 @@
1
+ module PgFulltext; end
2
+
3
+ require 'pg_fulltext/active_record'
4
+ require 'pg_fulltext/query'
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: []