runestone 1.0 → 2.0.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: 3756ae6e1dcf043dc6d4ca6e1312a0409ceac0fd15a90e6a7859c7d5cfec0ef9
4
- data.tar.gz: a2e9bfd44ae564d10ddd6ee9f25179b267b12979dd7bfe2311825ce2bc348d97
3
+ metadata.gz: 3c069ec88ae02b52e3cb0a155efe7d17b0cb3d0f9b248e74deec97cb1ed752e0
4
+ data.tar.gz: 4e627b4879f88ae65b8b56090604e7e8fe0379f93f5a72b4142d76d5eabf08f7
5
5
  SHA512:
6
- metadata.gz: 4c6b1cd82ea806fd419ab2b9986b27d2fabb0d0d898decf69068bb9b824c642d9ef333e8c2f6874ca5151d5250ef5f4d81796cce7813b2bfdb66dbb47ed5a01d
7
- data.tar.gz: ec0d633c0eaab16dccea6a340f53f6160e6932a530d6d1398ce3462b9366abbc018548e5789cccf2974c8f45495e78949a865a3b270f09ab7d61b9738a5e7db6
6
+ metadata.gz: 7f3f103de6f864ce9d8047232023f1c6db98867be4a7015d899c088875b830d9ee34e84c31d23b18bbdb88fc9058efe7a455891958d5adbb6b3600aed0ff4552
7
+ data.tar.gz: 014ab3c81cb2c6df0473cd5ec0c65a35a6aa46a41c7f351962b26a97abc32eac74820738bcccddfa0d67b5a3b8c715d8ed5a6d1810d1099be1f134d85ac54679
data/README.md CHANGED
@@ -4,7 +4,107 @@ Runestone provides full text search PostgreSQL's full text search capabilities.
4
4
  It was inspired by [Postgres full-text search is Good Enough!][1] and
5
5
  [Super Fuzzy Searching on PostgreSQL][2]
6
6
 
7
+ ## Installation
7
8
 
9
+ Install Runestone from RubyGems:
10
+
11
+ ``` sh
12
+ $ gem install runestone
13
+ ```
14
+
15
+ Or include it in your project's `Gemfile` with Bundler:
16
+
17
+ ``` ruby
18
+ gem 'runestone'
19
+ ```
20
+
21
+ After installation, run the Runestone's migration to to enable the necessary database extensions and create the runestones and runestone corpus tables.
22
+
23
+ ```sh
24
+ $ rails db:migrate
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Indexing
30
+
31
+ To index your ActiveRecord Models:
32
+
33
+ ```ruby
34
+ class Building < ApplicationRecord
35
+
36
+ runestone do
37
+ index 'name', 'addresses.global', 'addresses.national'
38
+
39
+ attributes(:name)
40
+ attributes(:size, :floors)
41
+ attribute(:addresses) {
42
+ addresses&.map{|address| {
43
+ local: address.local,
44
+ regional: address.regional,
45
+ national: address.national,
46
+ global: address.global
47
+ } }
48
+ }
49
+ end
50
+
51
+ end
52
+ ```
53
+
54
+ When searching the attribute(s) will be available in `data` on the result(s), but only the attributes specified by `index` will indexed and used for searching.
55
+
56
+ ### Searching
57
+
58
+ To search for the Building:
59
+
60
+ ```ruby
61
+ Building.search("Empire")
62
+ ```
63
+
64
+ You can also search through all indexed models with:
65
+
66
+ ```ruby
67
+ Runestone::Model.search("needle")
68
+ ```
69
+
70
+ Additionally you can highlight the results. When this is done each result will have a `highlights` attribute which is the same as data, but with matches wrapped in a `<b>` tag:
71
+
72
+ ```ruby
73
+ Runestone::Model.highlight(@results, "needle")
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ ### Synonym
79
+
80
+ ```ruby
81
+ Runestone.add_synonym('ten', '10')
82
+
83
+ Runestone.add_synonym('one hundred', '100')
84
+ Runestone.add_synonym('100', 'one hundred')
85
+ ```
86
+
87
+ ### Defaults
88
+
89
+ #### dictionary
90
+
91
+ The default dictionary that Runestone uses is the `runestone` dictionary. Which
92
+ is the `simple` dictionary in PostgreSQL with `unaccent` to tranliterate some
93
+ characters to the ASCII equivlent.
94
+
95
+ ```ruby
96
+ Runestone.dictionary = :runesonte
97
+ ```
98
+
99
+ #### normalization for ranking
100
+
101
+ Ranking can be configured to use the `normalization` paramater as described
102
+ in the [PostgreSQL documentation][3]. The default is `16`
103
+
104
+ ```ruby
105
+ Runestone.dictionary = 16
106
+ ```
8
107
 
