runestone 2.0.0 → 2.1
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 +4 -4
- data/.github/workflows/main.yml +45 -0
- data/.gitignore +1 -0
- data/README.md +56 -3
- data/lib/runestone/active_record/base_methods.rb +4 -3
- data/lib/runestone/engine.rb +6 -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
|
|
@@ -93,7 +126,17 @@ is the `simple` dictionary in PostgreSQL with `unaccent` to tranliterate some
|
|
93
126
|
characters to the ASCII equivlent.
|
94
127
|
|
95
128
|
```ruby
|
96
|
-
|
129
|
+
module RailsApplicationName
|
130
|
+
class Application < Rails::Application
|
131
|
+
config.runestone.dictionary = :runestone
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
If you are not using Rails, you can use the following:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
Runestone.dictionary = :runestone
|
97
140
|
```
|
98
141
|
|
99
142
|
#### normalization for ranking
|
@@ -102,7 +145,17 @@ Ranking can be configured to use the `normalization` paramater as described
|
|
102
145
|
in the [PostgreSQL documentation][3]. The default is `16`
|
103
146
|
|
104
147
|
```ruby
|
105
|
-
|
148
|
+
module RailsApplicationName
|
149
|
+
class Application < Rails::Application
|
150
|
+
config.runestone.normalization = 1|16
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
If you are not using Rails, you can use the following:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Runestone.normalization = 16
|
106
159
|
```
|
107
160
|
|
108
161
|
[1]: http://rachbelaid.com/postgres-full-text-search-is-good-enough/
|
@@ -51,13 +51,13 @@ 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)
|
58
58
|
dictionary ||= Runestone.dictionary
|
59
59
|
|
60
|
-
rsettings = self.runestone_settings[name].find { |s| s.dictionary.to_s == dictionary.to_s}
|
60
|
+
rsettings = self.runestone_settings[name].find { |s| s.dictionary.to_s == dictionary.to_s }
|
61
61
|
@highlights ||= highlight_indexes(rsettings.indexes.values.flatten.map{ |i| i.to_s.split('.') })
|
62
62
|
end
|
63
63
|
|
@@ -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
|
|
@@ -14,8 +14,8 @@ class Runestone::Engine < Rails::Engine
|
|
14
14
|
|
15
15
|
Runestone.runner = options.runner if options.runner
|
16
16
|
Runestone.dictionary = options.dictionary if options.dictionary
|
17
|
+
Runestone.normalization = options.normalization if options.normalization
|
17
18
|
Runestone.job_queue = options.job_queue if options.job_queue
|
18
19
|
Runestone.typo_tolerances = options.typo_tolerances if options.typo_tolerances
|
19
20
|
end
|
20
|
-
|
21
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
|