runestone 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.tm_properties +1 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +10 -0
- data/Rakefile +14 -0
- data/db/migrate/20181101150207_create_ts_tables.rb +31 -0
- data/lib/runestone.rb +103 -0
- data/lib/runestone/active_record/base_methods.rb +135 -0
- data/lib/runestone/active_record/relation_methods.rb +83 -0
- data/lib/runestone/corpus.rb +45 -0
- data/lib/runestone/engine.rb +21 -0
- data/lib/runestone/indexing_job.rb +8 -0
- data/lib/runestone/model.rb +92 -0
- data/lib/runestone/settings.rb +106 -0
- data/lib/runestone/version.rb +3 -0
- data/lib/runestone/web_search.rb +203 -0
- data/lib/runestone/web_search/and.rb +17 -0
- data/lib/runestone/web_search/or.rb +11 -0
- data/lib/runestone/web_search/phrase.rb +19 -0
- data/lib/runestone/web_search/token.rb +27 -0
- data/runestone.gemspec +32 -0
- data/test/corpus_test.rb +42 -0
- data/test/database.rb +119 -0
- data/test/delayed_index_test.rb +34 -0
- data/test/helper_test.rb +40 -0
- data/test/highlight_test.rb +26 -0
- data/test/indexing_test.rb +151 -0
- data/test/multi_index_test.rb +177 -0
- data/test/query_test.rb +129 -0
- data/test/synonym_test.rb +128 -0
- data/test/test_helper.rb +185 -0
- metadata +239 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
class Runestone::WebSearch::Phrase
|
2
|
+
attr_accessor :values, :prefix, :negative, :distance
|
3
|
+
def initialize(values, prefix: false, negative: false, distance: nil)
|
4
|
+
@values = values
|
5
|
+
@prefix = prefix
|
6
|
+
@negative = negative
|
7
|
+
@distance = distance
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
v = if values.size == 1
|
12
|
+
values.first.to_s
|
13
|
+
else
|
14
|
+
seperator = distance ? " <#{distance}> " : ' <-> '
|
15
|
+
"(#{values.map(&:to_s).join(seperator)})"
|
16
|
+
end
|
17
|
+
negative ? "!#{v}" : v
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Runestone::WebSearch::Token
|
2
|
+
attr_accessor :value, :prefix, :negative, :alts
|
3
|
+
def initialize(value, prefix: false, negative: false, alts: nil)
|
4
|
+
@value = value
|
5
|
+
@prefix = prefix
|
6
|
+
@negative = negative
|
7
|
+
@alts = alts || []
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
if negative
|
12
|
+
"!#{value}"
|
13
|
+
elsif prefix
|
14
|
+
if alts.empty?
|
15
|
+
"#{value}:*"
|
16
|
+
else
|
17
|
+
"(#{value}:* | #{alts.join(' | ')})"
|
18
|
+
end
|
19
|
+
else
|
20
|
+
if alts.empty?
|
21
|
+
value
|
22
|
+
else
|
23
|
+
"(#{value} | #{alts.join(' | ')})"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/runestone.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path("../lib/runestone/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "runestone"
|
5
|
+
s.version = Runestone::VERSION
|
6
|
+
s.authors = ["Jon Bracy"]
|
7
|
+
s.email = ["jonbracy@gmail.com"]
|
8
|
+
s.homepage = "https://github.com/malomalo/runestone"
|
9
|
+
s.summary = %q{Full Text Search for Active Record / Rails}
|
10
|
+
s.description = %q{PostgreSQL Full Text Search for Active Record and Rails}
|
11
|
+
s.license = 'MIT'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
# Developoment
|
19
|
+
s.add_development_dependency 'rake'
|
20
|
+
s.add_development_dependency 'bundler'
|
21
|
+
s.add_development_dependency 'minitest'
|
22
|
+
s.add_development_dependency 'minitest-reporters'
|
23
|
+
s.add_development_dependency 'pg'
|
24
|
+
s.add_development_dependency 'byebug'
|
25
|
+
s.add_development_dependency 'faker'
|
26
|
+
s.add_development_dependency 'simplecov'
|
27
|
+
s.add_development_dependency 'activejob', '>= 6.0'
|
28
|
+
|
29
|
+
# Runtime
|
30
|
+
s.add_runtime_dependency 'arel-extensions', '>= 6.0'
|
31
|
+
s.add_runtime_dependency 'activerecord', '>= 6.0'
|
32
|
+
end
|
data/test/corpus_test.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CorpusTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
test 'similar_words' do
|
6
|
+
Address.create(name: 'Address name broccolini')
|
7
|
+
|
8
|
+
assert_equal(
|
9
|
+
{},
|
10
|
+
Runestone::Corpus.similar_words('nam')
|
11
|
+
)
|
12
|
+
|
13
|
+
assert_equal(
|
14
|
+
{'addresz' => ['address']},
|
15
|
+
Runestone::Corpus.similar_words('addresz')
|
16
|
+
)
|
17
|
+
|
18
|
+
assert_equal(
|
19
|
+
{'brockolinl' => ['broccolini']},
|
20
|
+
Runestone::Corpus.similar_words('brockolinl')
|
21
|
+
)
|
22
|
+
|
23
|
+
assert_equal(
|
24
|
+
{
|
25
|
+
'addresz' => ['address'],
|
26
|
+
'brockolinl' => ['broccolini']
|
27
|
+
},
|
28
|
+
Runestone::Corpus.similar_words('nam', 'addresz', 'brockolinl')
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'adding words to corpus downcases them' do
|
33
|
+
Runestone::Corpus.add('Allée')
|
34
|
+
assert_equal(
|
35
|
+
{
|
36
|
+
"Allee" => ["allée"]
|
37
|
+
},
|
38
|
+
Runestone::Corpus.similar_words('Allee')
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/test/database.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
GlobalID.app = 'TestApp'
|
2
|
+
|
3
|
+
task = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new({
|
4
|
+
'adapter' => 'postgresql',
|
5
|
+
'database' => "arel-extensions-test"
|
6
|
+
})
|
7
|
+
task.drop
|
8
|
+
task.create
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection({
|
11
|
+
adapter: "postgresql",
|
12
|
+
database: "arel-extensions-test",
|
13
|
+
encoding: "utf8"
|
14
|
+
})
|
15
|
+
|
16
|
+
ActiveRecord::Migration.suppress_messages do
|
17
|
+
ActiveRecord::Schema.define do
|
18
|
+
enable_extension 'pgcrypto'
|
19
|
+
enable_extension 'pg_trgm'
|
20
|
+
enable_extension 'unaccent'
|
21
|
+
enable_extension 'fuzzystrmatch'
|
22
|
+
|
23
|
+
create_table :addresses, id: :uuid, force: :cascade do |t|
|
24
|
+
t.string "name"
|
25
|
+
t.uuid "property_id"
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table :properties, id: :uuid, force: :cascade do |t|
|
29
|
+
t.string "name", limit: 255
|
30
|
+
end
|
31
|
+
|
32
|
+
create_table :regions, id: :uuid, force: :cascade do |t|
|
33
|
+
t.string "name", limit: 255
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :buildings, id: :uuid, force: :cascade do |t|
|
37
|
+
t.string "name_en", limit: 255
|
38
|
+
t.string "name_ru", limit: 255
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :runestones, id: :uuid, force: :cascade do |t|
|
42
|
+
t.string :record_type, null: false
|
43
|
+
t.uuid :record_id, null: false
|
44
|
+
t.string :name
|
45
|
+
t.string :dictionary
|
46
|
+
t.jsonb :data, null: false
|
47
|
+
t.tsvector :vector, null: false
|
48
|
+
end
|
49
|
+
|
50
|
+
add_index :runestones, [:record_type, :record_id, :name, :dictionary], unique: true, name: 'index_runestones_for_uniqueness'
|
51
|
+
add_index :runestones, :vector, using: :gin
|
52
|
+
|
53
|
+
execute <<-SQL
|
54
|
+
CREATE TABLE runestone_corpus ( word varchar, CONSTRAINT word UNIQUE(word) );
|
55
|
+
|
56
|
+
CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
|
57
|
+
|
58
|
+
|
59
|
+
CREATE TEXT SEARCH CONFIGURATION simple_unaccent (COPY = simple);
|
60
|
+
ALTER TEXT SEARCH CONFIGURATION simple_unaccent
|
61
|
+
ALTER MAPPING FOR hword, hword_part, word
|
62
|
+
WITH unaccent, simple;
|
63
|
+
SQL
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Address < ActiveRecord::Base
|
68
|
+
|
69
|
+
belongs_to :property
|
70
|
+
|
71
|
+
runestone do
|
72
|
+
index 'name'
|
73
|
+
|
74
|
+
attribute(:name)
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
class Region < ActiveRecord::Base
|
80
|
+
|
81
|
+
include GlobalID::Identification
|
82
|
+
|
83
|
+
runestone runner: :active_job do
|
84
|
+
index 'name'
|
85
|
+
|
86
|
+
attribute(:name)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
class Property < ActiveRecord::Base
|
92
|
+
|
93
|
+
has_many :addresses
|
94
|
+
|
95
|
+
runestone do
|
96
|
+
index :name
|
97
|
+
index 'addresses.name', weight: 3
|
98
|
+
|
99
|
+
attribute(:name)
|
100
|
+
attribute(:addresses) { addresses.map{ |a| a&.attributes&.slice('id', 'name') } }
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
class Building < ActiveRecord::Base
|
106
|
+
|
107
|
+
runestone dictionary: 'english' do
|
108
|
+
index :name
|
109
|
+
|
110
|
+
attribute(:name) { name_en }
|
111
|
+
end
|
112
|
+
|
113
|
+
runestone dictionary: 'russian' do
|
114
|
+
index :name
|
115
|
+
|
116
|
+
attribute(:name) { name_ru }
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class DelayedIndexingTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
test 'simple_unaccent index' do
|
6
|
+
region = assert_no_difference 'Runestone::Model.count' do
|
7
|
+
assert_no_sql(/setweight\(to_tsvector\('simple_unaccent', 'address name'\), 'A'\)/) do
|
8
|
+
Region.create(name: 'Region name')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
job = assert_enqueued_with(
|
13
|
+
job: Runestone::IndexingJob,
|
14
|
+
args: [region, :create_runestones!],
|
15
|
+
queue: 'runestone_indexing'
|
16
|
+
)
|
17
|
+
|
18
|
+
job.perform_now
|
19
|
+
|
20
|
+
assert_equal([[
|
21
|
+
'Region', region.id,
|
22
|
+
{"name" => "Region name"},
|
23
|
+
"'name':2A 'region':1A"
|
24
|
+
]], region.runestones.map { |runestone|
|
25
|
+
[
|
26
|
+
runestone.record_type,
|
27
|
+
runestone.record_id,
|
28
|
+
runestone.data,
|
29
|
+
runestone.vector
|
30
|
+
]
|
31
|
+
})
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/test/helper_test.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class WebSearchTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
test '::parse' do
|
6
|
+
assert_equal "the & fat & rats:*", Runestone::WebSearch.parse('The fat rats').to_s
|
7
|
+
assert_equal "(supernovae <-> stars) & !crab", Runestone::WebSearch.parse('"supernovae stars" -crab').to_s
|
8
|
+
# assert_equal "(sad <-> cat) | (fat <-> rat)", TS.websearch_to_tsquery('"sad cat" || "fat rat"')
|
9
|
+
assert_equal "signal & !(segmentation <-> fault)", Runestone::WebSearch.parse('signal -"segmentation fault"').to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
test '::parse(query, prefix: :all)' do
|
13
|
+
assert_equal "the:* & fat:* & rats:*", Runestone::WebSearch.parse('The fat rats', prefix: :all).to_s
|
14
|
+
assert_equal "(supernovae <-> stars) & !crab", Runestone::WebSearch.parse('"supernovae stars" -crab', prefix: :all).to_s
|
15
|
+
# assert_equal "(sad <-> cat) | (fat <-> rat)", TS.websearch_to_tsquery('"sad cat" || "fat rat"')
|
16
|
+
assert_equal "signal:* & !(segmentation <-> fault)", Runestone::WebSearch.parse('signal -"segmentation fault"', prefix: :all).to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
test '::parse(query, prefix: :last)' do
|
20
|
+
assert_equal "the & fat & rats:*", Runestone::WebSearch.parse('The fat rats', prefix: :last).to_s
|
21
|
+
assert_equal "(supernovae <-> stars) & !crab", Runestone::WebSearch.parse('"supernovae stars" -crab', prefix: :last).to_s
|
22
|
+
# assert_equal "(sad <-> cat) | (fat <-> rat)", TS.websearch_to_tsquery('"sad cat" || "fat rat"')
|
23
|
+
assert_equal "signal & !(segmentation <-> fault)", Runestone::WebSearch.parse('signal -"segmentation fault"', prefix: :last).to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
test '::parse(query, prefix: :none)' do
|
27
|
+
assert_equal "the & fat & rats", Runestone::WebSearch.parse('The fat rats', prefix: :none).to_s
|
28
|
+
assert_equal "(supernovae <-> stars) & !crab", Runestone::WebSearch.parse('"supernovae stars" -crab', prefix: :none).to_s
|
29
|
+
# assert_equal "(sad <-> cat) | (fat <-> rat)", TS.websearch_to_tsquery('"sad cat" || "fat rat"')
|
30
|
+
assert_equal "signal & !(segmentation <-> fault)", Runestone::WebSearch.parse('signal -"segmentation fault"', prefix: :none).to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
test '::parse(weird query, prefix: :none)' do
|
34
|
+
assert_equal "(supernovae <-> stars)", Runestone::WebSearch.parse('"supernovae stars', prefix: :none).to_s
|
35
|
+
assert_equal "signal", Runestone::WebSearch.parse('signal -', prefix: :none).to_s
|
36
|
+
assert_equal "signal", Runestone::WebSearch.parse('signal -"', prefix: :none).to_s
|
37
|
+
assert_equal "signal", Runestone::WebSearch.parse('signal -""', prefix: :none).to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HighlightTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
test '::highlights(query)' do
|
6
|
+
Property.create(name: 'Empire state building', addresses: [Address.create(name: 'address uno')])
|
7
|
+
Property.create(name: 'Big state building', addresses: [Address.create(name: 'address of state duo')])
|
8
|
+
|
9
|
+
tsmodels = Runestone::Model.search('state')
|
10
|
+
Runestone::Model.highlight(tsmodels, 'state')
|
11
|
+
assert_equal([
|
12
|
+
{
|
13
|
+
"name"=>"Big <b>state</b> building",
|
14
|
+
"addresses"=> [{"name"=>"address of <b>state</b> duo"}]
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"name"=>"Empire <b>state</b> building",
|
18
|
+
"addresses"=> [{"name"=>"address uno"}]
|
19
|
+
},
|
20
|
+
{
|
21
|
+
"name"=>"address of <b>state</b> duo"
|
22
|
+
}
|
23
|
+
], tsmodels.map(&:highlights))
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class IndexingTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
test 'simple_unaccent index' do
|
6
|
+
address = assert_difference 'Runestone::Model.count', 1 do
|
7
|
+
assert_sql(/setweight\(to_tsvector\('simple_unaccent', 'address name'\), 'A'\)/) do
|
8
|
+
Address.create(name: 'Address name')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
assert_equal([[
|
13
|
+
'Address', address.id,
|
14
|
+
{"name" => "Address name"},
|
15
|
+
"'address':1A 'name':2A"
|
16
|
+
]], address.runestones.map { |runestone|
|
17
|
+
[
|
18
|
+
runestone.record_type,
|
19
|
+
runestone.record_id,
|
20
|
+
runestone.data,
|
21
|
+
runestone.vector
|
22
|
+
]
|
23
|
+
})
|
24
|
+
end
|
25
|
+
|
26
|
+
test 'empty index' do
|
27
|
+
address = assert_difference 'Runestone::Model.count', 1 do
|
28
|
+
Address.create(name: nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
assert_equal([[
|
32
|
+
'Address', address.id,
|
33
|
+
{},
|
34
|
+
""
|
35
|
+
]], address.runestones.map { |runestone|
|
36
|
+
[
|
37
|
+
runestone.record_type,
|
38
|
+
runestone.record_id,
|
39
|
+
runestone.data,
|
40
|
+
runestone.vector
|
41
|
+
]
|
42
|
+
})
|
43
|
+
end
|
44
|
+
|
45
|
+
test 'complex index' do
|
46
|
+
property = assert_difference 'Runestone::Model.count', 3 do
|
47
|
+
Property.create(name: 'Property name', addresses: [
|
48
|
+
Address.create(name: 'Address 1'),
|
49
|
+
Address.create(name: 'Address 2')
|
50
|
+
])
|
51
|
+
end
|
52
|
+
|
53
|
+
assert_equal([[
|
54
|
+
'Property', property.id,
|
55
|
+
{
|
56
|
+
"name"=>"Property name",
|
57
|
+
"addresses"=>[
|
58
|
+
{ "id" => property.addresses.first.id, "name" => "Address 1" },
|
59
|
+
{ "id" => property.addresses.last.id, "name" => "Address 2" }
|
60
|
+
]
|
61
|
+
},
|
62
|
+
"'1':4C '2':6C 'address':3C,5C 'name':2A 'property':1A"
|
63
|
+
]], property.runestones.map { |runestone|
|
64
|
+
[
|
65
|
+
runestone.record_type,
|
66
|
+
runestone.record_id,
|
67
|
+
runestone.data,
|
68
|
+
runestone.vector
|
69
|
+
]
|
70
|
+
})
|
71
|
+
end
|
72
|
+
|
73
|
+
test 'index Unicode strings get normalized' do
|
74
|
+
address = Address.create(name: "on\u0065\u0301")
|
75
|
+
|
76
|
+
assert_equal(["'on\u00e9':1A"], address.reload.runestones.map(&:vector))
|
77
|
+
end
|
78
|
+
|
79
|
+
test 'index gets created on Model.create' do
|
80
|
+
address = assert_difference 'Runestone::Model.count', 1 do
|
81
|
+
Address.create(name: 'Address name')
|
82
|
+
end
|
83
|
+
|
84
|
+
assert_corpus('address', 'name')
|
85
|
+
end
|
86
|
+
|
87
|
+
test 'index gets updated on Model.create' do
|
88
|
+
address = Address.create(name: 'Address name')
|
89
|
+
assert_no_difference 'Runestone::Model.count' do
|
90
|
+
address.update!(name: 'Address name two')
|
91
|
+
end
|
92
|
+
|
93
|
+
assert_equal([[
|
94
|
+
'Address', address.id,
|
95
|
+
{"name" => "Address name two"},
|
96
|
+
"'address':1A 'name':2A 'two':3A"
|
97
|
+
]], address.runestones.map { |runestone|
|
98
|
+
[
|
99
|
+
runestone.record_type,
|
100
|
+
runestone.record_id,
|
101
|
+
runestone.data,
|
102
|
+
runestone.vector
|
103
|
+
]
|
104
|
+
})
|
105
|
+
|
106
|
+
assert_corpus('address', 'name', 'two')
|
107
|
+
end
|
108
|
+
|
109
|
+
test 'index gets deleted on Model.destroy' do
|
110
|
+
address = Address.create(name: 'Address name')
|
111
|
+
assert_difference 'Runestone::Model.count', -1 do
|
112
|
+
address.destroy!
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
test 'reindex! deleted removed records' do
|
117
|
+
a1 = Address.create(name: 'one')
|
118
|
+
a2 = Address.create(name: 'two')
|
119
|
+
|
120
|
+
assert_no_difference 'Runestone::Model.count' do
|
121
|
+
a2.delete
|
122
|
+
end
|
123
|
+
|
124
|
+
assert_difference 'Runestone::Model.count', -1 do
|
125
|
+
Address.reindex!
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
test 'reindex! updates runestone on outdated indexes' do
|
130
|
+
address = Address.create(name: 'one')
|
131
|
+
address.update_columns(name: 'two')
|
132
|
+
|
133
|
+
assert_equal(["'one':1A"], address.runestones.map(&:vector))
|
134
|
+
assert_no_difference 'Runestone::Model.count' do
|
135
|
+
Address.reindex!
|
136
|
+
end
|
137
|
+
assert_equal(["'two':1A"], address.runestones.map { |runestone| runestone.reload.vector })
|
138
|
+
end
|
139
|
+
|
140
|
+
test 'reindex! creates index if not there' do
|
141
|
+
address = Address.create(name: 'one')
|
142
|
+
address.runestones.each(&:delete)
|
143
|
+
|
144
|
+
assert_equal 0, address.reload.runestones.size
|
145
|
+
assert_difference 'Runestone::Model.count', 1 do
|
146
|
+
Address.reindex!
|
147
|
+
end
|
148
|
+
assert_equal(["'one':1A"], address.reload.runestones.map(&:vector))
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|