9
108
  [1]: http://rachbelaid.com/postgres-full-text-search-is-good-enough/
10
- [2]: http://www.www-old.bartlettpublishing.com/site/bartpub/blog/3/entry/350
109
+ [2]: http://www.www-old.bartlettpublishing.com/site/bartpub/blog/3/entry/350
110
+ [3]: https://www.postgresql.org/docs/13/textsearch-controls.html#TEXTSEARCH-RANKING
@@ -4,6 +4,7 @@ class CreateRunestoneTables < ActiveRecord::Migration[6.0]
4
4
  enable_extension 'pgcrypto'
5
5
  enable_extension 'pg_trgm'
6
6
  enable_extension 'fuzzystrmatch'
7
+ enable_extension 'unaccent'
7
8
 
8
9
  create_table :runestones, id: :uuid do |t|
9
10
  t.belongs_to :record, type: :uuid, polymorphic: true, null: false
@@ -21,8 +22,8 @@ class CreateRunestoneTables < ActiveRecord::Migration[6.0]
21
22
 
22
23
  CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
23
24
 
24
- CREATE TEXT SEARCH CONFIGURATION simple_unaccent (COPY = simple);
25
- ALTER TEXT SEARCH CONFIGURATION simple_unaccent
25
+ CREATE TEXT SEARCH CONFIGURATION runestone (COPY = simple);
26
+ ALTER TEXT SEARCH CONFIGURATION runestone
26
27
  ALTER MAPPING FOR hword, hword_part, word
27
28
  WITH unaccent, simple;
28
29
  SQL
@@ -6,7 +6,8 @@ module Runestone
6
6
  autoload :WebSearch, "#{File.dirname(__FILE__)}/runestone/web_search"
7
7
  autoload :IndexingJob, "#{File.dirname(__FILE__)}/runestone/indexing_job"
8
8
 
9
- mattr_accessor :dictionary, default: :simple_unaccent
9
+ mattr_accessor :dictionary, default: :runestone
10
+ mattr_accessor :normalization, default: 16
10
11
  mattr_accessor :runner, default: :inline
11
12
  mattr_accessor :job_queue, default: :runestone_indexing
12
13
  mattr_accessor :typo_tolerances, default: { 1 => 4..7, 2 => 8.. }
@@ -15,6 +16,37 @@ module Runestone
15
16
  { }
16
17
  end
17
18
 
19
+ DEFAULT_APPROXIMATIONS = {
20
+ "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
21
+ "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
22
+ "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
23
+ "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
24
+ "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
25
+ "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
26
+ "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
27
+ "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
28
+ "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
29
+ "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
30
+ "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
31
+ "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
32
+ "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
33
+ "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
34
+ "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
35
+ "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
36
+ "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
37
+ "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
38
+ "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
39
+ "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
40
+ "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
41
+ "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
42
+ "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
43
+ "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
44
+ "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
45
+ "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
46
+ "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
47
+ "Ž"=>"Z", "ž"=>"z"
48
+ }.freeze
49
+
18
50
  def self.normalize(string)
19
51
  string = string.downcase
20
52
  string = string.unicode_normalize!
@@ -22,7 +54,17 @@ module Runestone
22
54
  rescue Encoding::CompatibilityError
23
55
  string
24
56
  end
25
-
57
+
58
+ def self.normalize!(string)
59
+ string.downcase!
60
+ string.unicode_normalize!
61
+ rescue Encoding::CompatibilityError
62
+ end
63
+
64
+ def transliterate(string)
65
+ string.gsub(/[^\x00-\x7f]/u) { |char| approximations[char] || char }
66
+ end
67
+
26
68
  def self.add_synonyms(dictionary)
