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.
@@ -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
@@ -79,6 +79,49 @@ module Arcade
79
79
  # response.raise_for_status
80
80
  end
81
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
+
82
125
  # ------------------------------ transaction ------------------------------------------------- #
83
126
  #
84
127
  # The payload, optional as a JSON, accepts the following parameters:
data/lib/arcade/base.rb CHANGED
@@ -53,7 +53,7 @@ module Arcade
53
53
  begin
54
54
  loop do
55
55
  if the_class.respond_to?(:demodulize)
56
- if [ 'Document','Vertex', 'Edge'].include?(the_class.demodulize)
56
+ if [ 'Document','Vertex', 'Edge', 'Timeseries'].include?(the_class.demodulize)
57
57
  if the_class == superclass # no inheritance
58
58
  db.create_type the_class.demodulize, to_s.snake_case
59
59
  else
@@ -186,7 +186,7 @@ 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?
@@ -195,7 +195,15 @@ module Arcade
195
195
  content = "CONTENT #{ content_params.to_json }"
196
196
  target = target_params.map{|y,z| y==:type ? z : "#{y.to_s} #{ z } "}.join
197
197
  result = Api.execute( database, session_id: session ){ "INSERT INTO #{target} #{content} "}
198
- result &.first.allocate_model(false)
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
 
