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.
data/lib/arcade/init.rb CHANGED
@@ -14,7 +14,8 @@ module Arcade
14
14
  #
15
15
  class Init
16
16
  extend Dry::Core::ClassAttributes
17
- defines :db # database handle
17
+ defines :db # database handle
18
+ defines :models # static array of database name for arcade-model classes
18
19
 
19
20
  def self.connect e= :development
20
21
 
@@ -25,10 +26,12 @@ module Arcade
25
26
  else
26
27
  :development
27
28
  end
28
- # set the class attribute
29
29
 
30
+ # set the class attributes
31
+ models Base.descendants.map{|x| x.to_s.split("Arcade::").last }
30
32
  db Database.new(env)
31
33
  end
34
+
32
35
  end
33
36
 
34
37
  # Provides method `db` to every Model class
data/lib/arcade/match.rb CHANGED
@@ -4,22 +4,64 @@ module Arcade
4
4
  include Arcade::Support::Sql
5
5
 
6
6
  =begin
7
- This is a very simple wrapper for the match statement
7
+ This is a wrapper for the match statement
8
8
 
9
9
  Initialize: a= Arcade::Match.new type: Arcade::DatabaseType, where: { property: 'value' }, as: :alias
10
10
  Complete: b = a.out( Arcade::EdgeType ).node( while: true, as: item )[ .in.node ... ]
11
11
  Inspect b.to_s
12
12
  Query DB b.execute [.allocate_model]
13
13
  [.analyse_result]
14
+ Customize the return values:
15
+ b.to_s{ "customized return statement" }
16
+ b.execute{ "customized return statment" }
17
+
18
+ Conditions on edges:
19
+ m = Match.new type: Arcade::DatabaseType
20
+ m.inE via: Arcade::Edge || via: [Arcade::Edge1, Arcade::Edge2], where: { var: 3..6 }
21
+ ---> inE('edge(1)'){ where: (var between 3 and 6 )}.outV('edge(2)')
22
+
23
+ Address a single database record:
24
+ m = Match.new( vertex: { Arcade::Vertex-Instance } )
25
+ .....
26
+ .node( vertex: { Arcade::Vertex-Instance } ) # instead of where statement
27
+
28
+ Example
29
+
30
+ symbol_or_title = symbol.present? ? { :symbol => symbol } : { :title => title }
31
+ where = { :right => right }
32
+
33
+ a= Arcade::Match.new( type: self.class, where: symbol_or_title )
34
+ .out( Arcade::HasContract )
35
+ .node( as: :c, where: where )
36
+ a.execute do "c.last_trading_day as expiry, # c is returned form the query [as: :c]
37
+ count(c) as contracts,
38
+ min(c.strike) as s_min,
39
+ max(c.strike) as s_max
40
+ group by c.last_trading_day order by c.last_trading_day"
41
+ end
14
42
 
15
- =end
16
43
 
17
- def initialize type: , **args
44
+ =end
18
45
 
46
+ def initialize **args
47
+ type = args.delete :type
48
+ vertex = args.delete :vertex
49
+ rid = args.delete :rid
50
+ raise "MATCH#new:: parameter rid is not supported. Use `vertex: Arcade::Vertex.instance` instead." if rid.present?
19
51
  @args = args
20
52
  @as = []
21
53
 
22
- @stack = [ "MATCH { type: #{type.database_name}, #{ assigned_parameters } }" ]
54
+ @stack = if vertex.is_a?( Arcade::Vertex ) && args.empty?
55
+ [ "MATCH { type: #{vertex.class.database_name }, rid: #{vertex.rid} }" ]
56
+ elsif vertex.is_a?( Arcade::Vertex )
57
+ [ "MATCH { type: #{vertex.class.database_name }, rid: #{vertex.rid}, #{ assigned_parameters } }" ]
58
+ elsif type.is_a?( Class) && type.ancestors.include?(Arcade::Vertex) && args.empty?
59
+ [ "MATCH { type: #{type.database_name} }" ]
60
+ elsif type.is_a?( Class) && type.ancestors.include?(Arcade::Vertex)
61
+ [ "MATCH { type: #{type.database_name}, #{ assigned_parameters } }" ]
62
+ else
63
+ raise "Match:: Either type (Arcade::Vertex-Class) or vertex (Arcade::Vertex-Object) is required as parameter"
64
+ end
23
65
 