27
69
  dictionary.each do |k, v|
28
70
  add_synonym(k, *v)
@@ -53,7 +95,7 @@ module Runestone
53
95
  syn[last].uniq!
54
96
  end
55
97
 
56
- def search(query, dictionary: nil, prefix: :last)
98
+ def search(query, dictionary: nil, prefix: :last, normalization: nil)
57
99
  exact_search = Runestone::WebSearch.parse(query, prefix: prefix)
58
100
  typo_search = exact_search.typos
59
101
  syn_search = typo_search.synonymize
@@ -65,11 +107,11 @@ module Runestone
65
107
  q = if select_values.empty?
66
108
  select(
67
109
  klass.arel_table[Arel.star],
68
- *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
110
+ *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary, normalization: normalization), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
69
111
  )
70
112
  else
71
113
  select(
72
- *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
114
+ *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary, normalization: normalization), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
73
115
  )
74
116
  end
75
117
 
@@ -39,14 +39,17 @@ module Runestone::ActiveRecord
39
39
  )
40
40
  end
41
41
 
42
- def ts_rank_cd(vector, query, dictionary: nil)
42
+ def ts_rank_cd(vector, query, dictionary: nil, normalization: nil)
43
+ normalization ||= Runestone.normalization
44
+
43
45
  Arel::Nodes::TSRankCD.new(
44
46
  ts_vector(vector, dictionary: dictionary),
45
- ts_query(query, dictionary: dictionary)
47
+ ts_query(query, dictionary: dictionary),
48
+ normalization
46
49
  )
47
50
  end
48
51
 
49
- def search(query, dictionary: nil, prefix: nil)
52
+ def search(query, dictionary: nil, prefix: nil, normalization: nil)
50
53
  exact_search = Runestone::WebSearch.parse(query, prefix: prefix)
51
54
  typo_search = exact_search.typos
52
55
  syn_search = typo_search.synonymize
@@ -58,11 +61,11 @@ module Runestone::ActiveRecord
58
61
  q = if select_values.empty?
59
62
  select(
60
63
  klass.arel_table[Arel.star],
61
- *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
64
+ *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary, normalization: normalization), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
62
65
  )
63
66
  else
64
67
  select(
65
- *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
68
+ *tsqueries.each_with_index.map { |q, i| Arel::Nodes::As.new(ts_rank_cd(:vector, q, dictionary: dictionary, normalization: normalization), Arel::Nodes::SqlLiteral.new("rank#{i}")) }
66
69
  )
67
70
  end
68
71
 
@@ -6,21 +6,22 @@ module Runestone::Corpus
6
6
  conn = Runestone::Model.connection
7
7
  conn.execute(<<-SQL)
8
8
  INSERT INTO runestone_corpus ( word )
