polysearch 0.1.0 → 0.2.3

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: 21bf6d369ed9516908e5ac52bb3b5688335e05952e920741cccd40d73b2279d8
4
- data.tar.gz: 7257fa7063ad0b250c7d3b602a7acc5bc3b42984b6fefa47202629eedbb03018
3
+ metadata.gz: e527bae221d08edf537ee05002258f8cdf982c09f0748eccfee8f170fb83629e
4
+ data.tar.gz: 3f4d4d07575755d6a89e2d20be671128a861305cc5f41b7437a7d4c88a0a7bb2
5
5
  SHA512:
6
- metadata.gz: 8995e6d99585aadc2add2e47b9d18829313dd29853468c706e0297a4361b633acbe593db5fcd34e0a2890cc1832f4758d95f74d3a10ad990fde0dc41e91f348b
7
- data.tar.gz: c6e75132f957e688b23ef5499fc8af50189edeb470ed664ad3dab4ca5096a3a343e7e33902d98fb0db2113b7993610b8126ee20653bac026787c7c3397c9d380
6
+ metadata.gz: '09093059f97e4f6af87403c21e8eb3b834ba82acfaebfa9708992f7028304d59bfc32cad6a7c38b482d7045a535f6e7c1cbc379416e594ec2fcef65b9a17d445'
7
+ data.tar.gz: 03f1cf68155d2cf68431e032459363c43252c7bfe359e93a43db3f2cb3ef4827f784ea16ee7539ed34999ad30936a82f735c2eb57857773eb23c6d319ffa581e
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polysearch (0.1.0)
4
+ polysearch (0.2.3)
5
5
  rails (>= 6.0)
6
6
 
7
7
  GEM
@@ -84,13 +84,9 @@ 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.4)
91
- mini_portile2 (~> 2.5.0)
92
- racc (~> 1.4)
93
- nokogiri (1.11.4-arm64-darwin)
89
+ nokogiri (1.11.5-arm64-darwin)
94
90
  racc (~> 1.4)
95
91
  parallel (1.20.1)
96
92
  parser (3.0.1.1)
data/README.md CHANGED
@@ -1,6 +1,10 @@
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
+
1
3
  # Polysearch
2
4
 
3
- #### Simplified polymorphic full text + similarity search based on postgres
5
+ Simplified polymorphic full text + similarity search based on postgres.
6
+
7
+ > NOTE: This project is narrower in scope and more opinionated than [pg_search](https://github.com/Casecommons/pg_search).
4
8
 
5
9
  ## Requirements
6
10
 
@@ -43,23 +47,43 @@
43
47
  after_save_commit :update_polysearch
44
48
 
45
49
  def to_tsvectors
46
- []
47
- .then { |list| list << make_tsvector(first_name, weight: "A") }
48
- .then { |list| list << make_tsvector(last_name, weight: "A") }
49
- .then { |list| list << make_tsvector(email, weight: "B") }
50
+ [
51
+ make_tsvector(first_name, weight: "A"),
52
+ make_tsvector(last_name, weight: "A"),
53
+ make_tsvector(nickname, weight: "B")
54
+ ]
50
55
  end
51
56
  end
52
57
  ```
53
58
 
54
- 1. Start searching
59
+ If you have existing records that need to create/update a polysearch record, you can save them like this.
55
60
 
61
+ ```ruby
62
+ User.find_each(&:update_polysearch)
56
63
  ```
57
- User.create first_name: "Nate", last_name: "Hopkins", email: "nhopkins@mailinator.com"
58
64
 
59
- User.polysearch("nate")
60
- User.polysearch("ntae") # misspellings also return results
61
- User.polysearch("nate").where(created_at: 1.day.ago..Current.time) # active record chaining
62
- User.polysearch("nate").order(created_at: :desc) # chain additional ordering after the polysearch scope
65
+ 1. Start searching
66
+
67
+ ```ruby
68
+ User.create first_name: "Shawn", last_name: "Spencer", nickname: "Maverick"
69
+
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 both a full text search and similarity search
77
+ User.combined_search("shwn")
78
+
79
+ # perform a full text search and fall back to similarity search (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)
63
87
  ```
64
88
 
65
89
  ## License
@@ -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 ...................................................
@@ -36,39 +36,54 @@ 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.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
 
63
62
  def update_polysearch
63
+ return unless persisted?
64
64
  tsvectors = to_tsvectors.compact.uniq
65
65
  return if tsvectors.blank?
66
+
66
67
  tsvectors.pop while tsvectors.size > 500
67
68
  tsvectors.concat similarity_words_tsvectors
68
69
  tsvector = tsvectors.join(" || ")
69
- fts = Polysearch::Record.where(searchable: self).first_or_create
70
- fts.update_value tsvector
71
- fts.update_columns words: similarity_words.join(" ")
70
+
71
+ attributes = {
72
+ searchable_type: self.class.name,
73
+ searchable_id: id,
74
+ words: similarity_words.join(" "),
75
+ created_at: Time.current,
76
+ updated_at: Time.current
77
+ }
78
+
79
+ result = Polysearch::Record.upsert(
80
+ attributes,
81
+ unique_by: [:searchable_type, :searchable_id],
82
+ returning: :id
83
+ )
84
+
85
+ record = Polysearch::Record.find_by(id: result.first["id"])
86
+ record.update_value tsvector
72
87
  end
73
88
 
74
89
  # Polysearch::Searchable#to_tsvectors is abstract... a noop by default
data/bin/loc ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ cloc --exclude-dir=test --include-ext=rb .
data/lib/polysearch.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "polysearch/version"
4
- require_relative "../app/models/record"
5
- require_relative "../app/models/concerns/searchable"
4
+ require_relative "../app/models/polysearch/record"
5
+ require_relative "../app/models/polysearch/searchable"
6
6
 
7
7
  module Polysearch
8
8
  class Engine < Rails::Engine
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polysearch
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.3"
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.0
4
+ version: 0.2.3
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-18 00:00:00.000000000 Z
11
+ date: 2021-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -92,9 +92,10 @@ files:
92
92
  - LICENSE.txt
93
93
  - README.md
94
94
  - Rakefile
95
- - app/models/concerns/searchable.rb
96
- - app/models/record.rb
95
+ - app/models/polysearch/record.rb
96
+ - app/models/polysearch/searchable.rb
97
97
  - bin/console
98
+ - bin/loc
98
99
  - bin/setup
99
100
  - bin/standardize
100
101
  - lib/generators/polysearch/migration_generator.rb