24
66
  return self
25
67
  end
@@ -49,49 +91,55 @@ module Arcade
49
91
  end
50
92
 
51
93
 
52
- # todo : metaprogramming!
53
- def out edge=""
54
- raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
55
- @stack << ".out(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
56
- return self
57
- end
58
- def in edge=""
59
- raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
60
- @stack << ".in(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
61
- return self
62
- end
63
- def both edge=""
64
- raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
65
- @stack << ".both(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
66
- return self
94
+
95
+ def self.define_base_edge *direction
96
+ direction.each do | e_d |
97
+ define_method e_d do | edge = "" |
98
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
99
+ @stack << ".#{ e_d }(#{edge.is_a?(Class) ? edge.database_name.to_or : ''})"
100
+ self
101
+ end
102
+ end
67
103
  end
68
104
 
105
+
69
106
  # add conditions on edges to the match statement
70
- def inE edge="", **args
71
- raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
72
- @stack << ".inE(#{edge.is_a?(Class) ? edge.database_name.to_or : ''}) #{assigned_parameters}.outV()"
73
- return self
74
- end
75
- def outE edge="", **args
76
- raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
77
- @stack << ".outE(#{edge.is_a?(Class) ? edge.database_name.to_or : ''}) #{assigned_parameters}.inV()"
78
- return self
79
- end
80
- def bothE edge="", **args
81
- raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
82
- @stack << ".bothE(#{edge.is_a?(Class) ? edge.database_name : ''}) #{assigned_parameters}.bothV()"
83
- return self
107
+ def self.define_inner_edge start_dir, final_dir
108
+ define_method start_dir do | edge = "", **a |
109
+ raise "edge must be a Database-class" unless edge.is_a?(Class) || edge.empty?
110
+
111
+ print_edges = -> (e){ e.is_a?(Class) ? e.database_name.to_or : ''}
112
+ edges = [ a.delete(:via) ].flatten
113
+ edges << edge unless edge == ''
114
+ n = if a.empty?
115
+ ""
116
+ else
117
+ @args = a
118
+ "{ #{ assigned_parameters } }"
119
+ end
120
+ @stack << ".#{ start_dir }(#{print_edges.call(edges.first)})#{ n }.#{ final_dir }(#{print_edges.call(edges.last)})"
121
+ return self
122
+ end
123
+
84
124
  end
85
125
 
126
+ define_base_edge :out, :in, :both
127
+ define_inner_edge :inE, :outV
128
+ define_inner_edge :outE, :inV
129
+
130
+
86
131
 
87
132
  # general declation of a node (ie. vertex)
88
133
  def node **args
134
+ vertex = args.delete :vertex
135
+ rid = args.delete :rid
136
+ raise "MATCH#node:: parameter rid is not supported. Use `vertex: Arcade::Vertex.instance` instead." if rid.present?
89
137
  @args = args
90
138
  @stack << if args.empty?
91
- "{}"
92
- else
93
- "{ #{ assigned_parameters } }"
94
- end
139
+ vertex.present? ? "{ type: #{vertex.class.database_name }, rid: #{vertex.rid} }" : "{}"
140
+ else
141
+ vertex.present? ? "{ type: #{vertex.class.database_name }, rid: #{vertex.rid}, #{assigned_parameters} }" : "{ #{ assigned_parameters } }"
142
+ end
95
143
  return self
96
144
  end
97
145
 
data/lib/arcade/query.rb CHANGED
@@ -27,7 +27,8 @@ module Arcade
27
27
  [], # aliases
28
28
  '', # database
29
29
  [], #set,
30
- [] # remove
30
+ [], # remove
31
+ [] # group by
31
32
  args.each{|k,v| send k, v}