9
- VALUES (#{words.map { |w| conn.quote(w.downcase) }.join('),(')})
9
+ VALUES (#{words.map { |w| conn.quote(Runestone.normalize(w)) }.join('),(')})
10
10
  ON CONFLICT DO NOTHING
11
11
  SQL
12
12
  end
13
13
 
14
14
  def self.similar_words(*words)
15
15
  lut = {}
16
+ conn = Runestone::Model.connection
16
17
  words = words.inject([]) do |ws, w|
17
18
  tt = typo_tolerance(w)
18
- ws << "#{Runestone::Model.connection.quote(w)}, #{Runestone::Model.connection.quote(w.downcase)}, #{tt}" if tt > 0
19
+ ws << "#{conn.quote(w)}, #{conn.quote(w.downcase)}, #{tt}" if tt > 0
19
20
  ws
20
21
  end
21
22
  return lut if words.size == 0
22
23
 
23
- result = Runestone::Model.connection.execute(<<-SQL)
24
+ result = conn.execute(<<-SQL)
24
25
  WITH tokens (token, token_downcased, typo_tolerance) AS (VALUES (#{words.join('), (')}))
25
26
  SELECT token, word, levenshtein(runestone_corpus.word, tokens.token_downcased)
26
27
  FROM tokens
@@ -1,3 +1,3 @@
1
1
  module Runestone
2
- VERSION = '1.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -26,11 +26,7 @@ class Runestone::WebSearch
26
26
  # prefix options: :all, :last, :none (default: :last)
27
27
  def self.parse(query, prefix: :last)
28
28
  prefix ||= :last
29
- begin
30
- query.unicode_normalize!
31
- rescue Encoding::CompatibilityError
32
- end
33
- query.downcase!
29
+ Runestone.normalize!(query)
34
30
 
35
31
  q = []
36
32
  stack = []
@@ -27,6 +27,6 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency 'activejob', '>= 6.0'
28
28
 
29
29
  # Runtime
30
- s.add_runtime_dependency 'arel-extensions', '>= 6.0'
30
+ s.add_runtime_dependency 'arel-extensions', '>= 6.0.0.9'
31
31
  s.add_runtime_dependency 'activerecord', '>= 6.0'
32
32
  end
@@ -38,5 +38,24 @@ class CorpusTest < ActiveSupport::TestCase
38
38
  Runestone::Corpus.similar_words('Allee')
39
39
  )
40
40
  end
41
-
41
+
42
+ test 'adding words to corpus normalizes them' do
43
+ Runestone::Corpus.add("all\u00e9e")
44
+ assert_equal(
45
+ {
46
+ "Allee" => ["all\u00e9e"]
47
+ },
48
+ Runestone::Corpus.similar_words('Allee')
49
+ )
50
+
51
+ Runestone::Model.connection.execute('DELETE FROM runestone_corpus')
52
+ Runestone::Corpus.add("all\u0065\u0301e")
53
+ assert_equal(
54
+ {
55
+ "Allee" => ["all\u00e9e"]
56
+ },
57
+ Runestone::Corpus.similar_words('Allee')
58
+ )
59
+ end
60
+
42
61
  end
@@ -56,8 +56,8 @@ ActiveRecord::Migration.suppress_messages do
56
56
  CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
57
57
 
58
58
 
59
- CREATE TEXT SEARCH CONFIGURATION simple_unaccent (COPY = simple);
60
- ALTER TEXT SEARCH CONFIGURATION simple_unaccent
59
+ CREATE TEXT SEARCH CONFIGURATION runestone (COPY = simple);
60
+ ALTER TEXT SEARCH CONFIGURATION runestone
61
61
  ALTER MAPPING FOR hword, hword_part, word
62
62
  WITH unaccent, simple;
63
63
  SQL
@@ -2,9 +2,9 @@ require 'test_helper'
2
2
 
3
3
  class DelayedIndexingTest < ActiveSupport::TestCase
4
4
 
5
- test 'simple_unaccent index' do
5
+ test 'runestone index' do
6
6
  region = assert_no_difference 'Runestone::Model.count' do
7
- assert_no_sql(/setweight\(to_tsvector\('simple_unaccent', 'address name'\), 'A'\)/) do
7
+ assert_no_sql(/setweight\(to_tsvector\('runestone', 'address name'\), 'A'\)/) do
8
8
  Region.create(name: 'Region name')
9
9
  end
10
10
  end
@@ -9,6 +9,7 @@ class HighlightTest < ActiveSupport::TestCase
9
9
  tsmodels = Runestone::Model.search('state')
10
10
  Runestone::Model.highlight(tsmodels, 'state')
11
11
  assert_equal([
12
+ { "name"=>"address of <b>state</b> duo" },
12
13
  {
13
14
  "name"=>"Big <b>state</b> building",
14
15
  "addresses"=> [{"name"=>"address of <b>state</b> duo"}]
@@ -17,10 +18,31 @@ class HighlightTest < ActiveSupport::TestCase
17
18
  "name"=>"Empire <b>state</b> building",
18
19
  "addresses"=> [{"name"=>"address uno"}]
19
20
  },
21
+
22
+ ], tsmodels.map(&:highlights))
23
+ end
24
+
25
+ test '::highlights(query) with an accent in the result' do
26
+ Property.create(name: 'Émpire state building', addresses: [Address.create(name: 'address uno')])
27
+ Property.create(name: 'Big state building', addresses: [Address.create(name: 'addréss of state duo')])
28
+
29
+ tsmodels = Runestone::Model.search('empire')
30
+ Runestone::Model.highlight(tsmodels, 'empire')
31
+ assert_equal([
20
32
  {
21
- "name"=>"address of <b>state</b> duo"
33
+ "name"=>"<b>Émpire</b> state building",
34
+ "addresses"=>[ {"name"=>"address uno"} ]
22
35
  }
23
36
  ], tsmodels.map(&:highlights))
37
+
38
+ tsmodels = Runestone::Model.search('address')
39
+ Runestone::Model.highlight(tsmodels, 'address')
40
+ assert_equal([
41
+ {"name"=>"<b>address</b> uno"},
42
+ {"name"=>"<b>addréss</b> of state duo"},
43
+ {"addresses"=>[{"name"=>"<b>address</b> uno"}], "name"=>"Émpire state building"},
44
+ {"addresses"=>[{"name"=>"<b>addréss</b> of state duo"}], "name"=>"Big state building"}
45
+ ], tsmodels.map(&:highlights))
24
46
  end
25
47
 
26
48
  end
@@ -2,9 +2,9 @@ require 'test_helper'
2
2
 
3
3
  class IndexingTest < ActiveSupport::TestCase
4
4
 
5
- test 'simple_unaccent index' do
5
+ test 'runestone index' do
6
6
  address = assert_difference 'Runestone::Model.count', 1 do
7
- assert_sql(/setweight\(to_tsvector\('simple_unaccent', 'address name'\), 'A'\)/) do
7
+ assert_sql(/setweight\(to_tsvector\('runestone', 'address name'\), 'A'\)/) do
8
8
  Address.create(name: 'Address name')
9
9
  end
10
10
  end
@@ -33,16 +33,16 @@ class MultiIndexTest < ActiveSupport::TestCase
33
33
 
34
34
  query = Runestone::Model.search('empire')
35
35
  assert_sql(<<~SQL, query.to_sql)
36
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'empire:*')) AS rank0
36
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'empire:*'), 16) AS rank0
37
37
  FROM "runestones"
