pg-searchable 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: eaf2e17acef704a8fd936d20b9716dace4e9f4319a26f8d72e4f4a79fa515bde
4
+ data.tar.gz: 8153451549b6fd494227f9c01377b0021241026188509778b2c3c430b087fa23
5
+ SHA512:
6
+ metadata.gz: 70964f08dad60214f6d87f7f5f2497ce7118ad92d3ced7df0d79bcf31435bb45d9d269ff096e700d27c840df4b67383435b1ef96978e6d95429756d9d5ab6e99
7
+ data.tar.gz: 4904c0b55c7c847f15a1f2b2097a684a577c3ae28d1380bd9fed5f290db3fc899e5b2e8fde7f26212e5a13921a1fb05228213e272683589ccf15e05718d157a3
data/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ root = true
2
+
3
+ [**.{rb,md}]
4
+ indent_size = 2
5
+ indent_style = space
@@ -0,0 +1,32 @@
1
+ name: Test
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ rspec-test:
7
+ name: RSpec
8
+ runs-on: ubuntu-18.04
9
+ services:
10
+ postgres:
11
+ image: postgres:12
12
+ env:
13
+ POSTGRES_USER: postgres
14
+ POSTGRES_PASSWORD: postgres
15
+ POSTGRES_DB: postgres_test
16
+ ports:
17
+ - 5432:5432
18
+ # needed because the postgres container does not provide a healthcheck
19
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
20
+ steps:
21
+ - uses: actions/checkout@v1
22
+ - uses: actions/setup-ruby@v1
23
+ with:
24
+ ruby-version: 2.5
25
+ - name: Install postgres client
26
+ run: sudo apt-get install libpq-dev
27
+ - name: Install dependencies
28
+ run: |
29
+ gem install bundler
30
+ bundler install
31
+ - name: Run tests
32
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ spec/log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'rake'
5
+ gem 'rspec'
6
+ gem 'rails'
7
+ gem 'rspec-rails'
8
+ gem 'pg'
9
+ gem 'combustion', '~> 1.1'
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,167 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ actioncable (6.0.2.2)
5
+ actionpack (= 6.0.2.2)
6
+ nio4r (~> 2.0)
7
+ websocket-driver (>= 0.6.1)
8
+ actionmailbox (6.0.2.2)
9
+ actionpack (= 6.0.2.2)
10
+ activejob (= 6.0.2.2)
11
+ activerecord (= 6.0.2.2)
12
+ activestorage (= 6.0.2.2)
13
+ activesupport (= 6.0.2.2)
14
+ mail (>= 2.7.1)
15
+ actionmailer (6.0.2.2)
16
+ actionpack (= 6.0.2.2)
17
+ actionview (= 6.0.2.2)
18
+ activejob (= 6.0.2.2)
19
+ mail (~> 2.5, >= 2.5.4)
20
+ rails-dom-testing (~> 2.0)
21
+ actionpack (6.0.2.2)
22
+ actionview (= 6.0.2.2)
23
+ activesupport (= 6.0.2.2)
24
+ rack (~> 2.0, >= 2.0.8)
25
+ rack-test (>= 0.6.3)
26
+ rails-dom-testing (~> 2.0)
27
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
28
+ actiontext (6.0.2.2)
29
+ actionpack (= 6.0.2.2)
30
+ activerecord (= 6.0.2.2)
31
+ activestorage (= 6.0.2.2)
32
+ activesupport (= 6.0.2.2)
33
+ nokogiri (>= 1.8.5)
34
+ actionview (6.0.2.2)
35
+ activesupport (= 6.0.2.2)
36
+ builder (~> 3.1)
37
+ erubi (~> 1.4)
38
+ rails-dom-testing (~> 2.0)
39
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
40
+ activejob (6.0.2.2)
41
+ activesupport (= 6.0.2.2)
42
+ globalid (>= 0.3.6)
43
+ activemodel (6.0.2.2)
44
+ activesupport (= 6.0.2.2)
45
+ activerecord (6.0.2.2)
46
+ activemodel (= 6.0.2.2)
47
+ activesupport (= 6.0.2.2)
48
+ activestorage (6.0.2.2)
49
+ actionpack (= 6.0.2.2)
50
+ activejob (= 6.0.2.2)
51
+ activerecord (= 6.0.2.2)
52
+ marcel (~> 0.3.1)
53
+ activesupport (6.0.2.2)
54
+ concurrent-ruby (~> 1.0, >= 1.0.2)
55
+ i18n (>= 0.7, < 2)
56
+ minitest (~> 5.1)
57
+ tzinfo (~> 1.1)
58
+ zeitwerk (~> 2.2)
59
+ builder (3.2.4)
60
+ combustion (1.1.2)
61
+ activesupport (>= 3.0.0)
62
+ railties (>= 3.0.0)
63
+ thor (>= 0.14.6)
64
+ concurrent-ruby (1.1.6)
65
+ crass (1.0.6)
66
+ diff-lcs (1.3)
67
+ erubi (1.9.0)
68
+ globalid (0.4.2)
69
+ activesupport (>= 4.2.0)
70
+ i18n (1.8.2)
71
+ concurrent-ruby (~> 1.0)
72
+ loofah (2.4.0)
73
+ crass (~> 1.0.2)
74
+ nokogiri (>= 1.5.9)
75
+ mail (2.7.1)
76
+ mini_mime (>= 0.1.1)
77
+ marcel (0.3.3)
78
+ mimemagic (~> 0.3.2)
79
+ method_source (1.0.0)
80
+ mimemagic (0.3.4)
81
+ mini_mime (1.0.2)
82
+ mini_portile2 (2.4.0)
83
+ minitest (5.14.0)
84
+ nio4r (2.5.2)
85
+ nokogiri (1.10.9)
86
+ mini_portile2 (~> 2.4.0)
87
+ pg (1.2.3)
88
+ rack (2.2.2)
89
+ rack-test (1.1.0)
90
+ rack (>= 1.0, < 3)
91
+ rails (6.0.2.2)
92
+ actioncable (= 6.0.2.2)
93
+ actionmailbox (= 6.0.2.2)
94
+ actionmailer (= 6.0.2.2)
95
+ actionpack (= 6.0.2.2)
96
+ actiontext (= 6.0.2.2)
97
+ actionview (= 6.0.2.2)
98
+ activejob (= 6.0.2.2)
99
+ activemodel (= 6.0.2.2)
100
+ activerecord (= 6.0.2.2)
101
+ activestorage (= 6.0.2.2)
102
+ activesupport (= 6.0.2.2)
103
+ bundler (>= 1.3.0)
104
+ railties (= 6.0.2.2)
105
+ sprockets-rails (>= 2.0.0)
106
+ rails-dom-testing (2.0.3)
107
+ activesupport (>= 4.2.0)
108
+ nokogiri (>= 1.6)
109
+ rails-html-sanitizer (1.3.0)
110
+ loofah (~> 2.3)
111
+ railties (6.0.2.2)
112
+ actionpack (= 6.0.2.2)
113
+ activesupport (= 6.0.2.2)
114
+ method_source
115
+ rake (>= 0.8.7)
116
+ thor (>= 0.20.3, < 2.0)
117
+ rake (13.0.1)
118
+ rspec (3.9.0)
119
+ rspec-core (~> 3.9.0)
120
+ rspec-expectations (~> 3.9.0)
121
+ rspec-mocks (~> 3.9.0)
122
+ rspec-core (3.9.1)
123
+ rspec-support (~> 3.9.1)
124
+ rspec-expectations (3.9.1)
125
+ diff-lcs (>= 1.2.0, < 2.0)
126
+ rspec-support (~> 3.9.0)
127
+ rspec-mocks (3.9.1)
128
+ diff-lcs (>= 1.2.0, < 2.0)
129
+ rspec-support (~> 3.9.0)
130
+ rspec-rails (3.9.1)
131
+ actionpack (>= 3.0)
132
+ activesupport (>= 3.0)
133
+ railties (>= 3.0)
134
+ rspec-core (~> 3.9.0)
135
+ rspec-expectations (~> 3.9.0)
136
+ rspec-mocks (~> 3.9.0)
137
+ rspec-support (~> 3.9.0)
138
+ rspec-support (3.9.2)
139
+ sprockets (4.0.0)
140
+ concurrent-ruby (~> 1.0)
141
+ rack (> 1, < 3)
142
+ sprockets-rails (3.2.1)
143
+ actionpack (>= 4.0)
144
+ activesupport (>= 4.0)
145
+ sprockets (>= 3.0.0)
146
+ thor (1.0.1)
147
+ thread_safe (0.3.6)
148
+ tzinfo (1.2.6)
149
+ thread_safe (~> 0.1)
150
+ websocket-driver (0.7.1)
151
+ websocket-extensions (>= 0.1.0)
152
+ websocket-extensions (0.1.4)
153
+ zeitwerk (2.3.0)
154
+
155
+ PLATFORMS
156
+ ruby
157
+
158
+ DEPENDENCIES
159
+ combustion (~> 1.1)
160
+ pg
161
+ rails
162
+ rake
163
+ rspec
164
+ rspec-rails
165
+
166
+ BUNDLED WITH
167
+ 2.1.2
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ ## pg-searchable
2
+
3
+ pg-searchable is a quick way to make Postgres columns full-text searchable in Rails models, with support for composing complex OR/AND queries.
4
+
5
+ If all you need is a straightforward search with trigram/multi-column support, [pg_search](https://github.com/Casecommons/pg_search) is probably a better fit.
6
+
7
+ ### Install
8
+
9
+ `gem "pg-searchable"`
10
+
11
+ ### Setup
12
+
13
+ ```ruby
14
+ class Product < ActiveRecord::Base
15
+ include PgSearchable::Model
16
+ searchable_column :name
17
+ end
18
+ ```
19
+
20
+ ### Usage
21
+
22
+ ```ruby
23
+ # Add test data
24
+ sour_bread = Product.create!(name: "Sour bread")
25
+ italian_pasta = Product.create!(name: "Italian pasta")
26
+ rye_bread = Product.create!(name: "Rye bread")
27
+ sushi = Product.create!(name: "Sour Sushi Itamae")
28
+
29
+ # Basic options
30
+ bread_condition = Product.search_name("bread")
31
+ expect(Product.where(bread_condition)).to match_array [sour_bread, rye_bread]
32
+
33
+ ita_condition = Product.search_name("ita", prefix: true)
34
+ expect(Product.where(ita_condition)).to match_array [italian_pasta, sushi]
35
+
36
+ nonsour_condition = Product.search_name("!sour")
37
+ expect(Product.where(nonsour_condition)).to match_array [italian_pasta, rye_bread]
38
+
39
+ # Composing searches
40
+ non_sour_breads = Product.where(bread_condition.and(nonsour_condition))
41
+ expect(non_sour_breads).to match_array [rye_bread]
42
+
43
+ nonsour_or_ita = Product.where(nonsour_condition.or(ita_condition))
44
+ expect(nonsour_or_ita).to match_array [rye_bread, italian_pasta, sushi]
45
+ ```
46
+
47
+ ### Development
48
+
49
+ **Testing locally:**
50
+
51
+ 1. Start local Postgres: `bin/local-docker-testing`
52
+ 2. Run tests: `bundle exec rspec`
53
+
54
+ ### Credits
55
+
56
+ pg_search provided much of the inspiration and basis
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ docker-compose -f docker-compose.test.yml up postgres
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
@@ -0,0 +1,10 @@
1
+ version: "3"
2
+ services:
3
+ postgres:
4
+ image: postgres:12.1
5
+ ports:
6
+ - 5432:5432
7
+ environment:
8
+ POSTGRES_USERNAME: postgres
9
+ POSTGRES_PASSWORD: postgres
10
+ POSTGRES_DB: postgres_test
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module PgSearchable
6
+ module Model
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def searchable_column(column_name)
11
+ name = "search_#{column_name.to_s}"
12
+ define_singleton_method(name) do |*args|
13
+ Searcher.new(self).exec(column_name, *args)
14
+ end
15
+ end
16
+ end
17
+
18
+ class Searcher
19
+ def initialize(model)
20
+ @model = model
21
+ end
22
+
23
+ def dictionary
24
+ Arel::Nodes.build_quoted(:simple)
25
+ end
26
+
27
+ def exec(column_name, query, raw:false, prefix:false)
28
+ column_ref = "#{@model.quoted_table_name}.#{column_name}"
29
+
30
+ vector = Arel::Nodes::NamedFunction.new(
31
+ "to_tsvector",
32
+ [dictionary, Arel.sql(column_ref)]
33
+ )
34
+
35
+ query = Builder.tsquery(query, prefix: prefix, raw: raw)
36
+
37
+ condition = Arel::Nodes::Grouping.new(
38
+ Arel::Nodes::InfixOperation.new("@@", vector, query)
39
+ )
40
+
41
+ condition
42
+ end
43
+ end
44
+
45
+ module Builder
46
+
47
+ DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/.freeze
48
+
49
+ def self.dictionary
50
+ Arel::Nodes.build_quoted(:simple)
51
+ end
52
+
53
+ def self.normalize(x)
54
+ x # TODO
55
+ end
56
+
57
+ def self.tsquery_for_terms(terms, prefix:)
58
+ terms = terms.map { |term| tsquery_term(term, prefix: prefix) }
59
+ terms.inject do |memo, term|
60
+ term_anded = Arel::Nodes::InfixOperation.new("||", Arel::Nodes.build_quoted(" & "), term)
61
+ Arel::Nodes::InfixOperation.new("||", memo, term_anded)
62
+ end
63
+ end
64
+
65
+ def self.tsquery_term(unsanitized_term, prefix:)
66
+ negated = false
67
+
68
+ if unsanitized_term.start_with?("!")
69
+ unsanitized_term[0] = ''
70
+ negated = true
71
+ end
72
+
73
+ sanitized_term = unsanitized_term.gsub(DISALLOWED_TSQUERY_CHARACTERS, " ")
74
+ tsquery_expression(sanitized_term, negated: negated, prefix: prefix)
75
+ end
76
+
77
+ # After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
78
+ # If :prefix is true, then the term will have :* appended to the end.
79
+ # If :negated is true, then the term will have ! prepended to the front.
80
+ def self.tsquery_expression(term, negated:, prefix:)
81
+ terms = [
82
+ (Arel::Nodes.build_quoted('!') if negated),
83
+ Arel::Nodes.build_quoted("' "),
84
+ Arel::Nodes.build_quoted(term),
85
+ Arel::Nodes.build_quoted(" '"),
86
+ (Arel::Nodes.build_quoted(":*") if prefix)
87
+ ].compact
88
+
89
+ terms.inject do |memo, term|
90
+ Arel::Nodes::InfixOperation.new("||", memo, Arel::Nodes.build_quoted(term))
91
+ end
92
+ end
93
+
94
+ def self.tsquery(query, prefix:, raw:)
95
+ query_terms = query.split(" ").compact
96
+ tsq =
97
+ if query.blank?
98
+ Arel::Nodes.build_quoted("")
99
+ elsif raw
100
+ Arel::Nodes.build_quoted(query)
101
+ else
102
+ tsquery_for_terms(query_terms, prefix: prefix)
103
+ end
104
+
105
+ Arel::Nodes::NamedFunction.new("to_tsquery", [dictionary, tsq])
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSearchable
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg-searchable/model"
4
+ require "pg-searchable/version"
@@ -0,0 +1,11 @@
1
+ require_relative "lib/pg-searchable/version"
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = ["wyozi"]
5
+ gem.name = "pg-searchable"
6
+ gem.summary = "Simple full-text searching for Rails"
7
+
8
+ gem.files = `git ls-files | grep -Ev '^(spec)'`.split("\n")
9
+ gem.version = PgSearchable::VERSION
10
+ gem.required_ruby_version = ">= 2.5.0"
11
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg-searchable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - wyozi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".editorconfig"
20
+ - ".github/workflows/test.yml"
21
+ - ".gitignore"
22
+ - ".rspec"
23
+ - Gemfile
24
+ - Gemfile.lock
25
+ - README.md
26
+ - bin/local-docker-testing
27
+ - config.ru
28
+ - docker-compose.test.yml
29
+ - lib/pg-searchable.rb
30
+ - lib/pg-searchable/model.rb
31
+ - lib/pg-searchable/version.rb
32
+ - pg-searchable.gemspec
33
+ homepage:
34
+ licenses: []
35
+ metadata: {}
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 2.5.0
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.1.2
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Simple full-text searching for Rails
55
+ test_files: []