32
33
  @fill = block_given? ? yield : 'and'
33
34
  end
@@ -62,12 +63,10 @@ module Arcade
62
63
  @q[:kind]
63
64
  end
64
65
  end
65
- =begin
66
- Output the compiled query
67
- Parameter: destination (rest, batch )
68
- If the query is submitted via the REST-Interface (as get-command), the limit parameter is extracted.
69
- =end
70
-
66
+ # ------------------ compose---------------------------------------
67
+ # Output the compiled query
68
+ # Parameter: destination (rest, batch )
69
+ # If the query is submitted via the REST-Interface (as get-command), the limit parameter is extracted.
71
70
  def compose(destination: :batch)
72
71
  if kind.to_sym == :update
73
72
  return_statement = "return after " + ( @q[:aliases].empty? ? "$current" : @q[:aliases].first.to_s)
@@ -126,9 +125,8 @@ module Arcade
126
125
  end
127
126
  end
128
127
 
129
- =begin
130
- from can either be a Databaseclass to operate on or a Subquery providing data to query further
131
- =end
128
+ # -------------------- from ---------------------------------------
129
+ # arg can either be a Database class to operate on or a Subquery providing data to query further
132
130
  def from arg = nil
133
131
  if arg.present?
134
132
  @q[:database] = arg
@@ -315,11 +313,8 @@ end # class << self
315
313
  self
316
314
  end
317
315
 
318
- # # connects by adding {in_or_out}('edgeClass')
319
- # def connect_with in_or_out, via: nil
320
- # argument = " #{in_or_out}(#{via.to_or if via.present?})"
321
- # end
322
316
 
317
+ # ------------------ nodes ---------------------------------------
323
318
  # adds a connection
324
319
  # in_or_out: :out ---> out('edgeClass')[where-condition]
325
320
  # :in ---> in('edgeClass')[where-condition]
@@ -335,7 +330,7 @@ end # class << self
335
330
 
336
331
  def nodes in_or_out = :out, via: nil, where: nil, expand: false
337
332
 
338
- condition = where.present? ? "[ #{generate_sql_list(where)} ]" : ""
333
+ condition = where.present? ? "[ #{generate_sql_list(where)} ]" : ""
339
334
  via = resolve_edge_name(via) unless via.nil?
340
335
 
341
336
  argument = if in_or_out.to_s[-1] == 'E'
@@ -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
@@ -15,35 +15,31 @@ module Arcade
15
15
  end.join(',')
16
16
  end
17
17
 
18
-
19
18
  # used by array#allocate_model
20
19
  def _allocate_model response=nil, auto = Config.autoload
21
20
 
22
-
23
21
  if response.is_a? Hash
24
22
  # save rid to a safe place
25
- temp_rid = response.delete :"@rid"
26
- type = response.delete :"@type"
27
- cat = response.delete :"@cat"
23
+ temp_rid = response.delete( :"@rid" ) || response.delete( :rid ) || "#0:0"
24
+ type = response.delete( :"@type" ) || response.delete( :type ) || nil
25
+ cat = response.delete( :"@cat" ) || response.delete( :cat ) || "d"
28
26
 
29
27
  return response if type.nil?
30
- temp_rid = "#0:0" if temp_rid.nil?
31
- type = "d" if type.nil?
32
28
  # extract type infos and convert to database-name
33
29
  namespace, type_name = type.camelcase_and_namespace
34
30
  namespace = self.namespace if namespace.nil?
35
31
  # autoconvert rid's in attributes to model-records (exclude edges!)
36
32
  if auto && !(cat.to_s =='e')
37
- response.transform_values! do |x|
33
+ response.transform_values do |x|
38
34
  case x
39
35
  when String
40
36
  x.rid? ? x.load_rid : x # follow links
41
37
  when Array
42
38
  a =[]
43
- x.map do| y |
44
- # Thread.new do ## thread or fiber decrease the performance significantly.
39
+ x.map do | y |
40
+ # Thread.new do ## using thread or fiber decreases the performance significantly.
45
41
  if y.is_a?(Hash) && y.include?(:@type) # if embedded documents are present, load them