38
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'empire:*')
38
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'empire:*')
39
39
  ORDER BY rank0 DESC
40
40
  SQL
41
41
 
42
42
  query = Runestone::Model.search('empire', dictionary: 'english')
43
43
  assert_sql(<<~SQL, query.to_sql)
44
44
  SELECT
45
- "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('english', 'empire:*')) AS rank0
45
+ "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('english', 'empire:*'), 16) AS rank0
46
46
  FROM "runestones"
47
47
  WHERE "runestones"."vector" @@ to_tsquery('english', 'empire:*')
48
48
  AND "runestones"."dictionary" = 'english'
@@ -51,7 +51,7 @@ class MultiIndexTest < ActiveSupport::TestCase
51
51
 
52
52
  query = Runestone::Model.search('Эмпайр', dictionary: 'russian')
53
53
  assert_sql(<<~SQL, query.to_sql)
54
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('russian', 'эмпайр:*')) AS rank0
54
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('russian', 'эмпайр:*'), 16) AS rank0
55
55
  FROM "runestones"
56
56
  WHERE "runestones"."vector" @@ to_tsquery('russian', 'эмпайр:*')
57
57
  AND "runestones"."dictionary" = 'russian'
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ class OrderTest < ActiveSupport::TestCase
4
+
5
+ test 'smaller documents come first' do
6
+ a2 = Address.create(name: 'a big square')
7
+ a1 = Address.create(name: 'Square')
8
+
9
+ query = Runestone::Model.search('square')
10
+ assert_sql(<<~SQL, query.to_sql)
11
+ SELECT
12
+ "runestones".*,
13
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'square:*'), 16) AS rank0
14
+ FROM "runestones"
15
+ WHERE
16
+ "runestones"."vector" @@ to_tsquery('runestone', 'square:*')
17
+ ORDER BY rank0 DESC
18
+ SQL
19
+
20
+ assert_equal(query.map(&:record).map(&:name), [
21
+ 'Square',
22
+ 'a big square'
23
+ ])
24
+ end
25
+
26
+
27
+
28
+ end
@@ -6,9 +6,9 @@ class QueryTest < ActiveSupport::TestCase
6
6
  query = Runestone::Model.search('seaerch for this')
7
7
 
8
8
  assert_sql(<<~SQL, query.to_sql)
9
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
9
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch & for & this:*'), 16) AS rank0
10
10
  FROM "runestones"