@@ -0,0 +1,131 @@
1
+ module Arcade
2
+ ##
3
+ # Ragschema - A generic class to implement a RAG (Retrieval-Augmented Generation)
4
+ # compatible database schema for ArcadeDB.
5
+ #
6
+ # This module provides a default-index configuration optimized for:
7
+ # - Vector similarity search (semantic embeddings)
8
+ # - Metadata filtering
9
+ #
10
+ # Usage:
11
+ # class MyDocument < Arcade::Vertex
12
+ # include Arcade::Ragschema
13
+ # attribute :content, Types::String
14
+ # attribute :metadata, Types::Hash.optional
15
+ # end
16
+ #
17
+ # MyDocument.create_type
18
+ #
19
+ module Ragschema
20
+ def self.included(base)
21
+ base.extend(ClassMethods)
22
+ end
23
+
24
+ module ClassMethods
25
+ ##
26
+ # Returns the default RAG schema setup commands
27
+ # to be executed after type creation
28
+ #
29
+ # @param vector_dimension [Integer] Dimension of vector embeddings (default: 1536 for OpenAI)
30
+ # @return [String] SQL commands for index creation
31
+ def rag_schema(vector_dimension: 1536)
32
+ type_name = database_name
33
+ commands = []
34
+
35
+ # Vector index for semantic similarity search
36
+ commands << "CREATE INDEX ON #{type_name} (embedding) LSM_VECTOR METADATA {dimensions: #{vector_dimension}, similarity: 'COSINE'}"
37
+
38
+ # Metadata index for filtering
39
+ commands << "CREATE INDEX IF NOT EXISTS ON #{type_name} (metadata) NOTUNIQUE"
40
+
41
+ commands.join("\n")
42
+ end
43
+
44
+ ##
45
+ # Default db_init method for RAG-compatible models
46
+ # Override this in your model class to customize
47
+ #
48
+ # @return [String] SQL commands to be executed on database
49
+ def rag_db_init(vector_dimension: 1536)
50
+ <<~SQL
51
+ CREATE PROPERTY #{database_name}.embedding ARRAY_OF_FLOATS
52
+ CREATE PROPERTY #{database_name}.content STRING
53
+ CREATE PROPERTY #{database_name}.metadata MAP
54
+ CREATE PROPERTY #{database_name}.source_url STRING
55
+ CREATE PROPERTY #{database_name}.created_at DATETIME
56
+ #{rag_schema(vector_dimension: vector_dimension)}
57
+ SQL
58
+ end
59
+
60
+ ##
61
+ # Perform vector similarity search across all documents
62
+ # Returns metadata only (not embedding vectors) for efficiency
63
+ #
64
+ # @param query_embedding [Array<Float>] The embedding vector to search for
65
+ # @param limit [Integer] Maximum number of results (default: 10)
66
+ # @param threshold [Float] Minimum similarity score (default: 0.7)
67
+ # @return [Array<Hash>] Matching records with metadata and similarity score
68
+ def vector_search(query_embedding, limit: 10, threshold: 0.7)
69
+ raise "vector_search must be called on a model class" unless respond_to?(:database_name)
70
+
71
+ type_name = database_name
72
+ # Convert array to ArcadeDB array literal format: [1.0,2.0,3.0]
73
+ embedding_array = "[#{query_embedding.join(',')}]"
74
+
75
+ # Select metadata fields only (not embedding) for efficiency
76
+ # Include @rid for reference and all other fields except embedding
77
+ query = <<~SQL
78
+ SELECT @rid, title, content, category, tags, author, source_url, created_at, updated_at, distance AS similarity
79
+ FROM (
80
+ SELECT expand(vectorNeighbors('#{type_name}[embedding]', #{embedding_array}, #{limit}))
81
+ )
82
+ WHERE distance <= (1.0 - #{threshold})
83
+ ORDER BY distance ASC
84
+ SQL
85
+
86
+ db.query(query)
87
+ end
88
+
89
+ ##
90
+ # Perform hybrid search combining vector similarity with SQL LIKE filtering
91
+ # Returns metadata only (not embedding vectors) for efficiency
92
+ #
93
+ # @param query_embedding [Array<Float>] The embedding vector
94
+ # @param text_query [String] SQL LIKE search query
95
+ # @param limit [Integer] Maximum number of results (default: 10)
96
+ # @param vector_threshold [Float] Minimum vector similarity (default: 0.7)
97
+ # @return [Array<Hash>] Matching records with metadata and similarity score
98
+ def hybrid_search(query_embedding, text_query, limit: 10, vector_threshold: 0.7)
99
+ raise "hybrid_search must be called on a model class" unless respond_to?(:database_name)
100
+
101
+ type_name = database_name
102
+ # Convert array to ArcadeDB array literal format: [1.0,2.0,3.0]
103
+ embedding_array = "[#{query_embedding.join(',')}]"
104
+
105
+ # Select metadata fields only (not embedding) for efficiency
106
+ query = <<~SQL
107
+ SELECT @rid, title, content, category, tags, author, source_url, created_at, updated_at, distance AS similarity
108
+ FROM (
109
+ SELECT expand(vectorNeighbors('#{type_name}[embedding]', #{embedding_array}, #{limit}))
110
+ )
111
+ WHERE distance <= (1.0 - #{vector_threshold})
112
+ AND content LIKE '%#{text_query}%'
113
+ ORDER BY distance ASC
114
+ SQL
115
+
116
+ db.query(query)
117
+ end
118
+ end
119
+
120
+ ##
121
+ # Instance method to perform vector similarity search
122
+ #
123
+ # @param query_embedding [Array<Float>] The embedding vector to search for
124
+ # @param limit [Integer] Maximum number of results (default: 10)
125
+ # @param threshold [Float] Minimum similarity score (default: 0.7)
126
+ # @return [Array<Base>] Matching records
127
+ def vector_search(query_embedding, limit: 10, threshold: 0.7)
128
+ self.class.vector_search(query_embedding, limit: limit, threshold: threshold)
129
+ end
130
+ end
131
+ end
@@ -60,7 +60,18 @@ module Arcade
60
60
  end
61
61
  end
62
62
  # choose the appropriate class
63
- klass= Dry::Core::ClassBuilder.new( name: type_name, parent: nil, namespace: namespace ).call
63
+ # First, try to find an existing model class
64
+ klass = nil
65
+ begin
66
+ klass = namespace.const_get(type_name) if namespace && namespace.const_defined?(type_name)
67
+ rescue NameError
68
+ # Class not found, will create dynamically
69
+ end
70
+
71
+ # If no existing class found, create a new one
72
+ unless klass
73
+ klass = Dry::Core::ClassBuilder.new( name: type_name, parent: nil, namespace: namespace ).call
74
+ end
64
75
  #
65
76
  begin
66
77
  # create a new object of that class with the appropriate attributes