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,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
|
data/lib/arcade/database.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/arcade/support/model.rb
CHANGED
|
@@ -60,7 +60,18 @@ module Arcade
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
# choose the appropriate class
|
|
63
|
-
|
|
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
|