runestone 2.0.1 → 2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +45 -0
- data/.gitignore +1 -0
- data/README.md +36 -3
- data/lib/runestone/active_record/base_methods.rb +3 -2
- data/lib/runestone/engine.rb +5 -6
- data/lib/runestone/psql_schema_dumper.rb +17 -0
- data/lib/runestone/settings.rb +21 -4
- data/lib/runestone/version.rb +1 -1
- data/lib/runestone.rb +1 -35
- data/runestone.gemspec +3 -3
- data/test/database.rb +30 -13
- data/test/indexing_test.rb +170 -1
- data/test/test_helper.rb +56 -36
- metadata +11 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96c3c19ecdb5ba5e58fae758af825532f6dedd8a9ace5291a0e70ad3b28e2436
|
4
|
+
data.tar.gz: eab646d44fb2e24cd14ee1f98fcbbba7944fba4907cb2bb0de452dbdb21554cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d728fbc86eedba51c8fc230ca6a49960d7e63a4f7f95af31a5c28cb619df55385c505ac874ca23f53c89b9bba7a2e23d4279ef5583580df05e866b55d145c001
|
7
|
+
data.tar.gz: 8a536c404142691627e26d6d6a74f3434df09a34ae326df390cfb5621e563433dae7f0123985ec0ec4f9168239e0a45b0d942dc12e0f50c59baa60a146a13f62
|
@@ -0,0 +1,45 @@
|
|
1
|
+
name: Test Suite
|
2
|
+
on: [push, pull_request]
|
3
|
+
|
4
|
+
jobs:
|
5
|
+
test:
|
6
|
+
runs-on: ubuntu-22.04
|
7
|
+
strategy:
|
8
|
+
matrix:
|
9
|
+
rails-version:
|
10
|
+
- 7.0.8
|
11
|
+
- 7.1.2
|
12
|
+
ruby-version:
|
13
|
+
- 3.0
|
14
|
+
- 3.1
|
15
|
+
- 3.2
|
16
|
+
postgres-version:
|
17
|
+
- 15
|
18
|
+
|
19
|
+
steps:
|
20
|
+
- name: Install Postgresql
|
21
|
+
run: |
|
22
|
+
sudo apt-get -y --purge remove $(sudo apt list --installed | grep postgresql | awk '{print $1}')
|
23
|
+
sudo apt-get install curl ca-certificates gnupg
|
24
|
+
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
25
|
+
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
26
|
+
sudo apt-get update
|
27
|
+
sudo apt-get -y install postgresql-${{ matrix.postgres-version }}-postgis-3
|
28
|
+
sudo systemctl start postgresql@${{ matrix.postgres-version }}-main.service
|
29
|
+
sudo systemctl status postgresql@${{ matrix.postgres-version }}-main.service
|
30
|
+
sudo pg_lsclusters
|
31
|
+
sudo -u postgres createuser runner --superuser
|
32
|
+
sudo -u postgres psql -c "ALTER USER runner WITH PASSWORD 'runner';"
|
33
|
+
|
34
|
+
- uses: actions/checkout@v4
|
35
|
+
|
36
|
+
- run: |
|
37
|
+
echo 'gem "activerecord", "${{ matrix.rails-version }}"' >> Gemfile
|
38
|
+
|
39
|
+
- uses: ruby/setup-ruby@v1
|
40
|
+
with:
|
41
|
+
ruby-version: ${{ matrix.ruby-version }}
|
42
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
43
|
+
|
44
|
+
- name: Run Test Suite
|
45
|
+
run: bundle exec rake test
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -51,7 +51,40 @@ class Building < ApplicationRecord
|
|
51
51
|
end
|
52
52
|
```
|
53
53
|
|
54
|
-
When searching the attribute(s) will be available in `data` on the result(s),
|
54
|
+
When searching the attribute(s) will be available in `data` on the result(s),
|
55
|
+
but only the attributes specified by `index` will indexed and used for searching.
|
56
|
+
|
57
|
+
Generally Runestone will automatically update the search index if changes are
|
58
|
+
made. This is done by seeing if the corresponding column or association has
|
59
|
+
changed. If your search attribute is generated you need to define on the columns
|
60
|
+
or associations it depends on.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class User < ApplicationRecord
|
64
|
+
runestone do
|
65
|
+
# The attribute `:name` is generated from the `name_en` column
|
66
|
+
attribute(:name, :name_en) { name_en }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Building < ApplicationRecord
|
71
|
+
runestone do
|
72
|
+
# The attribute `:address_numbers` is generated from the association `addresses`
|
73
|
+
attribute(:address_numbers, :addresses) { addresses.map{ |a| a.number } }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class User < ActiveRecord::Base
|
78
|
+
runestone do
|
79
|
+
index 'name'
|
80
|
+
|
81
|
+
# The attribute `:name` is updated when the custom logic proc returns true
|
82
|
+
attribute :name, -> () { ...custom logic... } do
|
83
|
+
name
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
55
88
|
|
56
89
|
### Searching
|
57
90
|
|
@@ -95,7 +128,7 @@ characters to the ASCII equivlent.
|
|
95
128
|
```ruby
|
96
129
|
module RailsApplicationName
|
97
130
|
class Application < Rails::Application
|
98
|
-
config.runestone.dictionary = :
|
131
|
+
config.runestone.dictionary = :runestone
|
99
132
|
end
|
100
133
|
end
|
101
134
|
```
|
@@ -103,7 +136,7 @@ end
|
|
103
136
|
If you are not using Rails, you can use the following:
|
104
137
|
|
105
138
|
```ruby
|
106
|
-
Runestone.dictionary = :
|
139
|
+
Runestone.dictionary = :runestone
|
107
140
|
```
|
108
141
|
|
109
142
|
#### normalization for ranking
|
@@ -51,7 +51,7 @@ module Runestone::ActiveRecord
|
|
51
51
|
AND #{model_table}.id IS NULL;
|
52
52
|
SQL
|
53
53
|
|
54
|
-
find_each
|
54
|
+
find_each { |r| r.update_runestones!(false) }
|
55
55
|
end
|
56
56
|
|
57
57
|
def highlights(name: :default, dictionary: nil)
|
@@ -106,10 +106,11 @@ module Runestone::ActiveRecord
|
|
106
106
|
Runestone::IndexingJob.preform_later(self, :update_runestones!)
|
107
107
|
end
|
108
108
|
|
109
|
-
def update_runestones!
|
109
|
+
def update_runestones!(if_changed = true)
|
110
110
|
conn = Runestone::Model.connection
|
111
111
|
self.runestone_settings.each do |index_name, settings|
|
112
112
|
settings.each do |setting|
|
113
|
+
next if if_changed && !setting.changed?(self)
|
113
114
|
rdata = setting.extract_attributes(self)
|
114
115
|
|
115
116
|
if conn.execute(<<-SQL).cmd_tuples == 0
|
data/lib/runestone/engine.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
class Runestone::Engine < Rails::Engine
|
2
2
|
config.runestone = ActiveSupport::OrderedOptions.new
|
3
3
|
|
4
|
-
initializer :
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
initializer :runestone do |app|
|
5
|
+
ActiveSupport.on_load(:active_record) do
|
6
|
+
require 'active_record/connection_adapters/postgresql/schema_dumper'
|
7
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(Runestone::PsqlSchemaDumper)
|
8
|
+
ActiveRecord::Tasks::DatabaseTasks.migrations_paths << File.expand_path('../../../db/migrate', __FILE__)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
@@ -18,5 +18,4 @@ class Runestone::Engine < Rails::Engine
|
|
18
18
|
Runestone.job_queue = options.job_queue if options.job_queue
|
19
19
|
Runestone.typo_tolerances = options.typo_tolerances if options.typo_tolerances
|
20
20
|
end
|
21
|
-
|
22
21
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Runestone::PsqlSchemaDumper
|
2
|
+
|
3
|
+
def extensions(stream)
|
4
|
+
super(stream)
|
5
|
+
stream.puts <<-RB
|
6
|
+
## Install the default dictionary for Runestone in the database
|
7
|
+
execute <<-SQL
|
8
|
+
CREATE TEXT SEARCH CONFIGURATION runestone (COPY = simple);
|
9
|
+
ALTER TEXT SEARCH CONFIGURATION runestone
|
10
|
+
ALTER MAPPING FOR hword, hword_part, word
|
11
|
+
WITH unaccent, simple;
|
12
|
+
SQL
|
13
|
+
RB
|
14
|
+
stream
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/lib/runestone/settings.rb
CHANGED
@@ -14,11 +14,16 @@ class Runestone::Settings
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def attribute(*names, &block)
|
17
|
-
|
17
|
+
deps = if block_given? and names.length > 2
|
18
|
+
raise ArgumentError.new('Cannot pass multiple attribute names if block given')
|
19
|
+
else
|
20
|
+
names.length > 1 ? names.pop : names.first
|
21
|
+
end
|
22
|
+
deps = deps.to_s if !deps.is_a?(Proc)
|
18
23
|
|
19
24
|
@attributes ||= {}
|
20
25
|
names.each do |name|
|
21
|
-
@attributes[name.to_sym] = block ? block : nil
|
26
|
+
@attributes[name.to_sym] = [block ? block : nil, deps]
|
22
27
|
end
|
23
28
|
end
|
24
29
|
alias :attributes :attribute
|
@@ -27,8 +32,8 @@ class Runestone::Settings
|
|
27
32
|
attributes = {}
|
28
33
|
|
29
34
|
@attributes.each do |name, value|
|
30
|
-
attributes[name] = if value.is_a?(Proc)
|
31
|
-
record.instance_exec(&value)
|
35
|
+
attributes[name] = if value[0].is_a?(Proc)
|
36
|
+
record.instance_exec(&value[0])
|
32
37
|
else
|
33
38
|
rv = record.send(name)
|
34
39
|
end
|
@@ -37,6 +42,18 @@ class Runestone::Settings
|
|
37
42
|
remove_nulls(attributes)
|
38
43
|
end
|
39
44
|
|
45
|
+
def changed?(record)
|
46
|
+
@attributes.detect do |name, value|
|
47
|
+
if value[1].is_a?(Proc)
|
48
|
+
record.instance_exec(&value[1])
|
49
|
+
elsif record.attribute_names.include?(value[1])
|
50
|
+
record.previous_changes.has_key?(value[1])
|
51
|
+
elsif record._reflections[value[1]] && association = record.association(value[1])
|
52
|
+
association.loaded? && association.changed_for_autosave?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
40
57
|
def vectorize(data)
|
41
58
|
conn = Runestone::Model.connection
|
42
59
|
tsvector = []
|
data/lib/runestone/version.rb
CHANGED
data/lib/runestone.rb
CHANGED
@@ -5,6 +5,7 @@ module Runestone
|
|
5
5
|
autoload :Settings, "#{File.dirname(__FILE__)}/runestone/settings"
|
6
6
|
autoload :WebSearch, "#{File.dirname(__FILE__)}/runestone/web_search"
|
7
7
|
autoload :IndexingJob, "#{File.dirname(__FILE__)}/runestone/indexing_job"
|
8
|
+
autoload :PsqlSchemaDumper, "#{File.dirname(__FILE__)}/runestone/psql_schema_dumper"
|
8
9
|
|
9
10
|
mattr_accessor :dictionary, default: :runestone
|
10
11
|
mattr_accessor :normalization, default: 16
|
@@ -16,37 +17,6 @@ module Runestone
|
|
16
17
|
{ }
|
17
18
|
end
|
18
19
|
|
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
|
-
|
50
20
|
def self.normalize(string)
|
51
21
|
string = string.downcase
|
52
22
|
string = string.unicode_normalize!
|
@@ -61,10 +31,6 @@ module Runestone
|
|
61
31
|
rescue Encoding::CompatibilityError
|
62
32
|
end
|
63
33
|
|
64
|
-
def transliterate(string)
|
65
|
-
string.gsub(/[^\x00-\x7f]/u) { |char| approximations[char] || char }
|
66
|
-
end
|
67
|
-
|
68
34
|
def self.add_synonyms(dictionary)
|
69
35
|
dictionary.each do |k, v|
|
70
36
|
add_synonym(k, *v)
|
data/runestone.gemspec
CHANGED
@@ -24,9 +24,9 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.add_development_dependency 'byebug'
|
25
25
|
s.add_development_dependency 'faker'
|
26
26
|
s.add_development_dependency 'simplecov'
|
27
|
-
s.add_development_dependency 'activejob', '>=
|
27
|
+
s.add_development_dependency 'activejob', '>= 7.0'
|
28
28
|
|
29
29
|
# Runtime
|
30
|
-
s.add_runtime_dependency 'arel-extensions', '>=
|
31
|
-
s.add_runtime_dependency 'activerecord', '>=
|
30
|
+
s.add_runtime_dependency 'arel-extensions', '>= 7.0.0'
|
31
|
+
s.add_runtime_dependency 'activerecord', '>= 7.0'
|
32
32
|
end
|
data/test/database.rb
CHANGED
@@ -1,18 +1,16 @@
|
|
1
1
|
GlobalID.app = 'TestApp'
|
2
2
|
|
3
|
-
task = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new({
|
4
|
-
'adapter' => 'postgresql',
|
5
|
-
'database' => "arel-extensions-test"
|
6
|
-
})
|
7
|
-
task.drop
|
8
|
-
task.create
|
9
|
-
|
10
3
|
ActiveRecord::Base.establish_connection({
|
11
4
|
adapter: "postgresql",
|
12
5
|
database: "arel-extensions-test",
|
13
6
|
encoding: "utf8"
|
14
7
|
})
|
15
8
|
|
9
|
+
db_config = ActiveRecord::Base.connection_db_config
|
10
|
+
task = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new(db_config)
|
11
|
+
task.drop
|
12
|
+
task.create
|
13
|
+
|
16
14
|
ActiveRecord::Migration.suppress_messages do
|
17
15
|
ActiveRecord::Schema.define do
|
18
16
|
enable_extension 'pgcrypto'
|
@@ -22,21 +20,29 @@ ActiveRecord::Migration.suppress_messages do
|
|
22
20
|
|
23
21
|
create_table :addresses, id: :uuid, force: :cascade do |t|
|
24
22
|
t.string "name"
|
23
|
+
t.string "metadata"
|
25
24
|
t.uuid "property_id"
|
26
25
|
end
|
27
26
|
|
28
27
|
create_table :properties, id: :uuid, force: :cascade do |t|
|
29
|
-
t.string
|
28
|
+
t.string "name", limit: 255
|
29
|
+
t.string "metadata"
|
30
30
|
end
|
31
31
|
|
32
32
|
create_table :regions, id: :uuid, force: :cascade do |t|
|
33
33
|
t.string "name", limit: 255
|
34
|
+
t.boolean "pooblic", default: true
|
34
35
|
end
|
35
36
|
|
36
37
|
create_table :buildings, id: :uuid, force: :cascade do |t|
|
37
38
|
t.string "name_en", limit: 255
|
38
39
|
t.string "name_ru", limit: 255
|
39
40
|
end
|
41
|
+
|
42
|
+
create_table :people, id: :uuid, force: :cascade do |t|
|
43
|
+
t.string "name", limit: 255
|
44
|
+
t.boolean "pooblic", default: true
|
45
|
+
end
|
40
46
|
|
41
47
|
create_table :runestones, id: :uuid, force: :cascade do |t|
|
42
48
|
t.string :record_type, null: false
|
@@ -51,10 +57,9 @@ ActiveRecord::Migration.suppress_messages do
|
|
51
57
|
add_index :runestones, :vector, using: :gin
|
52
58
|
|
53
59
|
execute <<-SQL
|
54
|
-
|
55
|
-
|
56
|
-
CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
|
60
|
+
CREATE TABLE runestone_corpus ( word varchar, CONSTRAINT word UNIQUE(word) );
|
57
61
|
|
62
|
+
CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
|
58
63
|
|
59
64
|
CREATE TEXT SEARCH CONFIGURATION runestone (COPY = simple);
|
60
65
|
ALTER TEXT SEARCH CONFIGURATION runestone
|
@@ -90,7 +95,7 @@ end
|
|
90
95
|
|
91
96
|
class Property < ActiveRecord::Base
|
92
97
|
|
93
|
-
has_many :addresses
|
98
|
+
has_many :addresses, autosave: true
|
94
99
|
|
95
100
|
runestone do
|
96
101
|
index :name
|
@@ -107,7 +112,7 @@ class Building < ActiveRecord::Base
|
|
107
112
|
runestone dictionary: 'english' do
|
108
113
|
index :name
|
109
114
|
|
110
|
-
attribute(:name) { name_en }
|
115
|
+
attribute(:name, :name_en) { name_en }
|
111
116
|
end
|
112
117
|
|
113
118
|
runestone dictionary: 'russian' do
|
@@ -116,4 +121,16 @@ class Building < ActiveRecord::Base
|
|
116
121
|
attribute(:name) { name_ru }
|
117
122
|
end
|
118
123
|
|
124
|
+
end
|
125
|
+
|
126
|
+
class Person < ActiveRecord::Base
|
127
|
+
|
128
|
+
runestone do
|
129
|
+
index 'name'
|
130
|
+
|
131
|
+
attribute :name, -> () { pooblic } do
|
132
|
+
name
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
119
136
|
end
|
data/test/indexing_test.rb
CHANGED
@@ -84,7 +84,7 @@ class IndexingTest < ActiveSupport::TestCase
|
|
84
84
|
assert_corpus('address', 'name')
|
85
85
|
end
|
86
86
|
|
87
|
-
test 'index gets updated on Model.
|
87
|
+
test 'index gets updated on Model.update' do
|
88
88
|
address = Address.create(name: 'Address name')
|
89
89
|
assert_no_difference 'Runestone::Model.count' do
|
90
90
|
address.update!(name: 'Address name two')
|
@@ -106,6 +106,175 @@ class IndexingTest < ActiveSupport::TestCase
|
|
106
106
|
assert_corpus('address', 'name', 'two')
|
107
107
|
end
|
108
108
|
|
109
|
+
test 'index doesnt update on Model.update when updates dont affect index (column for attribute, depends on itself)' do
|
110
|
+
address = Address.create(name: 'Address name')
|
111
|
+
assert_no_difference 'Runestone::Model.count' do
|
112
|
+
assert_no_sql("UPDATE runestones SET data") do
|
113
|
+
address.update!(metadata: 'extra info not used in index')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
assert_equal([[
|
118
|
+
'Address', address.id,
|
119
|
+
{"name" => "Address name"},
|
120
|
+
"'address':1A 'name':2A"
|
121
|
+
]], address.runestones.map { |runestone|
|
122
|
+
[
|
123
|
+
runestone.record_type,
|
124
|
+
runestone.record_id,
|
125
|
+
runestone.data,
|
126
|
+
runestone.vector
|
127
|
+
]
|
128
|
+
})
|
129
|
+
|
130
|
+
assert_corpus('address', 'name')
|
131
|
+
end
|
132
|
+
|
133
|
+
test 'index doesnt update on Model.update when updates dont affect index (proc for attribute, depends on relation)' do
|
134
|
+
property = Property.create(name: 'Property name', addresses: [
|
135
|
+
Address.create(name: 'Address 1'),
|
136
|
+
Address.create(name: 'Address 2')
|
137
|
+
])
|
138
|
+
|
139
|
+
assert_no_difference 'Runestone::Model.count' do
|
140
|
+
assert_no_sql("UPDATE runestones SET data") do
|
141
|
+
property.update!(metadata: 'extra info not used in index')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
assert_equal([[
|
146
|
+
'Property', property.id,
|
147
|
+
{"name"=>"Property name", "addresses"=>[
|
148
|
+
{"id"=> property.addresses[0].id, "name"=>"Address 1"},
|
149
|
+
{"id"=> property.addresses[1].id, "name"=>"Address 2"}
|
150
|
+
]},
|
151
|
+
"'1':4C '2':6C 'address':3C,5C 'name':2A 'property':1A"
|
152
|
+
]], property.runestones.map { |runestone|
|
153
|
+
[
|
154
|
+
runestone.record_type,
|
155
|
+
runestone.record_id,
|
156
|
+
runestone.data,
|
157
|
+
runestone.vector
|
158
|
+
]
|
159
|
+
})
|
160
|
+
|
161
|
+
assert_corpus("1", "2", "address", "name", "property")
|
162
|
+
end
|
163
|
+
|
164
|
+
test 'index updates on Model.update when relation is load/changed (proc for attribute, depends on relation)' do
|
165
|
+
property = Property.create(name: 'Property name', addresses: [
|
166
|
+
Address.create(name: 'Address 1'),
|
167
|
+
Address.create(name: 'Address 2')
|
168
|
+
])
|
169
|
+
|
170
|
+
assert_no_difference 'Runestone::Model.count' do
|
171
|
+
assert_sql("UPDATE runestones SET data") do
|
172
|
+
property.addresses.first.name = 'Address rename 1'
|
173
|
+
property.save
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
assert_equal([[
|
178
|
+
'Property', property.id,
|
179
|
+
{"name"=>"Property name", "addresses"=>[
|
180
|
+
{"id"=> property.addresses[0].id, "name"=>"Address 1"},
|
181
|
+
{"id"=> property.addresses[1].id, "name"=>"Address 2"}
|
182
|
+
]},
|
183
|
+
"'1':4C '2':6C 'address':3C,5C 'name':2A 'property':1A"
|
184
|
+
]], property.runestones.map { |runestone|
|
185
|
+
[
|
186
|
+
runestone.record_type,
|
187
|
+
runestone.record_id,
|
188
|
+
runestone.data,
|
189
|
+
runestone.vector
|
190
|
+
]
|
191
|
+
})
|
192
|
+
|
193
|
+
assert_corpus("1", "2", "address", "name", "property", "rename")
|
194
|
+
end
|
195
|
+
|
196
|
+
test 'index doesnt update on Model.update when updates dont affect index (block for attribute, depends on attribute)' do
|
197
|
+
building = Building.create(name_en: 'name', name_ru: 'имя')
|
198
|
+
|
199
|
+
assert_no_difference 'Runestone::Model.count' do
|
200
|
+
assert_no_sql(/UPDATE runestones\s+SET\s+data = '{"name":"имя"}/i) do
|
201
|
+
building.update!(name_en: 'name 2')
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
assert_equal([[
|
206
|
+
'Building', building.id,
|
207
|
+
{"name"=>"name 2"},
|
208
|
+
"'2':2A 'name':1A"
|
209
|
+
], [
|
210
|
+
'Building', building.id,
|
211
|
+
{"name"=>"имя"},
|
212
|
+
"'имя':1A"
|
213
|
+
]
|
214
|
+
], building.runestones.map { |runestone|
|
215
|
+
[
|
216
|
+
runestone.record_type,
|
217
|
+
runestone.record_id,
|
218
|
+
runestone.data,
|
219
|
+
runestone.vector
|
220
|
+
]
|
221
|
+
})
|
222
|
+
|
223
|
+
assert_corpus('2', 'name', 'м')
|
224
|
+
end
|
225
|
+
|
226
|
+
test 'index doesnt update on Model.update when updates dont affect index (block for attribute, block for depends)' do
|
227
|
+
record = Person.create(name: 'person')
|
228
|
+
|
229
|
+
assert_no_difference 'Runestone::Model.count' do
|
230
|
+
assert_no_sql("UPDATE runestones SET data") do
|
231
|
+
record.update!(name: 'ghost', pooblic: false)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
assert_equal([[
|
236
|
+
'Person', record.id,
|
237
|
+
{"name"=>"person"},
|
238
|
+
"'person':1A"
|
239
|
+
]
|
240
|
+
], record.runestones.map { |runestone|
|
241
|
+
[
|
242
|
+
runestone.record_type,
|
243
|
+
runestone.record_id,
|
244
|
+
runestone.data,
|
245
|
+
runestone.vector
|
246
|
+
]
|
247
|
+
})
|
248
|
+
|
249
|
+
assert_corpus('person')
|
250
|
+
end
|
251
|
+
|
252
|
+
test 'index updates on Model.update when updates affect index (block for attribute, block for depends)' do
|
253
|
+
record = Person.create(name: 'person')
|
254
|
+
|
255
|
+
assert_no_difference 'Runestone::Model.count' do
|
256
|
+
assert_sql("UPDATE runestones SET data") do
|
257
|
+
record.update!(name: 'physical', pooblic: true)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
assert_equal([[
|
262
|
+
'Person', record.id,
|
263
|
+
{"name"=>"physical"},
|
264
|
+
"'physical':1A"
|
265
|
+
]
|
266
|
+
], record.runestones.map { |runestone|
|
267
|
+
[
|
268
|
+
runestone.record_type,
|
269
|
+
runestone.record_id,
|
270
|
+
runestone.data,
|
271
|
+
runestone.vector
|
272
|
+
]
|
273
|
+
})
|
274
|
+
|
275
|
+
assert_corpus('person', 'physical')
|
276
|
+
end
|
277
|
+
|
109
278
|
test 'index gets deleted on Model.destroy' do
|
110
279
|
address = Address.create(name: 'Address name')
|
111
280
|
assert_difference 'Runestone::Model.count', -1 do
|
data/test/test_helper.rb
CHANGED
@@ -63,50 +63,70 @@ class ActiveSupport::TestCase
|
|
63
63
|
ActiveRecord::Base.logger = nil
|
64
64
|
$debugging = false
|
65
65
|
end
|
66
|
+
|
67
|
+
def assert_sql(*expected)
|
68
|
+
return_value = nil
|
69
|
+
|
70
|
+
queries_ran = if block_given?
|
71
|
+
queries_ran = SQLLogger.log.size
|
72
|
+
return_value = yield if block_given?
|
73
|
+
SQLLogger.log[queries_ran...]
|
74
|
+
else
|
75
|
+
[expected.pop]
|
76
|
+
end
|
66
77
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
78
|
+
failed_patterns = []
|
79
|
+
|
80
|
+
expected.each do |pattern|
|
81
|
+
failed_patterns << pattern unless queries_ran.any?{ |sql| sql_equal(pattern, sql) }
|
82
|
+
end
|
83
|
+
|
84
|
+
assert failed_patterns.empty?, <<~MSG
|
85
|
+
Query pattern(s) not found:
|
86
|
+
- #{failed_patterns.map(&:inspect).join('\n - ')}
|
87
|
+
|
88
|
+
Queries Ran (queries_ran.size):
|
89
|
+
- #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
|
90
|
+
MSG
|
91
|
+
|
92
|
+
return_value
|
72
93
|
end
|
73
94
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
end
|
87
|
-
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLLogger.log.size == 0 ? '' : "\nQueries:\n#{SQLLogger.log.join("\n")}"}"
|
88
|
-
end
|
95
|
+
def assert_no_sql(*not_expected)
|
96
|
+
return_value = nil
|
97
|
+
queries_ran = block_given? ? SQLLogger.log.size : 0
|
98
|
+
|
99
|
+
return_value = yield if block_given?
|
100
|
+
|
101
|
+
ensure
|
102
|
+
failed_patterns = []
|
103
|
+
queries_ran = SQLLogger.log[queries_ran...]
|
104
|
+
|
105
|
+
not_expected.each do |pattern|
|
106
|
+
failed_patterns << pattern if queries_ran.any?{ |sql| sql_equal(pattern, sql) }
|
89
107
|
end
|
108
|
+
|
109
|
+
assert failed_patterns.empty?, <<~MSG
|
110
|
+
Unexpected Query pattern(s) found:
|
111
|
+
- #{failed_patterns.map(&:inspect).join('\n - ')}
|
112
|
+
|
113
|
+
Queries Ran (queries_ran.size):
|
114
|
+
- #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
|
115
|
+
MSG
|
116
|
+
|
117
|
+
return_value
|
90
118
|
end
|
91
119
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
begin
|
97
|
-
ret_value = nil
|
98
|
-
capture_sql { ret_value = yield }
|
99
|
-
ret_value
|
100
|
-
ensure
|
101
|
-
failed_patterns = []
|
102
|
-
patterns_to_match.each do |pattern|
|
103
|
-
failed_patterns << pattern unless SQLLogger.log_all.any?{ |sql| pattern === sql }
|
104
|
-
end
|
105
|
-
assert !failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} found.#{SQLLogger.log.size == 0 ? '' : "\nQueries:\n#{SQLLogger.log.join("\n")}"}"
|
106
|
-
end
|
120
|
+
def sql_equal(expected, sql)
|
121
|
+
sql = sql.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/[\s|\n]+/, ' ')
|
122
|
+
if expected.is_a?(String)
|
123
|
+
expected = Regexp.new(Regexp.escape(expected.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/[\s|\n]+/, ' ')), Regexp::IGNORECASE)
|
107
124
|
end
|
125
|
+
|
126
|
+
expected.match(sql)
|
108
127
|
end
|
109
128
|
|
129
|
+
|
110
130
|
def corpus
|
111
131
|
Runestone::Model.connection.execute('SELECT word FROM runestone_corpus ORDER BY word').values.flatten
|
112
132
|
end
|
@@ -157,7 +177,7 @@ class ActiveSupport::TestCase
|
|
157
177
|
self.class.log_all << sql
|
158
178
|
unless ignore =~ sql
|
159
179
|
if $debugging
|
160
|
-
puts caller.select { |l| l.
|
180
|
+
puts caller.select { |l| l.start_with?(File.expand_path('../../lib', __FILE__)) }
|
161
181
|
puts "\n\n"
|
162
182
|
end
|
163
183
|
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: 2.
|
4
|
+
version: '2.1'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Bracy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -128,42 +128,42 @@ dependencies:
|
|
128
128
|
requirements:
|
129
129
|
- - ">="
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
131
|
+
version: '7.0'
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: '
|
138
|
+
version: '7.0'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: arel-extensions
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - ">="
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version:
|
145
|
+
version: 7.0.0
|
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:
|
152
|
+
version: 7.0.0
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: activerecord
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ">="
|
158
158
|
- !ruby/object:Gem::Version
|
159
|
-
version: '
|
159
|
+
version: '7.0'
|
160
160
|
type: :runtime
|
161
161
|
prerelease: false
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
163
163
|
requirements:
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
|
-
version: '
|
166
|
+
version: '7.0'
|
167
167
|
description: PostgreSQL Full Text Search for Active Record and Rails
|
168
168
|
email:
|
169
169
|
- jonbracy@gmail.com
|
@@ -171,6 +171,7 @@ executables: []
|
|
171
171
|
extensions: []
|
172
172
|
extra_rdoc_files: []
|
173
173
|
files:
|
174
|
+
- ".github/workflows/main.yml"
|
174
175
|
- ".gitignore"
|
175
176
|
- ".tm_properties"
|
176
177
|
- Gemfile
|
@@ -185,6 +186,7 @@ files:
|
|
185
186
|
- lib/runestone/engine.rb
|
186
187
|
- lib/runestone/indexing_job.rb
|
187
188
|
- lib/runestone/model.rb
|
189
|
+
- lib/runestone/psql_schema_dumper.rb
|
188
190
|
- lib/runestone/settings.rb
|
189
191
|
- lib/runestone/version.rb
|
190
192
|
- lib/runestone/web_search.rb
|
@@ -223,7 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
223
225
|
- !ruby/object:Gem::Version
|
224
226
|
version: '0'
|
225
227
|
requirements: []
|
226
|
-
rubygems_version: 3.
|
228
|
+
rubygems_version: 3.5.4
|
227
229
|
signing_key:
|
228
230
|
specification_version: 4
|
229
231
|
summary: Full Text Search for Active Record / Rails
|