pg_fulltext 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []