polysearch 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e36fbf04831569261179657e616072e846db1f69ee69ac177fb573913563a21
4
- data.tar.gz: 5c54be09188c3cf6fe2b370770e6e506e712280dfcee3dca05406a37612bb8a9
3
+ metadata.gz: cbd94171f65db5d6d0e92c6ad1198716c8fe689c395f82b2f15d59adec6b8ce7
4
+ data.tar.gz: 720f9f7ea37e863dff3d05631abc61ff2ff09d0b5b2af2f68b53444cb42eba3c
5
5
  SHA512:
6
- metadata.gz: 0ff0320bf6b4feb3f859faf6717bd27bfb7537f2e00186348dbef096da6a937110f91a904df8ec5929dda5242d3e2c2fb940517dbde24a6081afcabb177408a5
7
- data.tar.gz: 5cd2653801702e0d1e37fe7ef8e93ff86193afe0788e8fe62a875bbf53d3a2ff4e29837cf49495d538d8ba9ae7639ceaa708845308535e767078109ba7be0e2d
6
+ metadata.gz: 2dbc836cb1279c2ae3ee5c7d2fc9a1bedb1ea2bbad8f7b2974369d6c48ff646bf7038f409902047b69f782821ca7a6e135ef0670282d732b366dcccb2cf2a37f
7
+ data.tar.gz: 8e2b04099b36fc14c9fc4f4247414b44d020c153a8510184114d5607b5fe48b89f165c1f2d098aed4c58dcb67c29e72279c6e0a0233f0b91c3fa5baaf327d67c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polysearch (0.1.1)
4
+ polysearch (0.2.0)
5
5
  rails (>= 6.0)
6
6
 
7
7
  GEM
@@ -84,12 +84,8 @@ GEM
84
84
  marcel (1.0.1)
85
85
  method_source (1.0.0)
86
86
  mini_mime (1.0.3)
87
- mini_portile2 (2.5.1)
88
87
  minitest (5.14.4)
89
88
  nio4r (2.5.7)
90
- nokogiri (1.11.5)
91
- mini_portile2 (~> 2.5.0)
92
- racc (~> 1.4)
93
89
  nokogiri (1.11.5-arm64-darwin)
94
90
  racc (~> 1.4)
