runestone 1.0 → 2.0.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: 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