11
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
11
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'seaerch & for & this:*')
12
12
  ORDER BY rank0 DESC
13
13
  SQL
14
14
  end
@@ -17,9 +17,9 @@ class QueryTest < ActiveSupport::TestCase
17
17
  query = Runestone::Model.search("the search for \u0065\u0301")
18
18
 
19
19
  assert_sql(<<~SQL, query.to_sql)
20
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'the & search & for & \u00e9:*')) AS rank0
20
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'the & search & for & \u00e9:*'), 16) AS rank0
21
21
  FROM "runestones"
22
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'the & search & for & \u00e9:*')
22
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'the & search & for & \u00e9:*')
23
23
  ORDER BY rank0 DESC
24
24
  SQL
25
25
  end
@@ -27,17 +27,17 @@ class QueryTest < ActiveSupport::TestCase
27
27
  test "::search(query with ')" do
28
28
  query = Runestone::Model.search("seaerch for ' this")
29
29
  assert_sql(<<~SQL, query.to_sql)
30
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
30
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch & for & this:*'), 16) AS rank0
31
31
  FROM "runestones"
32
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
32
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'seaerch & for & this:*')
33
33
  ORDER BY rank0 DESC
34
34
  SQL
35
35
 
36
36
  query = Runestone::Model.search("seaerch for james' map")
37
37
  assert_sql(<<~SQL, query.to_sql)
38
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & james'' & map:*')) AS rank0
38
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch & for & james'' & map:*'), 16) AS rank0
39
39
  FROM "runestones"
40
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & james'' & map:*')
40
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'seaerch & for & james'' & map:*')
41
41
  ORDER BY rank0 DESC
42
42
  SQL
43
43
  end
@@ -46,9 +46,9 @@ class QueryTest < ActiveSupport::TestCase
46
46
  query = Runestone::Model.search('seaerch for this', prefix: :all)
47
47
 
48
48
  assert_sql(<<~SQL, query.to_sql)
49
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch:* & for:* & this:*')) AS rank0
49
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch:* & for:* & this:*'), 16) AS rank0
50
50
  FROM "runestones"
51
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch:* & for:* & this:*')
51
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'seaerch:* & for:* & this:*')
52
52
  ORDER BY rank0 DESC
53
53
  SQL
54
54
  end
@@ -57,9 +57,9 @@ class QueryTest < ActiveSupport::TestCase
57
57
  query = Runestone::Model.search('seaerch for this').limit(10)
58
58
 
59
59
  assert_sql(<<~SQL, query.to_sql)
60
- SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
60
+ SELECT "runestones".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch & for & this:*'), 16) AS rank0
61
61
  FROM "runestones"
62
- WHERE "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
62
+ WHERE "runestones"."vector" @@ to_tsquery('runestone', 'seaerch & for & this:*')
63
63
  ORDER BY rank0 DESC
64
64
  LIMIT 10
65
65
  SQL
@@ -70,13 +70,13 @@ class QueryTest < ActiveSupport::TestCase
70
70
 
71
71
  assert_sql(<<~SQL, query.to_sql)
72
72
  SELECT
73
- "properties".*, ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0
73
+ "properties".*, ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch & for & this:*'), 16) AS rank0
74
74
  FROM "properties"
75
75
  INNER JOIN "runestones"
76
- ON "runestones"."record_id" = "properties"."id"
77
- AND "runestones"."record_type" = 'Property'
76
+ ON "runestones"."record_type" = 'Property'
77
+ AND "runestones"."record_id" = "properties"."id"
78
78
  WHERE
79
- "runestones"."vector" @@ to_tsquery('simple_unaccent', 'seaerch & for & this:*')
79
+ "runestones"."vector" @@ to_tsquery('runestone', 'seaerch & for & this:*')
80
80
  ORDER BY rank0 DESC
81
81
  SQL
82
82
  end
@@ -88,14 +88,14 @@ class QueryTest < ActiveSupport::TestCase
88
88
  assert_sql(<<~SQL, query.to_sql)
89
89
  SELECT
90
90
  "properties".*,