95
91
  parallel (1.20.1)
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- [![Lines of Code](http://img.shields.io/badge/lines_of_code-218-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
1
+ [![Lines of Code](http://img.shields.io/badge/lines_of_code-237-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
2
2
 
3
3
  # Polysearch
4
4
 
5
- Simplified polymorphic full text + similarity search based on postgres
5
+ Simplified polymorphic full text + similarity search based on postgres.
6
6
 
7
7
  > NOTE: This project is narrower in scope and more opinionated than [pg_search](https://github.com/Casecommons/pg_search).
8
8
 
@@ -50,7 +50,7 @@ Simplified polymorphic full text + similarity search based on postgres
50
50
  [
51
51
  make_tsvector(first_name, weight: "A"),
52
52
  make_tsvector(last_name, weight: "A"),
53
- make_tsvector(email, weight: "B")
53
+ make_tsvector(nickname, weight: "B")
54
54
  ]
55
55
  end
56
56
  end
@@ -65,12 +65,25 @@ Simplified polymorphic full text + similarity search based on postgres
65
65
  1. Start searching
66
66
 
67
67
  ```ruby
68
- User.create first_name: "Nate", last_name: "Hopkins", email: "nhopkins@mailinator.com"
68
+ User.create first_name: "Shawn", last_name: "Spencer", nickname: "Maverick"
69
69
 
70
- User.polysearch("nate")
71
- User.polysearch("ntae") # misspellings also return results
72
- User.polysearch("nate").where(created_at: 1.day.ago..Current.time) # active record chaining
73
- User.polysearch("nate").order(created_at: :desc) # chain additional ordering after the polysearch scope
70
+ # find natural language matches (faster)
71
+ User.full_text_search("shawn")
72
+
73
+ # find similarity matches, best for misspelled search terms (slower)
74
+ User.similarity_search("shwn")
75
+
76
+ # perform a combined full text search and a similarity search
77
+ User.combined_search("shwn")
78
+
79
+ # perform a full text search and fall back to similarity (faster than combined_search)
80
+ User.polysearch("shwn")
81
+
82
+ # calculate counts (explicitly pass :id to omit search rankings)
83
+ User.full_text_search("shawn").count(:id)
84
+ User.similarity_search("shwn").count(:id)
85
+ User.combined_search("shwn").count(:id)
86
+ User.polysearch("shwn").count(:id)
74
87
  ```
75
88
 
76
89
  ## License
@@ -36,27 +36,26 @@ module Polysearch
36
36
  has_one :polysearch, as: :searchable, class_name: "Polysearch::Record", inverse_of: "searchable"
37
37
  after_destroy :destroy_polysearch
38
38
 
39
+ scope :full_text_search, ->(value) {
40
+ value.blank? ? all : joins(:polysearch).merge(Polysearch::Record.full_text_search(value).select_full_text_search_rank(value))
41
+ }
42
+
43
+ scope :similarity_search, ->(value) {
44
+ value.blank? ? all : joins(:polysearch).merge(Polysearch::Record.similarity_search(value).select_similarity_rank(value))
45
+ }
46
+
47
+ scope :combined_search, ->(value) {
48
+ subquery = <<~SQL
49
+ (
50
+ SELECT #{table_name}.*, searchable_polysearches.search_rank from (#{Polysearch::Record.combined_search(value).except(:order).to_sql}) searchable_polysearches
51
+ LEFT JOIN LATERAL (select * from #{table_name} WHERE id = searchable_polysearches.searchable_id) #{table_name} ON TRUE
52
+ ) #{table_name}
53
+ SQL
54
+ from(subquery).order("search_rank desc")
55
+ }
56
+
39
57
  scope :polysearch, ->(value) {
40
- if value.blank?
41
- all
42
- else
43
- fts_rank_alias = "#{table_name.singularize}_fts_rank"
44
- similarity_rank_alias = "#{table_name.singularize}_similarity_rank"
45
-
46
- fts = Polysearch::Record
47
- .select_fts_rank(value, :searchable_id, rank_alias: fts_rank_alias)
48
- .select_similarity_rank(value, :searchable_id, rank_alias: similarity_rank_alias)
49
- .where(searchable_type: name)
50
- .fts(value).or(Polysearch::Record.where(searchable_type: name).similar(value))
51
-
52
- query = <<~SQL
53
- SELECT searchables.*, fts.#{fts_rank_alias}, fts.#{similarity_rank_alias} from (#{fts.to_sql}) fts
54
- LEFT JOIN LATERAL (select * from #{table_name} WHERE id = fts.searchable_id) searchables ON TRUE
55
- SQL
56
-
57
- select(Arel.star).from(Arel::Nodes::SqlLiteral.new("(#{query})").as(table_name))
58
- .reorder(fts_rank_alias => :desc, similarity_rank_alias => :desc)
59
- end
58
+ value.blank? ? all : joins(:polysearch).merge(Polysearch::Record.polysearch(value))
60
59
  }
61
60
  end
62
61
 
data/app/models/record.rb CHANGED
@@ -33,39 +33,57 @@ module Polysearch
33
33
 
34
34
  # scopes ....................................................................
35
35
 
36
- scope :select_fts_rank, ->(value, *selects, rank_alias: nil) {
37
- value = value.to_s.gsub(/\W/, " ").squeeze(" ").downcase.strip
38
- value = Arel::Nodes::SqlLiteral.new(sanitize_sql_array(["?", value.to_s]))
39
- plainto_tsquery = Arel::Nodes::NamedFunction.new("plainto_tsquery", [Arel::Nodes::SqlLiteral.new("'simple'"), value])
36
+ scope :select_full_text_search_rank, ->(value, *selects) {
37
+ plainto_tsquery = Arel::Nodes::NamedFunction.new("plainto_tsquery", [Arel::Nodes::SqlLiteral.new("'simple'"), arel_search_value(value)])
40
38
  ts_rank = Arel::Nodes::NamedFunction.new("ts_rank", [arel_table[:value], plainto_tsquery])
41
-
42
- rank_alias ||= "fts_rank"
43
39
  selects << Arel.star if selects.blank?
44
- selects << ts_rank.as(rank_alias)
45
- select(*selects).order("#{rank_alias} desc")
40
+ selects << ts_rank.as("search_rank")
41
+ select(*selects).reorder("search_rank desc")
46
42
  }
47
43
 
48
- scope :fts, ->(value) {
49
- value = value.to_s.gsub(/\W/, " ").squeeze(" ").downcase.strip
50
- value = Arel::Nodes::SqlLiteral.new(sanitize_sql_array(["?", value.to_s]))
51
- plainto_tsquery = Arel::Nodes::NamedFunction.new("plainto_tsquery", [Arel::Nodes::SqlLiteral.new("'simple'"), value])
52
- where(Arel::Nodes::InfixOperation.new("@@", arel_table[:value], plainto_tsquery))
44
+ scope :full_text_search, ->(value) {
45
+ if value.blank?
46
+ all
47
+ else
48
+ plainto_tsquery = Arel::Nodes::NamedFunction.new("plainto_tsquery", [Arel::Nodes::SqlLiteral.new("'simple'"), arel_search_value(value)])
49
+ where(Arel::Nodes::InfixOperation.new("@@", arel_table[:value], plainto_tsquery))
50
+ end
53
51
  }
54
52
 
55
- scope :select_similarity_rank, ->(value, *selects, rank_alias: nil) {
56
- value = value.to_s.gsub(/\W/, " ").squeeze(" ").downcase.strip
57
- value = Arel::Nodes::SqlLiteral.new(sanitize_sql_array(["?", value.to_s]))
58
-
59
- rank_alias ||= "similarity_rank"
53
+ scope :select_similarity_rank, ->(value, *selects) {
54
+ similarity = Arel::Nodes::NamedFunction.new("similarity", [arel_table[:words], arel_search_value(value)])
60
55
  selects << Arel.star if selects.blank?
61
- selects << Arel::Nodes::NamedFunction.new("similarity", [arel_table[:words], value]).as(rank_alias)
62
- select(*selects).order("#{rank_alias} desc")
56
+ selects << similarity.as("search_rank")
57
+ select(*selects).order("search_rank desc")
58
+ }
59
+
60
+ scope :similarity_search, ->(value) {
61
+ if value.blank?
62
+ all
63
+ else
64
+ where Arel::Nodes::NamedFunction.new("similarity", [arel_table[:words], arel_search_value(value)]).gt(0)
65
+ end
66
+ }
67
+
68
+ scope :combined_search, ->(value) {
69
+ subquery = <<~SQL
70
+ (
71
+ #{select_full_text_search_rank(value).full_text_search(value).except(:order).to_sql}
72
+ UNION ALL
73
+ #{select_similarity_rank(value).similarity_search(value).except(:order).to_sql}
74
+ ) AS #{table_name}
75
+ SQL
76
+ from(subquery).order("search_rank desc")
63
77
  }
64
78
 
65
- scope :similar, ->(value, range: 0.01) {
66
- value = value.to_s.gsub(/\W/, " ").squeeze(" ").downcase.strip
67
- value = Arel::Nodes::SqlLiteral.new(sanitize_sql_array(["?", value.to_s]))
68
- where Arel::Nodes::NamedFunction.new("similarity", [arel_table[:words], value]).gteq(range)
79
+ scope :polysearch, ->(value) {
80
+ if value.blank?
81
+ all
82
+ else
83
+ full_text_search(value).exists? ?
84
+ select_full_text_search_rank(value).full_text_search(value) :
85
+ select_similarity_rank(value).similarity_search(value)
86
+ end
69
87
  }
70
88
 
71
89
  # additional config (i.e. accepts_nested_attribute_for etc...) ..............
@@ -74,6 +92,10 @@ module Polysearch
74
92
 
75
93
  # class methods .............................................................
76
94
  class << self
95
+ def arel_search_value(value)
96
+ value = value.to_s.gsub(/\W/, " ").squeeze(" ").downcase.strip
97
+ Arel::Nodes::SqlLiteral.new(sanitize_sql_array(["?", value]))
98
+ end
77
99
  end
78
100
 
79
101
  # public instance methods ...................................................
data/bin/loc ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ cloc --exclude-dir=test --include-ext=rb .
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polysearch
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polysearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Hopkins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-20 00:00:00.000000000 Z
11
+ date: 2021-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -95,6 +95,7 @@ files:
95
95
  - app/models/concerns/searchable.rb
96
96
  - app/models/record.rb
97
97
  - bin/console
98
+ - bin/loc
98
99
  - bin/setup
99
100
  - bin/standardize
100
101
  - lib/generators/polysearch/migration_generator.rb