46
- y.allocate_model(false)
42
+ _allocate_model(y,false)
47
43
  elsif y.rid? # if links are present, load the object
48
44
  y.load_rid(false) # do not autoload further records, prevents from recursive locking
49
45
  else
@@ -64,7 +60,18 @@ module Arcade
64
60
  end
65
61
  end
66
62
  # choose the appropriate class
67
- 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
68
75
  #
69
76
  begin
70
77
  # create a new object of that class with the appropriate attributes
@@ -82,7 +89,7 @@ module Arcade
82
89
  elsif response.rid?
83
90
  # Autoload rid's
84
91
  response.load_rid
85
- # auto ? response.load_rid : response
92
+ # auto ? response.load_rid : response
86
93
  else
87
94
  response
88
95
  end
@@ -44,7 +44,7 @@ If »NULL« should be addressed, { key: nil } is translated to "key = NULL" (us
44
44
  attributes.map do |key, value|
45
45
  case value
46
46
  when nil
47
- "#{key}=NULL"
47
+ "#{key} is NULL"
48
48
  when ::Array
49
49
  if value == [nil]
50
50
  "#{key} is NULL"
@@ -11,9 +11,12 @@ module Arcade
11
11
  delimiters = Regexp.union(['-', '_'])
12
12
  n,c= self.split(delimiters).then { |first, *rest| [first.tap {|s| s[0] = s[0].upcase}, rest.map(&:capitalize).join] }
13
13
  ## if base is the first part of the type-name, probably Arcade::Base is choosen a namespace. thats wrong
14
- namespace_present = unless n == 'Base'
14
+ # Arcade::Base descendants are prefetched in Init.models
15
+ # They have to be fetched to load both `Arcade::Ge` and `Arcade::GeSomething`
16
+ # (otherwise: Arcade::Ge and Arcade::Ge::Something)
17
+ namespace_present = unless n == 'Base' || Arcade::Init.models.include?(n)
15
18
  Object.const_get(n) rescue false # Database.namespace
16
- else
19
+ else
17
20
  false
18
21
  end
19
22
  # if no namespace is found, leave it empty and return the capitalized string as class name
@@ -22,6 +25,10 @@ module Arcade
22
25
  [ Database.namespace, self.capitalize ]
23
26
  end
24
27
  end
28
+ def camelcase
29
+ # Split the string into words, capitalize each word, and join them together
30
+ self.split('_').map(&:capitalize).join
31
+ end
25
32
 
26
33
  def snake_case
27
34
  n= if split('::').first == Database.namespace.to_s
@@ -51,7 +58,7 @@ module Arcade
51
58
  end
52
59
  # return a valid rid (format: "nn:mm") or nil
53
60
  def rid
54
- self["#"].nil? ? "#"+ self : self if rid?
61
+ self["#"].nil? ? "#"+ self : self if rid?
55
62
  end
56
63
 
57
64
  def where **args
@@ -80,7 +87,7 @@ module Arcade
80
87
  ## Load the database object if the string is a rid
81
88
  # (Default: No Autoloading of rid-links)
82
89
  def load_rid autocomplete = false
83
- db.get( self ){ autocomplete } if rid? rescue nil
90
+ db.get( self.strip ){ autocomplete } if rid? rescue nil
84
91
  end
85
92
 
86
93
  # updates the record ### retired in favour of Arcade::Base.update
@@ -104,13 +111,3 @@ module Arcade
104
111
  end
105
112
 
106
113
  String.include Arcade::Support::String
107
-
108
- module Types
109
- include Dry.Types()
110
-
111
- # include in attribute definitions
112
- Rid = String.constrained( format: /\A[#]{1}[0-9]{1,}:[0-9]{1,}\z/ )
113
- Blockchain = String.constrained( format: /^(algo|eth|btc)$/ ) # add other blockchain symbols here
114
- Email = String.constrained( format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i )
115
-
116
- end