91
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'seaerch & for & this:*')) AS rank0,
92
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(seaerch | search) & for & this:*')) AS rank1
91
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'seaerch & for & this:*'), 16) AS rank0,
92
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(seaerch | search) & for & this:*'), 16) AS rank1
93
93
  FROM "properties"
94
- INNER JOIN "runestones" ON
95
- "runestones"."record_id" = "properties"."id"
96
- AND "runestones"."record_type" = 'Property'
94
+ INNER JOIN "runestones"
95
+ ON "runestones"."record_type" = 'Property'
96
+ AND "runestones"."record_id" = "properties"."id"
97
97
  WHERE
98
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '(seaerch | search) & for & this:*')
98
+ "runestones"."vector" @@ to_tsquery('runestone', '(seaerch | search) & for & this:*')
99
99
  ORDER BY
100
100
  rank0 DESC,
101
101
  rank1 DESC
@@ -113,12 +113,12 @@ class QueryTest < ActiveSupport::TestCase
113
113
  assert_sql(<<~SQL, Runestone::Model.search('avenue').to_sql)
114
114
  SELECT
115
115
  "runestones".*,
116
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'avenue:*')) AS rank0,
117
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(avenue:* | aveneue)')) AS rank1,
118
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '((avenue:* | aveneue) | av | ave | avn | aven | avenu | avnue)')) AS rank2
116
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'avenue:*'), 16) AS rank0,
117
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(avenue:* | aveneue)'), 16) AS rank1,
118
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '((avenue:* | aveneue) | av | ave | avn | aven | avenu | avnue)'), 16) AS rank2
119
119
  FROM "runestones"
120
120
  WHERE
121
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '((avenue:* | aveneue) | av | ave | avn | aven | avenu | avnue)')
121
+ "runestones"."vector" @@ to_tsquery('runestone', '((avenue:* | aveneue) | av | ave | avn | aven | avenu | avnue)')
122
122
  ORDER BY
123
123
  rank0 DESC,
124
124
  rank1 DESC,
@@ -13,11 +13,11 @@ class SynonymTest < ActiveSupport::TestCase
13
13
  assert_sql(<<~SQL, query.to_sql)
14
14
  SELECT
15
15
  "runestones".*,
16
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce:*')) AS rank0,
17
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')) AS rank1
16
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '17 & spruce:*'), 16) AS rank0,
17
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)'), 16) AS rank1
18
18
  FROM "runestones"
19
19
  WHERE
20
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
20
+ "runestones"."vector" @@ to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
21
21
  ORDER BY rank0 DESC, rank1 DESC
22
22
  SQL
23
23
  end
@@ -40,11 +40,11 @@ class SynonymTest < ActiveSupport::TestCase
40
40
  assert_sql(<<~SQL, query.to_sql)
41
41
  SELECT
42
42
  "runestones".*,
43
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce:*')) AS rank0,
44
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')) AS rank1
43
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '17 & spruce:*'), 16) AS rank0,
44
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)'), 16) AS rank1
45
45
  FROM "runestones"
46
46
  WHERE
47
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
47
+ "runestones"."vector" @@ to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
48
48
  ORDER BY rank0 DESC, rank1 DESC
49
49
  SQL
50
50
  end
@@ -59,11 +59,11 @@ class SynonymTest < ActiveSupport::TestCase
59
59
  assert_sql(<<~SQL, query.to_sql)
60
60
  SELECT
61
61
  "runestones".*,
62
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & !spruce')) AS rank0,
63
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & !spruce')) AS rank1
62
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '17 & !spruce'), 16) AS rank0,
63
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & !spruce'), 16) AS rank1
64
64
  FROM "runestones"
65
65
  WHERE
66
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & !spruce')
66
+ "runestones"."vector" @@ to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & !spruce')
67
67
  ORDER BY rank0 DESC, rank1 DESC
68
68
  SQL
69
69
  end
@@ -78,11 +78,11 @@ class SynonymTest < ActiveSupport::TestCase
78
78
  assert_sql(<<~SQL, query.to_sql)
79
79
  SELECT
80
80
  "runestones".*,
81
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce')) AS rank0,
82
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & spruce')) AS rank1
81
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '17 & spruce'), 16) AS rank0,
82
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & spruce'), 16) AS rank1
83
83
  FROM "runestones"
84
84
  WHERE
