arcadedb 0.5.0 → 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 +31 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +64 -58
- data/RAG_SCHEMA.md +239 -0
- data/README.md +131 -236
- data/arcade.yml +5 -5
- data/bin/console +1 -0
- data/examples/rag_example.rb +72 -0
- data/examples/rag_usecase_demo.rb +345 -0
- data/lib/arcade/api/operations.rb +33 -24
- data/lib/arcade/api/primitives.rb +52 -2
- data/lib/arcade/base.rb +35 -14
- data/lib/arcade/database.rb +16 -7
- data/lib/arcade/init.rb +5 -2
- data/lib/arcade/match.rb +85 -37
- data/lib/arcade/query.rb +10 -15
- data/lib/arcade/rag_schema.rb +131 -0
- data/lib/arcade/support/model.rb +20 -13
- data/lib/arcade/support/sql.rb +1 -1
- data/lib/arcade/support/string.rb +11 -14
- data/lib/arcade/timeseries.rb +211 -0
- data/lib/arcade/version.rb +1 -1
- data/lib/arcade.rb +7 -4
- data/lib/model/document.rb +1 -1
- data/lib/model/edge.rb +17 -0
- data/lib/model/revision.rb +41 -0
- data/lib/model/revision_record.rb +29 -0
- data/lib/model/timeseries.rb +6 -0
- data/lib/model/vertex.rb +52 -1
- data/lib/models.rb +3 -0
- data/lib/types.rb +35 -0
- metadata +12 -6
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# RAG Use Case Demo - Knowledge Base Search
|
|
2
|
+
#
|
|
3
|
+
# This example demonstrates a complete RAG (Retrieval-Augmented Generation) workflow
|
|
4
|
+
# using a knowledge base of technical documentation.
|
|
5
|
+
#
|
|
6
|
+
# Features demonstrated:
|
|
7
|
+
# - Document ingestion with vector embeddings
|
|
8
|
+
# - Vector similarity search for semantic queries
|
|
9
|
+
# - Hybrid search combining vector + full-text
|
|
10
|
+
# - Category and tag-based filtering
|
|
11
|
+
# - Real-world query scenarios
|
|
12
|
+
#
|
|
13
|
+
# Run with: ruby examples/rag_usecase_demo.rb
|
|
14
|
+
|
|
15
|
+
require 'bundler/setup'
|
|
16
|
+
require 'arcade'
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Model Definition
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
module UseCase
|
|
23
|
+
class KnowledgeDocument < Arcade::Vertex
|
|
24
|
+
include Arcade::Ragschema
|
|
25
|
+
|
|
26
|
+
attribute :title, Types::String
|
|
27
|
+
attribute :content, Types::String
|
|
28
|
+
attribute :embedding?, Types::Array.of(Types::Float).optional
|
|
29
|
+
attribute :category, Types::String.optional
|
|
30
|
+
attribute :tags, Types::Array.of(Types::String).optional
|
|
31
|
+
attribute :author, Types::String.optional
|
|
32
|
+
attribute :source_url, Types::String.optional
|
|
33
|
+
attribute :created_at, Types::DateTime.optional
|
|
34
|
+
attribute :updated_at, Types::DateTime.optional
|
|
35
|
+
|
|
36
|
+
def self.db_init
|
|
37
|
+
rag_db_init(
|
|
38
|
+
vector_dimension: 1536,
|
|
39
|
+
full_text_fields: ['title', 'content', 'category']
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Helper for generating mock embeddings (for demo purposes)
|
|
46
|
+
# In production, use a real embedding service (OpenAI, HuggingFace, etc.)
|
|
47
|
+
# =============================================================================
|
|
48
|
+
|
|
49
|
+
module EmbeddingHelper
|
|
50
|
+
##
|
|
51
|
+
# Generate a deterministic mock embedding for demonstration
|
|
52
|
+
#
|
|
53
|
+
# @param text [String] Text to embed
|
|
54
|
+
# @param seed [Integer] Seed for deterministic generation
|
|
55
|
+
# @return [Array<Float>] 1536-dimensional vector
|
|
56
|
+
def self.generate_mock_embedding(text, seed: 42)
|
|
57
|
+
base_hash = (text + seed.to_s).hash
|
|
58
|
+
vector = []
|
|
59
|
+
1536.times do |i|
|
|
60
|
+
value = ((base_hash * (i + 1) * 31) % 10000) / 10000.0 - 0.5
|
|
61
|
+
vector << value
|
|
62
|
+
end
|
|
63
|
+
vector
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# =============================================================================
|
|
69
|
+
# Demo Data - Technical Knowledge Base
|
|
70
|
+
# =============================================================================
|
|
71
|
+
|
|
72
|
+
KNOWLEDGE_BASE = [
|
|
73
|
+
{
|
|
74
|
+
title: 'Introduction to Graph Databases',
|
|
75
|
+
content: 'Graph databases are designed to store and treat relationships between data entities as first-class citizens. Unlike relational databases, graph databases use nodes and edges to represent and store data. This makes them ideal for analyzing complex relationships and patterns in data.',
|
|
76
|
+
category: 'Database Fundamentals',
|
|
77
|
+
tags: ['graph', 'database', 'nosql', 'fundamentals'],
|
|
78
|
+
author: 'Database Team'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: 'ArcadeDB Overview',
|
|
82
|
+
content: 'ArcadeDB is a multi-model graph database built from the ground up with support for vertices, edges, documents, arrays, and vectors. It provides a SQL-like query language and supports RAG (Retrieval-Augmented Generation) workloads with native vector search capabilities.',
|
|
83
|
+
category: 'Database Systems',
|
|
84
|
+
tags: ['arcadedb', 'graph', 'vector', 'rag'],
|
|
85
|
+
author: 'ArcadeDB Team'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: 'Vector Search Fundamentals',
|
|
89
|
+
content: 'Vector search, also known as semantic search, uses mathematical representations of data called embeddings. These embeddings capture the semantic meaning of text, allowing for similarity-based searches that understand context rather than just keyword matching.',
|
|
90
|
+
category: 'Search Technology',
|
|
91
|
+
tags: ['vector', 'embedding', 'similarity', 'ai'],
|
|
92
|
+
author: 'AI Research Team'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: 'RAG Architecture Patterns',
|
|
96
|
+
content: 'Retrieval-Augmented Generation (RAG) combines retrieval-based and generation-based approaches. It first retrieves relevant documents from a knowledge base using vector similarity, then uses a language model to generate responses based on the retrieved context.',
|
|
97
|
+
category: 'AI Architecture',
|
|
98
|
+
tags: ['rag', 'llm', 'retrieval', 'generation'],
|
|
99
|
+
author: 'ML Engineering'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
title: 'SQL Query Optimization',
|
|
103
|
+
content: 'Query optimization in SQL databases involves techniques like indexing, query planning, and execution strategy selection. Proper indexing strategies can dramatically improve query performance, especially for complex joins and aggregations.',
|
|
104
|
+
category: 'Database Performance',
|
|
105
|
+
tags: ['sql', 'optimization', 'indexing', 'performance'],
|
|
106
|
+
author: 'Performance Team'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: 'NoSQL vs SQL Databases',
|
|
110
|
+
content: 'NoSQL databases offer flexible schemas and horizontal scaling, making them suitable for unstructured data and high-velocity applications. SQL databases provide ACID transactions and structured querying capabilities. The choice depends on your specific use case requirements.',
|
|
111
|
+
category: 'Database Comparison',
|
|
112
|
+
tags: ['nosql', 'sql', 'comparison', 'architecture'],
|
|
113
|
+
author: 'Architecture Team'
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: 'Full-Text Search Implementation',
|
|
117
|
+
content: 'Full-text search indexes allow for efficient text querying beyond simple LIKE matches. They support tokenization, stemming, and relevance scoring. Modern search engines combine full-text search with vector embeddings for hybrid search capabilities.',
|
|
118
|
+
category: 'Search Technology',
|
|
119
|
+
tags: ['fulltext', 'search', 'indexing', 'hybrid'],
|
|
120
|
+
author: 'Search Team'
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: 'Embedding Models for Text',
|
|
124
|
+
content: 'Text embedding models like BERT, Sentence Transformers, and OpenAI embeddings convert text into dense vectors. These vectors capture semantic relationships, enabling tasks like semantic search, clustering, and similarity detection.',
|
|
125
|
+
category: 'Machine Learning',
|
|
126
|
+
tags: ['embedding', 'bert', 'nlp', 'transformers'],
|
|
127
|
+
author: 'NLP Team'
|
|
128
|
+
}
|
|
129
|
+
].freeze
|
|
130
|
+
|
|
131
|
+
# =============================================================================
|
|
132
|
+
# Demo Functions
|
|
133
|
+
# =============================================================================
|
|
134
|
+
|
|
135
|
+
def print_header(title)
|
|
136
|
+
puts "\n" + '=' * 70
|
|
137
|
+
puts title.center(70)
|
|
138
|
+
puts '=' * 70
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def print_section(title)
|
|
142
|
+
puts "\n--- #{title} ---\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def print_document(doc, show_similarity: false)
|
|
146
|
+
puts " Title: #{doc.title}"
|
|
147
|
+
puts " Category: #{doc.category}"
|
|
148
|
+
puts " Author: #{doc.author}"
|
|
149
|
+
puts " Tags: #{doc.tags.join(', ')}"
|
|
150
|
+
puts " Content: #{doc.content[0..150]}..."
|
|
151
|
+
puts " Similarity: #{doc.similarity.round(4)}" if doc.respond_to?(:similarity) && doc.similarity
|
|
152
|
+
puts
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def load_knowledge_base
|
|
156
|
+
print_section('Loading Knowledge Base')
|
|
157
|
+
|
|
158
|
+
KNOWLEDGE_BASE.each do |doc_data|
|
|
159
|
+
embedding = UseCase::EmbeddingHelper.generate_mock_embedding(
|
|
160
|
+
doc_data[:title] + ' ' + doc_data[:content]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
UseCase::KnowledgeDocument.create(
|
|
164
|
+
title: doc_data[:title],
|
|
165
|
+
content: doc_data[:content],
|
|
166
|
+
embedding: embedding,
|
|
167
|
+
category: doc_data[:category],
|
|
168
|
+
tags: doc_data[:tags],
|
|
169
|
+
author: doc_data[:author],
|
|
170
|
+
created_at: DateTime.now
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
puts " ✓ Loaded: #{doc_data[:title]}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
total = UseCase::KnowledgeDocument.count
|
|
177
|
+
puts "\nTotal documents: #{total}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def demo_vector_search
|
|
181
|
+
print_section('Vector Similarity Search')
|
|
182
|
+
|
|
183
|
+
queries = [
|
|
184
|
+
'What is a graph database and how does it work?',
|
|
185
|
+
'How does RAG architecture work?',
|
|
186
|
+
'Explain vector embeddings for text search'
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
queries.each do |query|
|
|
190
|
+
puts "\nQuery: \"#{query}\""
|
|
191
|
+
puts "-" * 50
|
|
192
|
+
|
|
193
|
+
query_embedding = UseCase::EmbeddingHelper.generate_mock_embedding(query)
|
|
194
|
+
|
|
195
|
+
results = UseCase::KnowledgeDocument.vector_search(
|
|
196
|
+
query_embedding,
|
|
197
|
+
limit: 3,
|
|
198
|
+
threshold: 0.0
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
results.each_with_index do |doc, i|
|
|
202
|
+
puts " [#{i + 1}] #{doc.title} (similarity: #{doc.similarity.round(4) if doc.similarity})"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def demo_hybrid_search
|
|
208
|
+
print_section('Hybrid Search (Vector + Full-Text)')
|
|
209
|
+
|
|
210
|
+
searches = [
|
|
211
|
+
{ query: 'database performance', text: 'optimization' },
|
|
212
|
+
{ query: 'search technology', text: 'Full-Text' },
|
|
213
|
+
{ query: 'machine learning', text: 'embedding' }
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
searches.each do |search|
|
|
217
|
+
puts "\nVector Query: \"#{search[:query]}\""
|
|
218
|
+
puts "Text Filter: \"#{search[:text]}\""
|
|
219
|
+
puts "-" * 50
|
|
220
|
+
|
|
221
|
+
query_embedding = UseCase::EmbeddingHelper.generate_mock_embedding(search[:query])
|
|
222
|
+
|
|
223
|
+
results = UseCase::KnowledgeDocument.hybrid_search(
|
|
224
|
+
query_embedding,
|
|
225
|
+
search[:text],
|
|
226
|
+
limit: 5,
|
|
227
|
+
vector_threshold: 0.0
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if results.empty?
|
|
231
|
+
puts " No results found"
|
|
232
|
+
else
|
|
233
|
+
results.each_with_index do |doc, i|
|
|
234
|
+
puts " [#{i + 1}] #{doc.title}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def demo_category_filtering
|
|
241
|
+
print_section('Category-Based Filtering')
|
|
242
|
+
|
|
243
|
+
categories = ['Search Technology', 'Database Systems', 'Machine Learning']
|
|
244
|
+
|
|
245
|
+
categories.each do |category|
|
|
246
|
+
puts "\nCategory: #{category}"
|
|
247
|
+
puts "-" * 50
|
|
248
|
+
|
|
249
|
+
results = UseCase::KnowledgeDocument.where("category = '#{category}'")
|
|
250
|
+
|
|
251
|
+
results.each do |doc|
|
|
252
|
+
puts " • #{doc.title} (#{doc.author})"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def demo_tag_search
|
|
258
|
+
print_section('Tag-Based Search')
|
|
259
|
+
|
|
260
|
+
tags = ['vector', 'graph', 'embedding']
|
|
261
|
+
|
|
262
|
+
tags.each do |tag|
|
|
263
|
+
puts "\nTag: #{tag}"
|
|
264
|
+
puts "-" * 50
|
|
265
|
+
|
|
266
|
+
results = UseCase::KnowledgeDocument.where("tags CONTAINS '#{tag}'")
|
|
267
|
+
|
|
268
|
+
results.each do |doc|
|
|
269
|
+
puts " • #{doc.title} [#{doc.tags.join(', ')}]"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def demo_real_world_queries
|
|
275
|
+
print_section('Real-World Query Scenarios')
|
|
276
|
+
|
|
277
|
+
scenarios = {
|
|
278
|
+
'What is ArcadeDB?' => 'Definition query',
|
|
279
|
+
'difference between SQL and NoSQL' => 'Comparison query',
|
|
280
|
+
'How to optimize database queries?' => 'How-to query',
|
|
281
|
+
'Explain RAG benefits' => 'Concept exploration'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
scenarios.each do |query, type|
|
|
285
|
+
puts "\n[#{type}] \"#{query}\""
|
|
286
|
+
puts "-" * 50
|
|
287
|
+
|
|
288
|
+
query_embedding = UseCase::EmbeddingHelper.generate_mock_embedding(query)
|
|
289
|
+
|
|
290
|
+
results = UseCase::KnowledgeDocument.vector_search(
|
|
291
|
+
query_embedding,
|
|
292
|
+
limit: 2,
|
|
293
|
+
threshold: 0.0
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
results.each do |doc|
|
|
297
|
+
puts " → #{doc.title}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# =============================================================================
|
|
303
|
+
# Main Demo Runner
|
|
304
|
+
# =============================================================================
|
|
305
|
+
|
|
306
|
+
def run_demo
|
|
307
|
+
print_header('RAG Use Case Demo - Knowledge Base Search')
|
|
308
|
+
|
|
309
|
+
# Connect to database
|
|
310
|
+
Arcade::Init.connect('test')
|
|
311
|
+
|
|
312
|
+
# Create type if needed
|
|
313
|
+
unless Arcade::Init.db.types.include?('UseCase_KnowledgeDocument')
|
|
314
|
+
puts 'Creating KnowledgeDocument type...'
|
|
315
|
+
UseCase::KnowledgeDocument.create_type
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Clear existing data for fresh demo
|
|
319
|
+
UseCase::KnowledgeDocument.delete_all rescue nil
|
|
320
|
+
|
|
321
|
+
# Run demos
|
|
322
|
+
load_knowledge_base
|
|
323
|
+
demo_vector_search
|
|
324
|
+
demo_hybrid_search
|
|
325
|
+
demo_category_filtering
|
|
326
|
+
demo_tag_search
|
|
327
|
+
demo_real_world_queries
|
|
328
|
+
|
|
329
|
+
print_header('Demo Complete!')
|
|
330
|
+
puts "\nThis demo showcased:"
|
|
331
|
+
puts " ✓ Document ingestion with vector embeddings"
|
|
332
|
+
puts " ✓ Vector similarity search for semantic queries"
|
|
333
|
+
puts " ✓ Hybrid search (vector + full-text)"
|
|
334
|
+
puts " ✓ Category and tag-based filtering"
|
|
335
|
+
puts " ✓ Real-world query scenarios"
|
|
336
|
+
puts "\nFor more information, see:"
|
|
337
|
+
puts " - RAG_SCHEMA.md"
|
|
338
|
+
puts " - spec/lib/usecase/rag_integration_spec.rb"
|
|
339
|
+
puts
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Run if executed directly
|
|
343
|
+
if $0 == __FILE__
|
|
344
|
+
run_demo
|
|
345
|
+
end
|
|
@@ -32,6 +32,13 @@ module Arcade
|
|
|
32
32
|
get_data 'databases'
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# ------------------------------ database? ------------------------------------------------- #
|
|
36
|
+
# returns true if the database exists #
|
|
37
|
+
|
|
38
|
+
def self.database? name
|
|
39
|
+
get_data "exists/#{name}"
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
# ------------------------------ create database ------------------------------------------------- #
|
|
36
43
|
# creates a database if not present #
|
|
37
44
|
def self.create_database name
|
|
@@ -54,23 +61,6 @@ module Arcade
|
|
|
54
61
|
logger.fatal "Drop database #{name} through \"POST drop database/#{name}\" failed"
|
|
55
62
|
raise
|
|
56
63
|
end
|
|
57
|
-
# ------------------------------ create document ------------------------------------------------- #
|
|
58
|
-
# adds a document to the specified database table
|
|
59
|
-
#
|
|
60
|
-
# specify database-fields as hash-type parameters
|
|
61
|
-
#
|
|
62
|
-
# i.e Arcade::Api.create_document 'devel', 'documents', name: 'herta meyer', age: 56, sex: 'f'
|
|
63
|
-
#
|
|
64
|
-
# returns the rid of the inserted dataset
|
|
65
|
-
#
|
|
66
|
-
def self.create_document database, type, session_id: nil, **attributes
|
|
67
|
-
payload = { "@type" => type }.merge( attributes )
|
|
68
|
-
if session_id.nil?
|
|
69
|
-
post_data "document/#{database}", payload
|
|
70
|
-
else
|
|
71
|
-
post_transaction "document/#{database}", payload, session_id: session_id
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
64
|
|
|
75
65
|
# ------------------------------ execute ------------------------------------------------- #
|
|
76
66
|
# executes a sql-query in the specified database
|
|
@@ -94,13 +84,28 @@ module Arcade
|
|
|
94
84
|
# ------------------------------ query ------------------------------------------------- #
|
|
95
85
|
# same for idempotent queries
|
|
96
86
|
def self.query database, query, session_id: nil
|
|
97
|
-
if session_id.nil?
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
87
|
+
# if session_id.nil?
|
|
88
|
+
get_data "query/#{database}/sql/" + provide_payload(query, action: :get)[:query]
|
|
89
|
+
# else
|
|
90
|
+
# post_transaction "query/#{database}" , provide_payload(query), session_id: session_id
|
|
91
|
+
# end
|
|
102
92
|
end
|
|
103
93
|
|
|
94
|
+
# ------------------------------ create Document ------------------------------------------------- #
|
|
95
|
+
# adds a record ( document, vertex or edge ) to the specified database type
|
|
96
|
+
#
|
|
97
|
+
# specify database-fields as hash-type parameters
|
|
98
|
+
#
|
|
99
|
+
# i.e Arcade::Api.create_record 'devel', 'documents', name: 'herta meyer', age: 56, sex: 'f'
|
|
100
|
+
#
|
|
101
|
+
# returns the rid of the inserted dataset
|
|
102
|
+
#
|
|
103
|
+
def self.create_document database, type, session_id: nil, **attributes
|
|
104
|
+
content = "CONTENT #{ attributes.to_json }"
|
|
105
|
+
result = execute( database, session_id: session_id ){ "INSERT INTO #{type} #{content} "}
|
|
106
|
+
result.first[:@rid] # emulate the »old« behavior of the /post/create_document api endpoint
|
|
107
|
+
end
|
|
108
|
+
|
|
104
109
|
# ------------------------------ get_record ------------------------------------------------- #
|
|
105
110
|
# fetches a record by providing database and rid
|
|
106
111
|
# and returns the result as hash
|
|
@@ -114,7 +119,7 @@ module Arcade
|
|
|
114
119
|
rid = rid.join(':')
|
|
115
120
|
rid = rid[1..-1] if rid[0]=="#"
|
|
116
121
|
if rid.rid?
|
|
117
|
-
|
|
122
|
+
get_data( "query/#{database}/sql/" + URI.encode_uri_component("select from #{rid}")) &.first
|
|
118
123
|
else
|
|
119
124
|
raise Error "Get requires a rid input"
|
|
120
125
|
end
|
|
@@ -181,7 +186,11 @@ module Arcade
|
|
|
181
186
|
the_yield.map do | key, value |
|
|
182
187
|
case key
|
|
183
188
|
when :query
|
|
184
|
-
|
|
189
|
+
if action == :post
|
|
190
|
+
[ :command, value ]
|
|
191
|
+
else
|
|
192
|
+
[ :query, URI.encode_uri_component(value )]
|
|
193
|
+
end
|
|
185
194
|
when :limit
|
|
186
195
|
[ :limit , value ]
|
|
187
196
|
when :params
|
|
@@ -9,6 +9,12 @@ module Arcade
|
|
|
9
9
|
|
|
10
10
|
def http
|
|
11
11
|
# break_on = -> (response) { response.status == 500 }
|
|
12
|
+
# Version 23.12: Persistent connection are inactive after 3 sec.
|
|
13
|
+
# Implemented a walk around, that renews the connection after 2 sec. of inactivity
|
|
14
|
+
t = Time.now
|
|
15
|
+
@t ||= Time.now
|
|
16
|
+
@http = nil if t-@t > 2
|
|
17
|
+
@t = t
|
|
12
18
|
@http ||= HTTPX.plugin(:basic_auth).basic_auth(auth[:username], auth[:password])
|
|
13
19
|
.plugin(:persistent)
|
|
14
20
|
.plugin(:circuit_breaker)
|
|
@@ -49,7 +55,7 @@ module Arcade
|
|
|
49
55
|
|
|
50
56
|
# ------------------------------ post data -------------------------------------------------------- #
|
|
51
57
|
def post_data command, payload
|
|
52
|
-
|
|
58
|
+
case response = http.post( Config.base_uri + command, json: payload )
|
|
53
59
|
in {status: 200..299}
|
|
54
60
|
# success
|
|
55
61
|
JSON.parse( response.body, symbolize_names: true )[:result]
|
|
@@ -64,7 +70,7 @@ module Arcade
|
|
|
64
70
|
sleep 1
|
|
65
71
|
post_data command, payload
|
|
66
72
|
else
|
|
67
|
-
|
|
73
|
+
raise Arcade::QueryError.new **response.json( symbolize_names: true )
|
|
68
74
|
end
|
|
69
75
|
else
|
|
70
76
|
# # http error
|
|
@@ -73,6 +79,49 @@ module Arcade
|
|
|
73
79
|
# response.raise_for_status
|
|
74
80
|
end
|
|
75
81
|
|
|
82
|
+
# ------------------------------ post text ----------------------------------------------------- #
|
|
83
|
+
# posts a plain-text body (used by the TimeSeries Line Protocol endpoint)
|
|
84
|
+
def post_text command, body
|
|
85
|
+
case response = http.post( Config.base_uri + command, body: body )
|
|
86
|
+
in {status: 200..299}
|
|
87
|
+
response.status
|
|
88
|
+
in {status: 400..}
|
|
89
|
+
answer = response.json( symbolize_names: true )
|
|
90
|
+
answer[:detail] ||= answer[:error] # ts-endpoints report error only
|
|
91
|
+
raise Arcade::QueryError.new **answer
|
|
92
|
+
else
|
|
93
|
+
raise response
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ------------------------------ post json raw ------------------------------------------------- #
|
|
98
|
+
# posts a json body, returns the parsed body (not the :result part)
|
|
99
|
+
def post_json command, payload
|
|
100
|
+
case response = http.post( Config.base_uri + command, json: payload )
|
|
101
|
+
in {status: 200..299}
|
|
102
|
+
JSON.parse( response.body, symbolize_names: true )
|
|
103
|
+
in {status: 400..}
|
|
104
|
+
answer = response.json( symbolize_names: true )
|
|
105
|
+
answer[:detail] ||= answer[:error]
|
|
106
|
+
raise Arcade::QueryError.new **answer
|
|
107
|
+
else
|
|
108
|
+
raise response
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ------------------------------ get json ------------------------------------------------------ #
|
|
113
|
+
# GET returning the parsed body (not only the :result part)
|
|
114
|
+
def get_json command
|
|
115
|
+
case response = http.get( Config.base_uri + command )
|
|
116
|
+
in {status: 200..299}
|
|
117
|
+
JSON.parse( response.body, symbolize_names: true )
|
|
118
|
+
in {status: 400..}
|
|
119
|
+
raise Arcade::QueryError.new **response.json( symbolize_names: true )
|
|
120
|
+
else
|
|
121
|
+
raise response
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
76
125
|
# ------------------------------ transaction ------------------------------------------------- #
|
|
77
126
|
#
|
|
78
127
|
# The payload, optional as a JSON, accepts the following parameters:
|
|
@@ -87,6 +136,7 @@ module Arcade
|
|
|
87
136
|
# ------------------------------ post transaction ------------------------------------------------- #
|
|
88
137
|
def post_transaction command, params, session_id:
|
|
89
138
|
http_a = http.with( headers: { "arcadedb-session-id" => session_id } , debug_level: 1)
|
|
139
|
+
puts "params #{params.inspect}"
|
|
90
140
|
case response = http_a.post( Config.base_uri + command, json: params )
|
|
91
141
|
in {status: 200..299}
|
|
92
142
|
# success
|
data/lib/arcade/base.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
module Arcade
|
|
2
|
+
|
|
2
3
|
class Base < Dry::Struct
|
|
3
4
|
|
|
4
5
|
extend Arcade::Support::Sql
|
|
@@ -20,6 +21,9 @@ module Arcade
|
|
|
20
21
|
## ----------------------------------------- Class Methods------------------------------------ ##
|
|
21
22
|
# #
|
|
22
23
|
class << self
|
|
24
|
+
def descendants
|
|
25
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
|
26
|
+
end
|
|
23
27
|
|
|
24
28
|
# this has to be implemented on class level
|
|
25
29
|
# otherwise it interfere with attributes
|
|
@@ -49,7 +53,7 @@ module Arcade
|
|
|
49
53
|
begin
|
|
50
54
|
loop do
|
|
51
55
|
if the_class.respond_to?(:demodulize)
|
|
52
|
-
if [ 'Document','Vertex', 'Edge'].include?(the_class.demodulize)
|
|
56
|
+
if [ 'Document','Vertex', 'Edge', 'Timeseries'].include?(the_class.demodulize)
|
|
53
57
|
if the_class == superclass # no inheritance
|
|
54
58
|
db.create_type the_class.demodulize, to_s.snake_case
|
|
55
59
|
else
|
|
@@ -294,16 +298,19 @@ module Arcade
|
|
|
294
298
|
Query.new( **{ from: self }.merge(args) )
|
|
295
299
|
end
|
|
296
300
|
|
|
297
|
-
#
|
|
298
|
-
#
|
|
301
|
+
# ## Immutable Support
|
|
302
|
+
#
|
|
303
|
+
# To make a database type immutable add
|
|
299
304
|
# `not_permitted :update, :upsert, :delete`
|
|
300
305
|
# to the model-specification
|
|
301
306
|
#
|
|
307
|
+
# Even after applying `not_permitted` the database-type can be modified via class-methods.
|
|
308
|
+
#
|
|
302
309
|
def not_permitted *m
|
|
303
310
|
m.each do | def_m |
|
|
304
|
-
define_method(
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
define_method( def_m ) do | v = nil |
|
|
312
|
+
raise Arcade::ImmutableError.new( "operation #{def_m} not permitted" )
|
|
313
|
+
end
|
|
307
314
|
end
|
|
308
315
|
end
|
|
309
316
|
|
|
@@ -366,8 +373,6 @@ module Arcade
|
|
|
366
373
|
end
|
|
367
374
|
|
|
368
375
|
def to_human
|
|
369
|
-
|
|
370
|
-
|
|
371
376
|
"<#{ self.class.to_s.snake_case }" + rid? ? "[#{ rid }]: " : " " + invariant_attributes.map do |attr, value|
|
|
372
377
|
v= case value
|
|
373
378
|
when Base
|
|
@@ -428,7 +433,7 @@ module Arcade
|
|
|
428
433
|
|
|
429
434
|
def update **args
|
|
430
435
|
Query.new( from: rid , kind: :update, set: args).execute
|
|
431
|
-
refresh
|
|
436
|
+
refresh # return the updated record (the object itself is untouched!)
|
|
432
437
|
end
|
|
433
438
|
|
|
434
439
|
# inserts or updates a embedded document
|
|
@@ -448,21 +453,37 @@ module Arcade
|
|
|
448
453
|
db.transmit { " update #{rid} set `#{embedded}`.`#{embedded_property}` = #{value.to_or}" }
|
|
449
454
|
end
|
|
450
455
|
|
|
451
|
-
|
|
452
|
-
|
|
456
|
+
# Adds List-Elements to embedded List
|
|
457
|
+
#
|
|
458
|
+
# Arguments:
|
|
459
|
+
# * list: A symbol of the list property
|
|
460
|
+
# * value: A embedded document or a hash
|
|
461
|
+
# * modus: :auto, :first, :append
|
|
462
|
+
#
|
|
463
|
+
# Prefered modus operandi
|
|
464
|
+
# * the-element.insert (...) , #{list}:[]
|
|
465
|
+
# * the_element.update_list list, value: :append
|
|
466
|
+
#
|
|
467
|
+
def update_list list, value, modus: :auto
|
|
468
|
+
value = if value.is_a? Document # embedded mode
|
|
453
469
|
value.to_json
|
|
454
470
|
else
|
|
455
471
|
value.to_or
|
|
456
472
|
end
|
|
457
|
-
if
|
|
458
|
-
db.
|
|
473
|
+
if modus == :auto
|
|
474
|
+
modus = db.query( "select #{list}.size() from #{rid}" ).select_result.first.zero? ? :first : :append
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
if modus == :first
|
|
478
|
+
db.transmit { "update #{ rid } set #{ list } = [#{ value }]" }
|
|
459
479
|
else
|
|
460
480
|
db.transmit { "update #{ rid } set #{ list } += #{ value }" }
|
|
461
481
|
end
|
|
462
|
-
|
|
482
|
+
# refresh
|
|
463
483
|
end
|
|
464
484
|
|
|
465
485
|
# updates a map property , actually adds the key-value pair to the property
|
|
486
|
+
## does not work on reduced model records
|
|
466
487
|
def update_map m, key, value
|
|
467
488
|
if send( m ).nil?
|
|
468
489
|
db.transmit { "update #{ rid } set #{ m } = MAP ( #{ key.to_s.to_or } , #{ value.to_or } ) " }
|
data/lib/arcade/database.rb
CHANGED
|
@@ -157,7 +157,7 @@ module Arcade
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
# ------------ create -----------
|
|
160
|
+
# ------------ create ----------- ## not supported anymore by the api
|
|
161
161
|
# returns an rid of the successfully created vertex or document
|
|
162
162
|
#
|
|
163
163
|
# Parameter: name of the vertex or document type
|
|
@@ -186,19 +186,28 @@ module Arcade
|
|
|
186
186
|
|
|
187
187
|
content_params = params.except( :type, :bucket, :index, :from, :return, :session_id )
|
|
188
188
|
target_params = params.slice( :type, :bucket, :index )
|
|
189
|
-
# session_id = params[:session_id] # extraxt session_id --> future-use?
|
|
189
|
+
# session_id = params[:session_id] # extraxt session_id --> future-use?
|
|
190
190
|
if target_params.empty?
|
|
191
191
|
raise "Could not insert: target missing (type:, bucket:, index:)"
|
|
192
192
|
elsif content_params.empty?
|
|
193
193
|
logger.error "Nothing to Insert"
|
|
194
194
|
else
|
|
195
|
-
content =
|
|
195
|
+
content = "CONTENT #{ content_params.to_json }"
|
|
196
196
|
target = target_params.map{|y,z| y==:type ? z : "#{y.to_s} #{ z } "}.join
|
|
197
|
-
result =
|
|
198
|
-
|
|
197
|
+
result = Api.execute( database, session_id: session ){ "INSERT INTO #{target} #{content} "}
|
|
198
|
+
#
|
|
199
|
+
result &.record&.allocate_model(false)
|
|
200
|
+
## commented for now. reactivate if necessary (performance issue)
|
|
201
|
+
#record = result&.first
|
|
202
|
+
# Ensure @type is present for proper model allocation
|
|
203
|
+
#if record && !record.key?(:@type) && target_params[:type]
|
|
204
|
+
# record = record.merge( :"@type" => target_params[:type] )
|
|
205
|
+
#end
|
|
206
|
+
#record&.allocate_model(false)
|
|
199
207
|
end
|
|
200
208
|
end
|
|
201
209
|
|
|
210
|
+
|
|
202
211
|
# ------------------------------ get ------------------------------------------------------ #
|
|
203
212
|
# Get fetches the record associated with the rid given as parameter.
|
|
204
213
|
#
|
|
@@ -224,7 +233,7 @@ module Arcade
|
|
|
224
233
|
end
|
|
225
234
|
end
|
|
226
235
|
|
|
227
|
-
# ------------------------------
|
|
236
|
+
# ------------------------------ delete ------------------------------------------------------ #
|
|
228
237
|
#
|
|
229
238
|
# Delete the specified rid
|
|
230
239
|
#
|
|
@@ -272,7 +281,7 @@ module Arcade
|
|
|
272
281
|
end
|
|
273
282
|
rescue Dry::Struct::Error, Arcade::QueryError => e
|
|
274
283
|
Api.rollback database, session_id: s, log: false
|
|
275
|
-
logger.
|
|
284
|
+
logger.fatal "Execution FAILED --> Status #{e}"
|
|
276
285
|
[] # return empty result
|
|
277
286
|
end
|
|
278
287
|
|