polysearch 0.1.1 → 0.2.0

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