arcadedb 0.5.2 → 0.5.4
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/.gitignore +3 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +64 -58
- data/RAG_SCHEMA.md +239 -0
- data/README.md +130 -310
- data/examples/rag_example.rb +72 -0
- data/examples/rag_usecase_demo.rb +345 -0
- data/lib/arcade/api/primitives.rb +43 -0
- data/lib/arcade/base.rb +1 -1
- data/lib/arcade/database.rb +10 -2
- data/lib/arcade/rag_schema.rb +131 -0
- data/lib/arcade/support/model.rb +12 -1
- data/lib/arcade/timeseries.rb +211 -0
- data/lib/arcade/version.rb +1 -1
- data/lib/arcade.rb +2 -0
- data/lib/model/timeseries.rb +6 -0
- data/lib/models.rb +1 -0
- metadata +9 -6
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
module Arcade
|
|
2
|
+
# Abstraction of ArcadeDB's native TimeSeries types.
|
|
3
|
+
#
|
|
4
|
+
# A TimeSeries type is not a vertex/document: it has no rid and carries no edges.
|
|
5
|
+
# Membership is expressed through tags, values through typed fields.
|
|
6
|
+
#
|
|
7
|
+
# class Ohlcv < Arcade::Timeseries
|
|
8
|
+
# timestamp :ts, precision: :second
|
|
9
|
+
# tags symbol: String, currency: String
|
|
10
|
+
# fields open: Float, high: Float, low: Float, close: Float, volume: Integer
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Ohlcv.create_type
|
|
14
|
+
# Ohlcv.write symbol: 'SAPX:GER', ts: Date.today, open: 1, high: 2, low: 0.5, close: 1.5, volume: 100
|
|
15
|
+
# Ohlcv.range from: '2026-01-01', tags: { symbol: 'SAPX:GER' }
|
|
16
|
+
# Ohlcv.latest tags: { symbol: 'SAPX:GER' }
|
|
17
|
+
# Ohlcv.bucket '7d', from: '2026-01-01', aggregate: { close: :last, volume: :sum }
|
|
18
|
+
#
|
|
19
|
+
# Conventions:
|
|
20
|
+
# * Writes go through the InfluxDB Line Protocol endpoint (fastest remote path);
|
|
21
|
+
# reads go through SQL (range pushdown, ts.* functions).
|
|
22
|
+
# * Timestamps are accepted as Time, DateTime, Date, ISO-String or epoch Numeric
|
|
23
|
+
# and normalised to the declared precision — never pass raw numbers of a
|
|
24
|
+
# different unit than declared, the engine will not complain but read wrong.
|
|
25
|
+
# * The server's DELETE on timeseries types reports a count but leaves the rows
|
|
26
|
+
# (ArcadeDB 26.10.1) — there is deliberately no window-replace helper here.
|
|
27
|
+
# Re-writing a timestamp appends a second sample; deduplicate on read if needed.
|
|
28
|
+
class Timeseries < Base
|
|
29
|
+
|
|
30
|
+
TYPES = { String => 'STRING', Float => 'DOUBLE', Integer => 'LONG',
|
|
31
|
+
:bool => 'BOOLEAN', :datetime => 'DATETIME' }.freeze
|
|
32
|
+
|
|
33
|
+
# SQL keyword and nanosecond-multiplier per supported precision
|
|
34
|
+
PRECISIONS = { second: ['SECOND', 1],
|
|
35
|
+
millisecond: ['MILLISECOND', 1_000],
|
|
36
|
+
microsecond: ['MICROSECOND', 1_000_000],
|
|
37
|
+
nanosecond: ['NANOSECOND', 1_000_000_000] }.freeze
|
|
38
|
+
|
|
39
|
+
LP_PRECISION = { second: 's', millisecond: 'ms', microsecond: 'us', nanosecond: 'ns' }.freeze
|
|
40
|
+
|
|
41
|
+
# ts.<fn>(field, ts) where the engine needs the timestamp, plain SQL otherwise
|
|
42
|
+
AGGREGATES = { last: ->(f, ts){ "ts.last(#{ f }, #{ ts })" },
|
|
43
|
+
first: ->(f, ts){ "ts.first(#{ f }, #{ ts })" },
|
|
44
|
+
avg: ->(f, _){ "avg(#{ f })" },
|
|
45
|
+
sum: ->(f, _){ "sum(#{ f })" },
|
|
46
|
+
min: ->(f, _){ "min(#{ f })" },
|
|
47
|
+
max: ->(f, _){ "max(#{ f })" },
|
|
48
|
+
count: ->(f, _){ "count(#{ f })" } }.freeze
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
|
|
52
|
+
# ---- schema declaration -----------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def timestamp name = nil, precision: nil, shards: nil, retention: nil
|
|
55
|
+
if name.nil? && precision.nil? # getter use: ts_name via timestamp()
|
|
56
|
+
@ts_name ||= :ts
|
|
57
|
+
@ts_precision ||= :second
|
|
58
|
+
return @ts_name
|
|
59
|
+
end
|
|
60
|
+
@ts_name, @ts_precision = name, (precision || :second)
|
|
61
|
+
@ts_shards, @ts_retention = shards, retention
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tags spec = nil
|
|
65
|
+
@ts_tags = spec if spec
|
|
66
|
+
@ts_tags ||= {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def fields spec = nil
|
|
70
|
+
@ts_fields = spec if spec
|
|
71
|
+
@ts_fields ||= {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ts_name = timestamp
|
|
75
|
+
def ts_precision = @ts_precision ||= :second
|
|
76
|
+
def type_name = database_name
|
|
77
|
+
|
|
78
|
+
# ---- type lifecycle ---------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def create_type
|
|
81
|
+
return true if type_exists?
|
|
82
|
+
tag_cols = tags.map { |k, t| "#{ k } #{ sql_type t }" }.join(', ')
|
|
83
|
+
field_cols = fields.map { |k, t| "#{ k } #{ sql_type t }" }.join(', ')
|
|
84
|
+
cmd = "CREATE TIMESERIES TYPE #{ type_name } " \
|
|
85
|
+
"TIMESTAMP #{ ts_name } PRECISION #{ PRECISIONS[ts_precision].first } " \
|
|
86
|
+
"TAGS (#{ tag_cols }) FIELDS (#{ field_cols })"
|
|
87
|
+
cmd += " SHARDS #{ @ts_shards }" if @ts_shards
|
|
88
|
+
cmd += " RETENTION #{ @ts_retention }" if @ts_retention
|
|
89
|
+
result = db.transmit { cmd }
|
|
90
|
+
# the engine registers the type asynchronously — wait until readable
|
|
91
|
+
# (writes immediately after create otherwise fail with unknownTypes)
|
|
92
|
+
10.times { break if type_exists?; sleep 0.2 }
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# NB: schema:types does NOT list timeseries types on 26.10 —
|
|
97
|
+
# probe the ts/query endpoint instead (400 unknownTypes = absent).
|
|
98
|
+
def type_exists?
|
|
99
|
+
Api.post_json "ts/#{ db.database }/query", { type: type_name.to_s }
|
|
100
|
+
true
|
|
101
|
+
rescue Arcade::QueryError
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def drop_type
|
|
106
|
+
db.transmit { "DROP TIMESERIES TYPE #{ type_name }" }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ---- writing ----------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
# one sample: Ohlcv.write symbol: 'SAP', ts: Date.today, close: 1.5
|
|
112
|
+
def write **sample
|
|
113
|
+
write_many [sample]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# batch: array of hashes, one per sample (Line Protocol, precision param matches class)
|
|
117
|
+
def write_many samples
|
|
118
|
+
return 0 if samples.empty?
|
|
119
|
+
lines = samples.map { |s| line_for s }
|
|
120
|
+
Api.post_text "ts/#{ db.database }/write?precision=#{ LP_PRECISION[ts_precision] }", lines.join("\n")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# ---- reading ----------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
# raw samples within [from, to] (both optional), filtered by tag equality
|
|
126
|
+
def range from: nil, to: nil, tags: {}, columns: nil, limit: nil
|
|
127
|
+
projections = columns ? ([ts_name] + Array(columns)).join(', ') : '*'
|
|
128
|
+
conditions = []
|
|
129
|
+
conditions << "#{ ts_name } >= #{ sql_time from }" if from
|
|
130
|
+
conditions << "#{ ts_name } <= #{ sql_time to }" if to
|
|
131
|
+
tags.each { |k, v| conditions << "#{ k } = '#{ v }'" }
|
|
132
|
+
sql = "SELECT #{ projections } FROM #{ type_name }"
|
|
133
|
+
sql += " WHERE #{ conditions.join ' AND ' }" unless conditions.empty?
|
|
134
|
+
sql += " ORDER BY #{ ts_name }"
|
|
135
|
+
sql += " LIMIT #{ limit }" if limit
|
|
136
|
+
db.transmit { sql }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# the most recent sample, via the dedicated latest-endpoint
|
|
140
|
+
# returns a Hash of column => value (nil when the series is empty)
|
|
141
|
+
def latest tags: {}
|
|
142
|
+
query = "ts/#{ db.database }/latest?type=#{ type_name }"
|
|
143
|
+
query += "&tag=#{ tags.first.join ':' }" unless tags.empty?
|
|
144
|
+
answer = Api.get_json query
|
|
145
|
+
return nil if answer[:latest].nil?
|
|
146
|
+
answer[:columns].map.with_index { |col, i| [ col.to_sym, answer[:latest][i] ] }.to_h
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# time-bucketed aggregation, e.g. weekly candles:
|
|
150
|
+
# Ohlcv.bucket '7d', aggregate: { close: :last, high: :max, low: :min, volume: :sum }
|
|
151
|
+
def bucket interval, from: nil, to: nil, tags: {}, aggregate: {}
|
|
152
|
+
measures = aggregate.map do |field, fn|
|
|
153
|
+
"#{ AGGREGATES.fetch(fn).call(field, ts_name) } as #{ field }_#{ fn }"
|
|
154
|
+
end.join(', ')
|
|
155
|
+
conditions = []
|
|
156
|
+
conditions << "#{ ts_name } >= #{ sql_time from }" if from
|
|
157
|
+
conditions << "#{ ts_name } <= #{ sql_time to }" if to
|
|
158
|
+
tags.each { |k, v| conditions << "#{ k } = '#{ v }'" }
|
|
159
|
+
sql = "SELECT ts.timeBucket('#{ interval }', #{ ts_name }) as `bucket`"
|
|
160
|
+
sql += ", #{ measures }" unless measures.empty?
|
|
161
|
+
sql += " FROM #{ type_name }"
|
|
162
|
+
sql += " WHERE #{ conditions.join ' AND ' }" unless conditions.empty?
|
|
163
|
+
sql += " GROUP BY `bucket` ORDER BY `bucket`"
|
|
164
|
+
db.transmit { sql }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# ---- normalisation ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def sql_type type
|
|
172
|
+
TYPES.fetch(type) { type.to_s.upcase }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# sample hash -> InfluxDB line, epoch normalised to the declared precision
|
|
176
|
+
def line_for sample
|
|
177
|
+
stamp = to_epoch sample[ts_name] || sample[ts_name.to_s]
|
|
178
|
+
raise ArgumentError, "#{ type_name }: missing timestamp '#{ ts_name }'" if stamp.nil?
|
|
179
|
+
tag_part = tags.keys.map { |t| "#{ t }=#{ escape_tag sample[t] }" }.join(',')
|
|
180
|
+
field_part = fields.keys.map { |f| "#{ f }=#{ sample[f] }" }.join(',')
|
|
181
|
+
"#{ type_name },#{ tag_part } #{ field_part } #{ stamp }"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def to_epoch value
|
|
185
|
+
seconds = case value
|
|
186
|
+
when Time then value.to_i
|
|
187
|
+
when Numeric then value.to_i # already epoch seconds
|
|
188
|
+
when Date then Time.utc(value.year, value.month, value.day).to_i
|
|
189
|
+
when String then Time.parse(value).to_i
|
|
190
|
+
else nil end
|
|
191
|
+
return nil if seconds.nil?
|
|
192
|
+
seconds * PRECISIONS[ts_precision].last
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def sql_time value
|
|
196
|
+
t = case value
|
|
197
|
+
when Time then value
|
|
198
|
+
when Date then Time.utc(value.year, value.month, value.day)
|
|
199
|
+
when String then Time.parse(value, in: 'UTC')
|
|
200
|
+
when Numeric then Time.at(value)
|
|
201
|
+
else raise ArgumentError, "unsupported time: #{ value.inspect }" end
|
|
202
|
+
"'#{ t.utc.strftime('%Y-%m-%d %H:%M:%S') }'"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def escape_tag value
|
|
206
|
+
value.to_s.gsub(/([,= ])/) { '\\' + $1 }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
data/lib/arcade/version.rb
CHANGED
data/lib/arcade.rb
CHANGED
|
@@ -31,7 +31,9 @@ require_relative '../lib/arcade/config'
|
|
|
31
31
|
require_relative '../lib/arcade/database'
|
|
32
32
|
require_relative '../lib/arcade/match'
|
|
33
33
|
require_relative '../lib/arcade/query'
|
|
34
|
+
require_relative '../lib/arcade/timeseries'
|
|
34
35
|
require_relative '../lib/arcade/init'
|
|
36
|
+
require_relative '../lib/arcade/rag_schema'
|
|
35
37
|
require_relative "../lib/models"
|
|
36
38
|
require_relative '../lib/railtie' if defined? Rails::Railtie
|
|
37
39
|
|
data/lib/models.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: arcadedb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hartmut Bischoff
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -134,6 +133,7 @@ files:
|
|
|
134
133
|
- Gemfile.lock
|
|
135
134
|
- Guardfile
|
|
136
135
|
- LICENSE
|
|
136
|
+
- RAG_SCHEMA.md
|
|
137
137
|
- README.md
|
|
138
138
|
- Rakefile
|
|
139
139
|
- arcade.yml
|
|
@@ -141,6 +141,8 @@ files:
|
|
|
141
141
|
- bin/+
|
|
142
142
|
- bin/console
|
|
143
143
|
- examples/books.rb
|
|
144
|
+
- examples/rag_example.rb
|
|
145
|
+
- examples/rag_usecase_demo.rb
|
|
144
146
|
- examples/relation_1__1.rb
|
|
145
147
|
- examples/relation_1__n.rb
|
|
146
148
|
- examples/relation_n_n.rb
|
|
@@ -158,12 +160,14 @@ files:
|
|
|
158
160
|
- lib/arcade/logging.rb
|
|
159
161
|
- lib/arcade/match.rb
|
|
160
162
|
- lib/arcade/query.rb
|
|
163
|
+
- lib/arcade/rag_schema.rb
|
|
161
164
|
- lib/arcade/support/class.rb
|
|
162
165
|
- lib/arcade/support/conversions.rb
|
|
163
166
|
- lib/arcade/support/model.rb
|
|
164
167
|
- lib/arcade/support/object.rb
|
|
165
168
|
- lib/arcade/support/sql.rb
|
|
166
169
|
- lib/arcade/support/string.rb
|
|
170
|
+
- lib/arcade/timeseries.rb
|
|
167
171
|
- lib/arcade/version.rb
|
|
168
172
|
- lib/model/basicdocument.rb
|
|
169
173
|
- lib/model/basicedge.rb
|
|
@@ -172,6 +176,7 @@ files:
|
|
|
172
176
|
- lib/model/edge.rb
|
|
173
177
|
- lib/model/revision.rb
|
|
174
178
|
- lib/model/revision_record.rb
|
|
179
|
+
- lib/model/timeseries.rb
|
|
175
180
|
- lib/model/vertex.rb
|
|
176
181
|
- lib/models.rb
|
|
177
182
|
- lib/types.rb
|
|
@@ -183,7 +188,6 @@ homepage: https://github.com/topofocus/arcadedb
|
|
|
183
188
|
licenses:
|
|
184
189
|
- MIT
|
|
185
190
|
metadata: {}
|
|
186
|
-
post_install_message:
|
|
187
191
|
rdoc_options: []
|
|
188
192
|
require_paths:
|
|
189
193
|
- lib
|
|
@@ -198,8 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
198
202
|
- !ruby/object:Gem::Version
|
|
199
203
|
version: '0'
|
|
200
204
|
requirements: []
|
|
201
|
-
rubygems_version:
|
|
202
|
-
signing_key:
|
|
205
|
+
rubygems_version: 4.0.8
|
|
203
206
|
specification_version: 4
|
|
204
207
|
summary: Ruby Interface to ArcadeDB
|
|
205
208
|
test_files: []
|