85
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & spruce')
85
+ "runestones"."vector" @@ to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & spruce')
86
86
  ORDER BY rank0 DESC, rank1 DESC
87
87
  SQL
88
88
  end
@@ -98,11 +98,11 @@ class SynonymTest < ActiveSupport::TestCase
98
98
  assert_sql(<<~SQL, query.to_sql)
99
99
  SELECT
100
100
  "runestones".*,
101
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '17 & spruce:*')) AS rank0,
102
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')) AS rank1
101
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '17 & spruce:*'), 16) AS rank0,
102
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)'), 16) AS rank1
103
103
  FROM "runestones"
104
104
  WHERE
105
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
105
+ "runestones"."vector" @@ to_tsquery('runestone', '(17 | 17th | seventeen | seventeenth) & (spruce:* | pine)')
106
106
  ORDER BY rank0 DESC, rank1 DESC
107
107
  SQL
108
108
  end
@@ -116,11 +116,11 @@ class SynonymTest < ActiveSupport::TestCase
116
116
  assert_sql(<<~SQL, query.to_sql)
117
117
  SELECT
118
118
  "runestones".*,
119
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', 'one & hundred & spruce:*')) AS rank0,
120
- ts_rank_cd("runestones"."vector", to_tsquery('simple_unaccent', '((one & hundred | 100) & spruce:* | (one & hundred | one hundy) & spruce:*)')) AS rank1
119
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', 'one & hundred & spruce:*'), 16) AS rank0,
120
+ ts_rank_cd("runestones"."vector", to_tsquery('runestone', '((one & hundred | 100) & spruce:* | (one & hundred | one hundy) & spruce:*)'), 16) AS rank1
121
121
  FROM "runestones"
122
122
  WHERE
123
- "runestones"."vector" @@ to_tsquery('simple_unaccent', '((one & hundred | 100) & spruce:* | (one & hundred | one hundy) & spruce:*)')
123
+ "runestones"."vector" @@ to_tsquery('runestone', '((one & hundred | 100) & spruce:* | (one & hundred | one hundy) & spruce:*)')
124
124
  ORDER BY rank0 DESC, rank1 DESC
125
125
  SQL
126
126
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: runestone
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.0'
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-29 00:00:00.000000000 Z
11
+ date: 2020-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: '6.0'
145
+ version: 6.0.0.9
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: '6.0'
152
+ version: 6.0.0.9
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: activerecord
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -177,7 +177,7 @@ files:
177
177
  - LICENSE
178
178
  - README.md
179
179
  - Rakefile
180
- - db/migrate/20181101150207_create_ts_tables.rb
180
+ - db/migrate/20181101150207_create_runestone_tables.rb
181
181
  - lib/runestone.rb
182
182
  - lib/runestone/active_record/base_methods.rb
183
183
  - lib/runestone/active_record/relation_methods.rb
@@ -200,6 +200,7 @@ files:
200
200
  - test/highlight_test.rb
201
201
  - test/indexing_test.rb
202
202
  - test/multi_index_test.rb
203
+ - test/ordering_test.rb
203
204
  - test/query_test.rb
204
205
  - test/synonym_test.rb
205
206
  - test/test_helper.rb
@@ -207,7 +208,7 @@ homepage: https://github.com/malomalo/runestone
207
208
  licenses:
208
209
  - MIT
209
210
  metadata: {}
210
- post_install_message:
211
+ post_install_message:
211
212
  rdoc_options: []
212
213
  require_paths:
213
214
  - lib
@@ -222,8 +223,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
223
  - !ruby/object:Gem::Version
223
224
  version: '0'
224
225
  requirements: []
225
- rubygems_version: 3.0.3
226
- signing_key:
226
+ rubygems_version: 3.1.4
227
+ signing_key:
227
228
  specification_version: 4
228
229
  summary: Full Text Search for Active Record / Rails
229
230
  test_files:
@@ -234,6 +235,7 @@ test_files:
234
235
  - test/highlight_test.rb
235
236
  - test/indexing_test.rb
236
237
  - test/multi_index_test.rb
238
+ - test/ordering_test.rb
237
239
  - test/query_test.rb
238
240
  - test/synonym_test.rb
239
241
  - test/test_helper.rb