litestack 0.3.0 → 0.4.2
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/.standard.yml +3 -0
- data/BENCHMARKS.md +34 -7
- data/CHANGELOG.md +21 -0
- data/Gemfile +1 -5
- data/Gemfile.lock +92 -0
- data/README.md +120 -6
- data/ROADMAP.md +45 -0
- data/Rakefile +3 -1
- data/WHYLITESTACK.md +1 -1
- data/assets/litecache_metrics.png +0 -0
- data/assets/litedb_metrics.png +0 -0
- data/assets/litemetric_logo_teal.png +0 -0
- data/assets/litesearch_logo_teal.png +0 -0
- data/bench/bench.rb +17 -10
- data/bench/bench_cache_rails.rb +10 -13
- data/bench/bench_cache_raw.rb +17 -22
- data/bench/bench_jobs_rails.rb +19 -13
- data/bench/bench_jobs_raw.rb +17 -10
- data/bench/bench_queue.rb +4 -6
- data/bench/rails_job.rb +5 -7
- data/bench/skjob.rb +4 -4
- data/bench/uljob.rb +6 -6
- data/lib/action_cable/subscription_adapter/litecable.rb +5 -8
- data/lib/active_job/queue_adapters/litejob_adapter.rb +6 -8
- data/lib/active_record/connection_adapters/litedb_adapter.rb +65 -75
- data/lib/active_support/cache/litecache.rb +38 -41
- data/lib/generators/litestack/install/install_generator.rb +3 -3
- data/lib/generators/litestack/install/templates/database.yml +7 -1
- data/lib/litestack/liteboard/liteboard.rb +269 -149
- data/lib/litestack/litecable.rb +44 -40
- data/lib/litestack/litecable.sql.yml +22 -11
- data/lib/litestack/litecache.rb +80 -89
- data/lib/litestack/litecache.sql.yml +81 -22
- data/lib/litestack/litecache.yml +1 -1
- data/lib/litestack/litedb.rb +39 -38
- data/lib/litestack/litejob.rb +31 -31
- data/lib/litestack/litejobqueue.rb +107 -106
- data/lib/litestack/litemetric.rb +83 -95
- data/lib/litestack/litemetric.sql.yml +244 -234
- data/lib/litestack/litemetric_collector.sql.yml +38 -41
- data/lib/litestack/litequeue.rb +39 -41
- data/lib/litestack/litequeue.sql.yml +39 -31
- data/lib/litestack/litescheduler.rb +84 -0
- data/lib/litestack/litesearch/index.rb +260 -0
- data/lib/litestack/litesearch/model.rb +179 -0
- data/lib/litestack/litesearch/schema.rb +190 -0
- data/lib/litestack/litesearch/schema_adapters/backed_adapter.rb +143 -0
- data/lib/litestack/litesearch/schema_adapters/basic_adapter.rb +137 -0
- data/lib/litestack/litesearch/schema_adapters/contentless_adapter.rb +14 -0
- data/lib/litestack/litesearch/schema_adapters/standalone_adapter.rb +31 -0
- data/lib/litestack/litesearch/schema_adapters.rb +4 -0
- data/lib/litestack/litesearch.rb +34 -0
- data/lib/litestack/litesupport.rb +85 -186
- data/lib/litestack/railtie.rb +1 -1
- data/lib/litestack/version.rb +2 -2
- data/lib/litestack.rb +7 -4
- data/lib/railties/rails/commands/dbconsole.rb +11 -15
- data/lib/sequel/adapters/litedb.rb +18 -22
- data/lib/sequel/adapters/shared/litedb.rb +168 -168
- data/scripts/build_metrics.rb +91 -0
- data/scripts/test_cable.rb +30 -0
- data/scripts/test_job_retry.rb +33 -0
- data/scripts/test_metrics.rb +60 -0
- data/template.rb +2 -2
- metadata +112 -7
@@ -1,56 +1,53 @@
|
|
1
1
|
schema:
|
2
2
|
1:
|
3
|
-
|
4
3
|
create_events: >
|
5
4
|
CREATE TABLE IF NOT EXISTS local_events(
|
6
5
|
topic TEXT NOT NULL,
|
7
|
-
name TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
|
8
|
-
key TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
|
9
|
-
count INTEGER DEFAULT(0) NOT NULL ON CONFLICT REPLACE,
|
10
|
-
value REAL,
|
11
|
-
minimum REAL,
|
12
|
-
maximum REAL,
|
13
|
-
created_at INTEGER DEFAULT((unixepoch()/300*300)) NOT NULL ON CONFLICT REPLACE,
|
14
|
-
resolution TEXT DEFAULT('minute') NOT NULL,
|
6
|
+
name TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
|
7
|
+
key TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
|
8
|
+
count INTEGER DEFAULT(0) NOT NULL ON CONFLICT REPLACE,
|
9
|
+
value REAL,
|
10
|
+
minimum REAL,
|
11
|
+
maximum REAL,
|
12
|
+
created_at INTEGER DEFAULT((unixepoch()/300*300)) NOT NULL ON CONFLICT REPLACE,
|
13
|
+
resolution TEXT DEFAULT('minute') NOT NULL,
|
15
14
|
PRIMARY KEY(resolution, created_at, topic, name, key)
|
16
|
-
) STRICT;
|
15
|
+
) STRICT;
|
17
16
|
|
18
17
|
stmts:
|
19
|
-
|
20
18
|
capture_event: >
|
21
|
-
INSERT INTO local_events(topic, name, key, created_at, count, value, minimum, maximum)
|
22
|
-
VALUES
|
23
|
-
(?1, ?2, ?3, ?4, ?5, ?6, ?6, ?6),
|
19
|
+
INSERT INTO local_events(topic, name, key, created_at, count, value, minimum, maximum)
|
20
|
+
VALUES
|
21
|
+
(?1, ?2, ?3, ?4, ?5, ?6, ?6, ?6),
|
24
22
|
(?1, ?2, '___', ?4, ?5, ?6, ?6, ?6),
|
25
|
-
(?1, '___', '___', ?4, ?5, ?6, ?6, ?6)
|
26
|
-
ON CONFLICT DO UPDATE
|
27
|
-
SET
|
28
|
-
count = count + EXCLUDED.count,
|
29
|
-
value = value + EXCLUDED.value,
|
30
|
-
minimum = min(minimum, EXCLUDED.minimum),
|
31
|
-
maximum = max(maximum, EXCLUDED.maximum)
|
32
|
-
|
33
|
-
migrate_events:
|
34
|
-
INSERT INTO m.events(topic, name, key, created_at, count, value, minimum, maximum)
|
35
|
-
SELECT topic, name, key, created_at, count, value, minimum, maximum
|
36
|
-
FROM local_events
|
37
|
-
ORDER BY resolution, created_at ASC, topic, name, key
|
23
|
+
(?1, '___', '___', ?4, ?5, ?6, ?6, ?6)
|
24
|
+
ON CONFLICT DO UPDATE
|
25
|
+
SET
|
26
|
+
count = count + EXCLUDED.count,
|
27
|
+
value = value + EXCLUDED.value,
|
28
|
+
minimum = min(minimum, EXCLUDED.minimum),
|
29
|
+
maximum = max(maximum, EXCLUDED.maximum);
|
30
|
+
|
31
|
+
migrate_events: >
|
32
|
+
INSERT INTO m.events(topic, name, key, created_at, count, value, minimum, maximum)
|
33
|
+
SELECT topic, name, key, created_at, count, value, minimum, maximum
|
34
|
+
FROM local_events
|
35
|
+
ORDER BY resolution, created_at ASC, topic, name, key
|
38
36
|
LIMIT ?
|
39
|
-
ON CONFLICT DO UPDATE
|
40
|
-
SET
|
41
|
-
count = count + EXCLUDED.count,
|
42
|
-
value = value + EXCLUDED.value,
|
43
|
-
minimum = min(minimum, EXCLUDED.minimum),
|
44
|
-
maximum = max(maximum, EXCLUDED.maximum)
|
37
|
+
ON CONFLICT DO UPDATE
|
38
|
+
SET
|
39
|
+
count = count + EXCLUDED.count,
|
40
|
+
value = value + EXCLUDED.value,
|
41
|
+
minimum = min(minimum, EXCLUDED.minimum),
|
42
|
+
maximum = max(maximum, EXCLUDED.maximum);
|
45
43
|
|
46
|
-
delete_migrated_events:
|
44
|
+
delete_migrated_events: >
|
47
45
|
DELETE FROM local_events WHERE rowid IN (
|
48
|
-
SELECT rowid
|
49
|
-
FROM local_events
|
50
|
-
ORDER BY resolution, created_at ASC, topic, name, key
|
46
|
+
SELECT rowid
|
47
|
+
FROM local_events
|
48
|
+
ORDER BY resolution, created_at ASC, topic, name, key
|
51
49
|
LIMIT ?
|
52
|
-
)
|
53
|
-
|
54
|
-
event_count:
|
50
|
+
);
|
51
|
+
|
52
|
+
event_count: >
|
55
53
|
SELECT count(*) FROM local_events;
|
56
|
-
|
data/lib/litestack/litequeue.rb
CHANGED
@@ -1,27 +1,26 @@
|
|
1
1
|
# frozen_stringe_literal: true
|
2
2
|
|
3
3
|
# all components should require the support module
|
4
|
-
require_relative
|
4
|
+
require_relative "litesupport"
|
5
5
|
|
6
|
-
#require 'securerandom'
|
6
|
+
# require 'securerandom'
|
7
7
|
|
8
8
|
##
|
9
|
-
#Litequeue is a simple queueing system for Ruby applications that allows you to push and pop values from a queue. It provides a straightforward API for creating and managing named queues, and for adding and removing values from those queues. Additionally, it offers options for scheduling pops at a certain time in the future, which can be useful for delaying processing until a later time.
|
9
|
+
# Litequeue is a simple queueing system for Ruby applications that allows you to push and pop values from a queue. It provides a straightforward API for creating and managing named queues, and for adding and removing values from those queues. Additionally, it offers options for scheduling pops at a certain time in the future, which can be useful for delaying processing until a later time.
|
10
10
|
#
|
11
|
-
#Litequeue is built on top of SQLite, which makes it very fast and efficient, even when handling large volumes of data. This lightweight and easy-to-use queueing system serves as a good foundation for building more advanced job processing frameworks that require basic queuing capabilities.
|
11
|
+
# Litequeue is built on top of SQLite, which makes it very fast and efficient, even when handling large volumes of data. This lightweight and easy-to-use queueing system serves as a good foundation for building more advanced job processing frameworks that require basic queuing capabilities.
|
12
12
|
#
|
13
|
-
|
14
|
-
class Litequeue
|
15
13
|
|
14
|
+
class Litequeue
|
16
15
|
# the default options for the queue
|
17
|
-
# can be overriden by passing new options in a hash
|
16
|
+
# can be overriden by passing new options in a hash
|
18
17
|
# to Litequeue.new
|
19
18
|
# path: "./queue.db"
|
20
19
|
# mmap_size: 128 * 1024 * 1024 -> 128MB to be held in memory
|
21
20
|
# sync: 1 -> sync only when checkpointing
|
22
|
-
|
21
|
+
|
23
22
|
include Litesupport::Liteconnection
|
24
|
-
|
23
|
+
|
25
24
|
DEFAULT_OPTIONS = {
|
26
25
|
path: Litesupport.root.join("queue.sqlite3"),
|
27
26
|
mmap_size: 32 * 1024,
|
@@ -35,70 +34,70 @@ class Litequeue
|
|
35
34
|
# queue.pop # => nil
|
36
35
|
# sleep 2
|
37
36
|
# queue.pop # => "somevalue"
|
38
|
-
|
37
|
+
|
39
38
|
def initialize(options = {})
|
40
39
|
init(options)
|
41
40
|
end
|
42
|
-
|
41
|
+
|
43
42
|
# push an item to the queue, optionally specifying the queue name (defaults to default) and after how many seconds it should be ready to pop (defaults to zero)
|
44
43
|
# a unique job id is returned from this method, can be used later to delete it before it fires. You can push string, integer, float, true, false or nil values
|
45
44
|
#
|
46
|
-
def push(value, delay=0, queue=
|
45
|
+
def push(value, delay = 0, queue = "default")
|
47
46
|
# @todo - check if queue is busy, back off if it is
|
48
47
|
# also bring back the synchronize block, to prevent
|
49
48
|
# a race condition if a thread hits the busy handler
|
50
49
|
# before the current thread proceeds after a backoff
|
51
|
-
#id = SecureRandom.uuid # this is somehow expensive, can we improve?
|
50
|
+
# id = SecureRandom.uuid # this is somehow expensive, can we improve?
|
52
51
|
run_stmt(:push, queue, delay, value)[0]
|
53
52
|
end
|
54
|
-
|
55
|
-
def repush(id, value, delay=0, queue=
|
53
|
+
|
54
|
+
def repush(id, value, delay = 0, queue = "default")
|
56
55
|
run_stmt(:repush, id, queue, delay, value)[0]
|
57
56
|
end
|
58
|
-
|
59
|
-
alias_method
|
57
|
+
|
58
|
+
alias_method :<<, :push
|
60
59
|
alias_method :"<<<", :repush
|
61
|
-
|
60
|
+
|
62
61
|
# pop an item from the queue, optionally with a specific queue name (default queue name is 'default')
|
63
|
-
def pop(queue=
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
62
|
+
def pop(queue = "default", limit = 1)
|
63
|
+
res = run_stmt(:pop, queue, limit)
|
64
|
+
return res[0] if res.length == 1
|
65
|
+
return nil if res.empty?
|
66
|
+
res
|
68
67
|
end
|
69
|
-
|
68
|
+
|
70
69
|
# delete an item from the queue
|
71
70
|
# queue = Litequeue.new
|
72
71
|
# id = queue.push("somevalue")
|
73
72
|
# queue.delete(id) # => "somevalue"
|
74
73
|
# queue.pop # => nil
|
75
74
|
def delete(id)
|
76
|
-
|
75
|
+
run_stmt(:delete, id)[0]
|
77
76
|
end
|
78
|
-
|
77
|
+
|
79
78
|
# deletes all the entries in all queues, or if a queue name is given, deletes all entries in that specific queue
|
80
|
-
def clear(queue=nil)
|
79
|
+
def clear(queue = nil)
|
81
80
|
run_sql("DELETE FROM queue WHERE iif(?1 IS NOT NULL, name = ?1, TRUE)", queue)
|
82
81
|
end
|
83
82
|
|
84
83
|
# returns a count of entries in all queues, or if a queue name is given, reutrns the count of entries in that queue
|
85
|
-
def count(queue=nil)
|
84
|
+
def count(queue = nil)
|
86
85
|
run_sql("SELECT count(*) FROM queue WHERE iif(?1 IS NOT NULL, name = ?1, TRUE)", queue)[0][0]
|
87
86
|
end
|
88
|
-
|
87
|
+
|
89
88
|
# return the size of the queue file on disk
|
90
|
-
#def size
|
91
|
-
# run_sql("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count")[0][0]
|
92
|
-
#end
|
93
|
-
|
89
|
+
# def size
|
90
|
+
# run_sql("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count")[0][0]
|
91
|
+
# end
|
92
|
+
|
94
93
|
def queues_info
|
95
94
|
run_stmt(:info)
|
96
95
|
end
|
97
|
-
|
96
|
+
|
98
97
|
def snapshot
|
99
98
|
queues = {}
|
100
99
|
queues_info.each do |qc|
|
101
|
-
#queues[qc[0]] = {count: qc[1], time_in_queue: {avg: qc[2], min: qc[3], max: qc[4]}}
|
100
|
+
# queues[qc[0]] = {count: qc[1], time_in_queue: {avg: qc[2], min: qc[3], max: qc[4]}}
|
102
101
|
queues[qc[0]] = qc[1]
|
103
102
|
end
|
104
103
|
{
|
@@ -109,15 +108,15 @@ class Litequeue
|
|
109
108
|
size: size,
|
110
109
|
jobs: count
|
111
110
|
},
|
112
|
-
queues: queues
|
111
|
+
queues: queues
|
113
112
|
}
|
114
113
|
end
|
115
114
|
|
116
|
-
private
|
117
|
-
|
115
|
+
private
|
116
|
+
|
118
117
|
def create_connection
|
119
118
|
super("#{__dir__}/litequeue.sql.yml") do |conn|
|
120
|
-
conn.wal_autocheckpoint = 10000
|
119
|
+
conn.wal_autocheckpoint = 10000
|
121
120
|
# check if there is an old database and convert entries to the new format
|
122
121
|
if conn.get_first_value("select count(*) from sqlite_master where name = '_ul_queue_'") == 1
|
123
122
|
conn.transaction(:immediate) do
|
@@ -127,5 +126,4 @@ class Litequeue
|
|
127
126
|
end
|
128
127
|
end
|
129
128
|
end
|
130
|
-
|
131
|
-
end
|
129
|
+
end
|
@@ -1,45 +1,53 @@
|
|
1
1
|
schema:
|
2
|
-
1:
|
3
|
-
create_table_queue: >
|
2
|
+
1:
|
3
|
+
create_table_queue: >
|
4
4
|
CREATE TABLE IF NOT EXISTS queue(
|
5
|
-
id TEXT PRIMARY KEY DEFAULT(hex(randomblob(32))) NOT NULL ON CONFLICT REPLACE,
|
6
|
-
name TEXT DEFAULT('default') NOT NULL ON CONFLICT REPLACE,
|
5
|
+
id TEXT PRIMARY KEY DEFAULT(hex(randomblob(32))) NOT NULL ON CONFLICT REPLACE,
|
6
|
+
name TEXT DEFAULT('default') NOT NULL ON CONFLICT REPLACE,
|
7
7
|
fire_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE,
|
8
8
|
value TEXT,
|
9
9
|
created_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE
|
10
|
-
) WITHOUT ROWID
|
11
|
-
|
10
|
+
) WITHOUT ROWID;
|
11
|
+
|
12
12
|
create_index_queue_by_name: >
|
13
|
-
CREATE INDEX IF NOT EXISTS idx_queue_by_name ON queue(name, fire_at ASC)
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_queue_by_name ON queue(name, fire_at ASC);
|
14
14
|
|
15
15
|
stmts:
|
16
16
|
|
17
|
-
push:
|
18
|
-
|
19
|
-
|
17
|
+
push: >
|
18
|
+
INSERT INTO queue(id, name, fire_at, value)
|
19
|
+
VALUES (hex(randomblob(32)), $1, (unixepoch() + $2), $3)
|
20
|
+
RETURNING id, name;
|
21
|
+
|
22
|
+
repush: >
|
23
|
+
INSERT INTO queue(id, name, fire_at, value)
|
24
|
+
VALUES (?, ?, (unixepoch() + ?), ?)
|
25
|
+
RETURNING name;
|
20
26
|
|
21
27
|
pop: >
|
22
|
-
DELETE FROM queue
|
23
|
-
WHERE (name, fire_at, id)
|
28
|
+
DELETE FROM queue
|
29
|
+
WHERE (name, fire_at, id)
|
24
30
|
IN (
|
25
|
-
|
26
|
-
|
27
|
-
AND fire_at <= (unixepoch())
|
28
|
-
|
29
|
-
|
30
|
-
)
|
31
|
-
RETURNING id, value
|
32
|
-
|
33
|
-
delete:
|
34
|
-
|
31
|
+
SELECT name, fire_at, id FROM queue
|
32
|
+
WHERE name = ifnull($1, 'default')
|
33
|
+
AND fire_at <= (unixepoch())
|
34
|
+
ORDER BY fire_at ASC
|
35
|
+
LIMIT ifnull($2, 1)
|
36
|
+
)
|
37
|
+
RETURNING id, value;
|
38
|
+
|
39
|
+
delete: >
|
40
|
+
DELETE FROM queue
|
41
|
+
WHERE id = $1
|
42
|
+
RETURNING value;
|
43
|
+
|
35
44
|
info: >
|
36
|
-
SELECT
|
37
|
-
name,
|
38
|
-
count(*) AS count,
|
39
|
-
avg(unixepoch() - created_at) AS avg,
|
40
|
-
min(unixepoch() - created_at) AS min,
|
45
|
+
SELECT
|
46
|
+
name,
|
47
|
+
count(*) AS count,
|
48
|
+
avg(unixepoch() - created_at) AS avg,
|
49
|
+
min(unixepoch() - created_at) AS min,
|
41
50
|
max(unixepoch() - created_at) AS max
|
42
|
-
FROM queue
|
43
|
-
GROUP BY name
|
44
|
-
ORDER BY count DESC
|
45
|
-
|
51
|
+
FROM queue
|
52
|
+
GROUP BY name
|
53
|
+
ORDER BY count DESC;
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_stringe_literal: true
|
2
|
+
|
3
|
+
module Litescheduler
|
4
|
+
# cache the scheduler we are running in
|
5
|
+
# it is an error to change the scheduler for a process
|
6
|
+
# or for a child forked from that process
|
7
|
+
def self.backend
|
8
|
+
@backend ||= if Fiber.scheduler
|
9
|
+
:fiber
|
10
|
+
elsif defined? Polyphony
|
11
|
+
:polyphony
|
12
|
+
elsif defined? Iodine
|
13
|
+
:iodine
|
14
|
+
else
|
15
|
+
:threaded
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# spawn a new execution context
|
20
|
+
def self.spawn(&block)
|
21
|
+
if backend == :fiber
|
22
|
+
Fiber.schedule(&block)
|
23
|
+
elsif backend == :polyphony
|
24
|
+
spin(&block)
|
25
|
+
elsif (backend == :threaded) || (backend == :iodine)
|
26
|
+
Thread.new(&block)
|
27
|
+
end
|
28
|
+
# we should never reach here
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.storage
|
32
|
+
if backend == :fiber || backend == :poylphony
|
33
|
+
Fiber.current.storage
|
34
|
+
else
|
35
|
+
Thread.current
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.current
|
40
|
+
if backend == :fiber || backend == :poylphony
|
41
|
+
Fiber.current
|
42
|
+
else
|
43
|
+
Thread.current
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# switch the execution context to allow others to run
|
48
|
+
def self.switch
|
49
|
+
if backend == :fiber
|
50
|
+
Fiber.scheduler.yield
|
51
|
+
true
|
52
|
+
elsif backend == :polyphony
|
53
|
+
Fiber.current.schedule
|
54
|
+
Thread.current.switch_fiber
|
55
|
+
true
|
56
|
+
else
|
57
|
+
# Thread.pass
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# bold assumption, we will only synchronize threaded code!
|
63
|
+
# If some code explicitly wants to synchronize a fiber
|
64
|
+
# they must send (true) as a parameter to this method
|
65
|
+
# else it is a no-op for fibers
|
66
|
+
def self.synchronize(fiber_sync = false, &block)
|
67
|
+
if (backend == :fiber) || (backend == :polyphony)
|
68
|
+
yield # do nothing, just run the block as is
|
69
|
+
else
|
70
|
+
mutex.synchronize(&block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.max_contexts
|
75
|
+
return 50 if backend == :fiber || backend == :polyphony
|
76
|
+
5
|
77
|
+
end
|
78
|
+
|
79
|
+
# mutex initialization
|
80
|
+
def self.mutex
|
81
|
+
# a single mutex per process (is that ok?)
|
82
|
+
@@mutex ||= Mutex.new
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require "oj"
|
2
|
+
require_relative "./schema"
|
3
|
+
|
4
|
+
class Litesearch::Index
|
5
|
+
DEFAULT_SEARCH_OPTIONS = {limit: 25, offset: 0}
|
6
|
+
|
7
|
+
def initialize(db, name)
|
8
|
+
@db = db # this index instance will always belong to this db instance
|
9
|
+
@stmts = {}
|
10
|
+
name = name.to_s.downcase.to_sym
|
11
|
+
# if in the db then put in cache and return if no schema is given
|
12
|
+
# if a schema is given then compare the new and the existing schema
|
13
|
+
# if they are the same put in cache and return
|
14
|
+
# if they differ only in weights then set the new weights, update the schema, put in cache and return
|
15
|
+
# if they differ in fields (added/removed/renamed) then update the structure, then rebuild if auto-rebuild is on
|
16
|
+
# if they differ in tokenizer then rebuild if auto-rebuild is on (error otherwise)
|
17
|
+
# if they differ in both then update the structure and rebuild if auto-rebuild is on (error otherwise)
|
18
|
+
load_index(name) if exists?(name)
|
19
|
+
|
20
|
+
if block_given?
|
21
|
+
schema = Litesearch::Schema.new
|
22
|
+
schema.schema[:name] = name
|
23
|
+
yield schema
|
24
|
+
schema.post_init
|
25
|
+
# now that we have a schema object we need to check if we need to create or modify and existing index
|
26
|
+
if @db.transaction_active?
|
27
|
+
if exists?(name)
|
28
|
+
load_index(name)
|
29
|
+
do_modify(schema)
|
30
|
+
else
|
31
|
+
do_create(schema)
|
32
|
+
end
|
33
|
+
prepare_statements
|
34
|
+
else
|
35
|
+
@db.transaction(:immediate) do
|
36
|
+
if exists?(name)
|
37
|
+
load_index(name)
|
38
|
+
do_modify(schema)
|
39
|
+
else
|
40
|
+
do_create(schema)
|
41
|
+
end
|
42
|
+
prepare_statements
|
43
|
+
end
|
44
|
+
end
|
45
|
+
elsif exists?(name)
|
46
|
+
load_index(name)
|
47
|
+
prepare_statements
|
48
|
+
# an index already exists, load it from the database and return the index instance to the caller
|
49
|
+
else
|
50
|
+
raise "index does not exist and no schema was supplied"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_index(name)
|
55
|
+
# we cannot use get_config_value here since the schema object is not created yet, should we allow something here?
|
56
|
+
@schema = begin
|
57
|
+
Litesearch::Schema.new(Oj.load(@db.get_first_value("SELECT v from #{name}_config where k = ?", :litesearch_schema.to_s)))
|
58
|
+
rescue
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
raise "index configuration not found, either corrupted or not a litesearch index!" if @schema.nil?
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def modify
|
66
|
+
schema = Litesearch::Schema.new
|
67
|
+
yield schema
|
68
|
+
schema.schema[:name] = @schema.schema[:name]
|
69
|
+
do_modify(schema)
|
70
|
+
end
|
71
|
+
|
72
|
+
def rebuild!
|
73
|
+
if @db.transaction_active?
|
74
|
+
do_rebuild
|
75
|
+
else
|
76
|
+
@db.transaction(:immediate) { do_rebuild }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def add(document)
|
81
|
+
@stmts[:insert].execute!(document)
|
82
|
+
@db.last_insert_row_id
|
83
|
+
end
|
84
|
+
|
85
|
+
def remove(id)
|
86
|
+
@stmts[:delete].execute!(id)
|
87
|
+
end
|
88
|
+
|
89
|
+
def count(term = nil)
|
90
|
+
if term
|
91
|
+
@stmts[:count].execute!(term)[0][0]
|
92
|
+
else
|
93
|
+
@stmts[:count_all].execute![0][0]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# search options include
|
98
|
+
# limit: how many records to return
|
99
|
+
# offset: start from which record
|
100
|
+
def search(term, options = {})
|
101
|
+
options = DEFAULT_SEARCH_OPTIONS.merge(options)
|
102
|
+
rs = @stmts[:search].execute(term, options[:limit], options[:offset])
|
103
|
+
generate_results(rs)
|
104
|
+
end
|
105
|
+
|
106
|
+
def similar(id, limit=10)
|
107
|
+
# pp term = @db.execute(@schema.sql_for(:similarity_query), id)
|
108
|
+
if @schema.schema[:tokenizer] == :trigram
|
109
|
+
# just use the normal similarity approach for now
|
110
|
+
# need to recondisder that for trigram indexes later
|
111
|
+
rs = @stmts[:similar].execute(id, limit)
|
112
|
+
else
|
113
|
+
rs = @stmts[:similar].execute(id, limit)
|
114
|
+
end
|
115
|
+
generate_results(rs)
|
116
|
+
end
|
117
|
+
|
118
|
+
def clear!
|
119
|
+
@stmts[:delete_all].execute!(id)
|
120
|
+
end
|
121
|
+
|
122
|
+
def drop!
|
123
|
+
if @schema.get(:type) == :backed
|
124
|
+
@db.execute_batch(@schema.sql_for(:drop_primary_triggers))
|
125
|
+
if @schema.sql_for(:create_secondary_triggers)
|
126
|
+
@db.execute_batch(@schema.sql_for(:drop_secondary_triggers))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
@db.execute(@schema.sql_for(:drop))
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def generate_results(rs)
|
135
|
+
result = []
|
136
|
+
if @db.results_as_hash
|
137
|
+
rs.each_hash do |hash|
|
138
|
+
result << hash
|
139
|
+
end
|
140
|
+
else
|
141
|
+
result = rs.to_a
|
142
|
+
end
|
143
|
+
result
|
144
|
+
end
|
145
|
+
|
146
|
+
def exists?(name)
|
147
|
+
@db.get_first_value("SELECT count(*) FROM SQLITE_MASTER WHERE name = ? AND type = 'table' AND (sql like '%fts5%' OR sql like '%FTS5%')", name.to_s) == 1
|
148
|
+
end
|
149
|
+
|
150
|
+
def prepare_statements
|
151
|
+
stmt_names = [:insert, :delete, :delete_all, :drop, :count, :count_all, :search, :similar]
|
152
|
+
stmt_names.each do |stmt_name|
|
153
|
+
@stmts[stmt_name] = @db.prepare(@schema.sql_for(stmt_name))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def do_create(schema)
|
158
|
+
@schema = schema
|
159
|
+
@schema.clean
|
160
|
+
# create index
|
161
|
+
@db.execute(schema.sql_for(:create_index, true))
|
162
|
+
@db.execute_batch(schema.sql_for(:create_vocab_tables))
|
163
|
+
# adjust ranking function
|
164
|
+
@db.execute(schema.sql_for(:ranks, true))
|
165
|
+
# create triggers (if any)
|
166
|
+
if @schema.get(:type) == :backed
|
167
|
+
@db.execute_batch(@schema.sql_for(:create_primary_triggers))
|
168
|
+
if (secondary_triggers_sql = @schema.sql_for(:create_secondary_triggers))
|
169
|
+
@db.execute_batch(secondary_triggers_sql)
|
170
|
+
end
|
171
|
+
@db.execute(@schema.sql_for(:rebuild)) if @schema.get(:rebuild_on_create)
|
172
|
+
end
|
173
|
+
set_config_value(:litesearch_schema, @schema.schema)
|
174
|
+
end
|
175
|
+
|
176
|
+
def do_modify(new_schema)
|
177
|
+
changes = @schema.compare(new_schema)
|
178
|
+
# ensure the new schema maintains feild order
|
179
|
+
new_schema.order_fields(@schema)
|
180
|
+
# with the changes object decide what needs to be done to the schema
|
181
|
+
requires_schema_change = false
|
182
|
+
requires_trigger_change = false
|
183
|
+
requires_rebuild = false
|
184
|
+
if changes[:fields] || changes[:table] || changes[:tokenizer] || changes[:filter_column] || changes[:removed_fields_count] > 0 # any change here will require a schema change
|
185
|
+
requires_schema_change = true
|
186
|
+
# only a change in tokenizer
|
187
|
+
requires_rebuild = changes[:tokenizer] || new_schema.get(:rebuild_on_modify)
|
188
|
+
requires_trigger_change = (changes[:table] || changes[:fields] || changes[:filter_column]) && @schema.get(:type) == :backed
|
189
|
+
end
|
190
|
+
if requires_schema_change
|
191
|
+
# 1. enable schema editing
|
192
|
+
@db.execute("PRAGMA WRITABLE_SCHEMA = TRUE")
|
193
|
+
# 2. update the index sql
|
194
|
+
@db.execute(new_schema.sql_for(:update_index), new_schema.sql_for(:create_index))
|
195
|
+
# 3. update the content table sql (if it exists)
|
196
|
+
@db.execute(new_schema.sql_for(:update_content_table), new_schema.sql_for(:create_content_table, new_schema.schema[:fields].count))
|
197
|
+
# adjust shadow tables
|
198
|
+
@db.execute(new_schema.sql_for(:expand_data), changes[:extra_fields_count])
|
199
|
+
@db.execute(new_schema.sql_for(:expand_docsize), changes[:extra_fields_count])
|
200
|
+
@db.execute("PRAGMA WRITABLE_SCHEMA = RESET")
|
201
|
+
# need to reprepare statements
|
202
|
+
end
|
203
|
+
if requires_trigger_change
|
204
|
+
@db.execute_batch(new_schema.sql_for(:drop_primary_triggers))
|
205
|
+
@db.execute_batch(new_schema.sql_for(:create_primary_triggers))
|
206
|
+
if (secondary_triggers_sql = new_schema.sql_for(:create_secondary_triggers))
|
207
|
+
@db.execute_batch(new_schema.sql_for(:drop_secondary_triggers))
|
208
|
+
@db.execute_batch(secondary_triggers_sql)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
if changes[:fields] || changes[:table] || changes[:tokenizer] || changes[:weights] || changes[:filter_column]
|
212
|
+
@schema = new_schema
|
213
|
+
set_config_value(:litesearch_schema, @schema.schema)
|
214
|
+
prepare_statements
|
215
|
+
# save_schema
|
216
|
+
end
|
217
|
+
# update the weights if they changed
|
218
|
+
@db.execute(@schema.sql_for(:ranks, true)) if changes[:weights]
|
219
|
+
@db.execute_batch(@schema.sql_for(:create_vocab_tables))
|
220
|
+
do_rebuild if requires_rebuild
|
221
|
+
end
|
222
|
+
|
223
|
+
def do_rebuild
|
224
|
+
# remove any zero weight columns
|
225
|
+
if @schema.get(:type) == :backed
|
226
|
+
@db.execute_batch(@schema.sql_for(:drop_primary_triggers))
|
227
|
+
if (secondary_triggers_sql = @schema.sql_for(:create_secondary_triggers))
|
228
|
+
@db.execute_batch(@schema.sql_for(:drop_secondary_triggers))
|
229
|
+
end
|
230
|
+
@db.execute(@schema.sql_for(:drop))
|
231
|
+
@db.execute(@schema.sql_for(:create_index, true))
|
232
|
+
@db.execute_batch(@schema.sql_for(:create_primary_triggers))
|
233
|
+
@db.execute_batch(secondary_triggers_sql) if secondary_triggers_sql
|
234
|
+
@db.execute(@schema.sql_for(:rebuild))
|
235
|
+
elsif @schema.get(:type) == :standalone
|
236
|
+
removables = []
|
237
|
+
@schema.get(:fields).each_with_index { |f, i| removables << [f[0], i] if f[1][:weight] == 0 }
|
238
|
+
removables.each do |col|
|
239
|
+
@db.execute(@schema.sql_for(:drop_content_col, col[1]))
|
240
|
+
@schema.get(:fields).delete(col[0])
|
241
|
+
end
|
242
|
+
@db.execute("PRAGMA WRITABLE_SCHEMA = TRUE")
|
243
|
+
@db.execute(@schema.sql_for(:update_index), @schema.sql_for(:create_index, true))
|
244
|
+
@db.execute(@schema.sql_for(:update_content_table), @schema.sql_for(:create_content_table, @schema.schema[:fields].count))
|
245
|
+
@db.execute("PRAGMA WRITABLE_SCHEMA = RESET")
|
246
|
+
@db.execute(@schema.sql_for(:rebuild))
|
247
|
+
end
|
248
|
+
@db.execute_batch(@schema.sql_for(:create_vocab_tables))
|
249
|
+
set_config_value(:litesearch_schema, @schema.schema)
|
250
|
+
@db.execute(@schema.sql_for(:ranks, true))
|
251
|
+
end
|
252
|
+
|
253
|
+
def get_config_value(key)
|
254
|
+
Oj.load(@db.get_first_value(@schema.sql_for(:get_config_value), key.to_s)) # rescue nil
|
255
|
+
end
|
256
|
+
|
257
|
+
def set_config_value(key, value)
|
258
|
+
@db.execute(@schema.sql_for(:set_config_value), key.to_s, Oj.dump(value))
|
259
|
+
end
|
260
|
+
end
|