vectra-client 0.1.2

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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Represents results from a vector query
5
+ #
6
+ # @example Working with query results
7
+ # results = client.query(index: 'my-index', vector: [0.1, 0.2], top_k: 5)
8
+ # results.each do |match|
9
+ # puts "ID: #{match.id}, Score: #{match.score}"
10
+ # end
11
+ #
12
+ class QueryResult
13
+ include Enumerable
14
+
15
+ attr_reader :matches, :namespace, :usage
16
+
17
+ # Initialize a new QueryResult
18
+ #
19
+ # @param matches [Array<Match>] array of match results
20
+ # @param namespace [String, nil] the namespace queried
21
+ # @param usage [Hash, nil] usage statistics from the provider
22
+ def initialize(matches: [], namespace: nil, usage: nil)
23
+ @matches = matches.map { |m| m.is_a?(Match) ? m : Match.new(**m.transform_keys(&:to_sym)) }
24
+ @namespace = namespace
25
+ @usage = usage
26
+ end
27
+
28
+ # Iterate over matches
29
+ #
30
+ # @yield [Match] each match
31
+ def each(&)
32
+ matches.each(&)
33
+ end
34
+
35
+ # Get the number of matches
36
+ #
37
+ # @return [Integer]
38
+ def size
39
+ matches.size
40
+ end
41
+
42
+ alias length size
43
+ alias count size
44
+
45
+ # Check if there are no matches
46
+ #
47
+ # @return [Boolean]
48
+ def empty?
49
+ matches.empty?
50
+ end
51
+
52
+ # Get the first match
53
+ #
54
+ # @return [Match, nil]
55
+ def first
56
+ matches.first
57
+ end
58
+
59
+ # Get the last match
60
+ #
61
+ # @return [Match, nil]
62
+ def last
63
+ matches.last
64
+ end
65
+
66
+ # Get match by index
67
+ #
68
+ # @param index [Integer]
69
+ # @return [Match, nil]
70
+ def [](index)
71
+ matches[index]
72
+ end
73
+
74
+ # Get all vector IDs
75
+ #
76
+ # @return [Array<String>]
77
+ def ids
78
+ matches.map(&:id)
79
+ end
80
+
81
+ # Get all scores
82
+ #
83
+ # @return [Array<Float>]
84
+ def scores
85
+ matches.map(&:score)
86
+ end
87
+
88
+ # Get the highest score
89
+ #
90
+ # @return [Float, nil]
91
+ def max_score
92
+ scores.max
93
+ end
94
+
95
+ # Get the lowest score
96
+ #
97
+ # @return [Float, nil]
98
+ def min_score
99
+ scores.min
100
+ end
101
+
102
+ # Filter matches by minimum score
103
+ #
104
+ # @param min_score [Float] minimum score threshold
105
+ # @return [QueryResult] new result with filtered matches
106
+ def above_score(min_score)
107
+ filtered = matches.select { |m| m.score >= min_score }
108
+ QueryResult.new(matches: filtered, namespace: namespace, usage: usage)
109
+ end
110
+
111
+ # Convert to array of hashes
112
+ #
113
+ # @return [Array<Hash>]
114
+ def to_a
115
+ matches.map(&:to_h)
116
+ end
117
+
118
+ # Convert to hash
119
+ #
120
+ # @return [Hash]
121
+ def to_h
122
+ {
123
+ matches: to_a,
124
+ namespace: namespace,
125
+ usage: usage
126
+ }.compact
127
+ end
128
+
129
+ # Create QueryResult from provider response
130
+ #
131
+ # @param data [Hash] raw response data
132
+ # @return [QueryResult]
133
+ def self.from_response(data)
134
+ data = data.transform_keys(&:to_sym)
135
+ matches = (data[:matches] || []).map do |match|
136
+ Match.from_hash(match)
137
+ end
138
+
139
+ new(
140
+ matches: matches,
141
+ namespace: data[:namespace],
142
+ usage: data[:usage]
143
+ )
144
+ end
145
+
146
+ # String representation
147
+ #
148
+ # @return [String]
149
+ def to_s
150
+ "#<Vectra::QueryResult matches=#{size} namespace=#{namespace.inspect}>"
151
+ end
152
+
153
+ alias inspect to_s
154
+ end
155
+
156
+ # Represents a single match from a query
157
+ class Match
158
+ attr_reader :id, :score, :values, :metadata, :sparse_values
159
+
160
+ # Initialize a new Match
161
+ #
162
+ # @param id [String] vector ID
163
+ # @param score [Float] similarity score
164
+ # @param values [Array<Float>, nil] vector values (if requested)
165
+ # @param metadata [Hash, nil] vector metadata (if requested)
166
+ # @param sparse_values [Hash, nil] sparse vector values
167
+ def initialize(id:, score:, values: nil, metadata: nil, sparse_values: nil)
168
+ @id = id.to_s
169
+ @score = score.to_f
170
+ @values = values
171
+ @metadata = metadata&.transform_keys(&:to_s) || {}
172
+ @sparse_values = sparse_values
173
+ end
174
+
175
+ # Check if values are included
176
+ #
177
+ # @return [Boolean]
178
+ def values?
179
+ !values.nil?
180
+ end
181
+
182
+ # Check if metadata is included
183
+ #
184
+ # @return [Boolean]
185
+ def metadata?
186
+ !metadata.empty?
187
+ end
188
+
189
+ # Get metadata value by key
190
+ #
191
+ # @param key [String, Symbol] metadata key
192
+ # @return [Object, nil]
193
+ def [](key)
194
+ metadata[key.to_s]
195
+ end
196
+
197
+ # Convert to Vector object
198
+ #
199
+ # @return [Vector]
200
+ # @raise [Error] if values are not included
201
+ def to_vector
202
+ raise Error, "Vector values not included in query result" unless values?
203
+
204
+ Vector.new(
205
+ id: id,
206
+ values: values,
207
+ metadata: metadata,
208
+ sparse_values: sparse_values
209
+ )
210
+ end
211
+
212
+ # Convert to hash
213
+ #
214
+ # @return [Hash]
215
+ def to_h
216
+ hash = { id: id, score: score }
217
+ hash[:values] = values if values?
218
+ hash[:metadata] = metadata if metadata?
219
+ hash[:sparse_values] = sparse_values if sparse_values
220
+ hash
221
+ end
222
+
223
+ # Create Match from hash
224
+ #
225
+ # @param hash [Hash]
226
+ # @return [Match]
227
+ def self.from_hash(hash)
228
+ hash = hash.transform_keys(&:to_sym)
229
+ new(
230
+ id: hash[:id],
231
+ score: hash[:score],
232
+ values: hash[:values],
233
+ metadata: hash[:metadata],
234
+ sparse_values: hash[:sparse_values]
235
+ )
236
+ end
237
+
238
+ # Check equality
239
+ #
240
+ # @param other [Match]
241
+ # @return [Boolean]
242
+ def ==(other)
243
+ return false unless other.is_a?(Match)
244
+
245
+ id == other.id && score == other.score
246
+ end
247
+
248
+ # String representation
249
+ #
250
+ # @return [String]
251
+ def to_s
252
+ "#<Vectra::Match id=#{id.inspect} score=#{score.round(4)}>"
253
+ end
254
+
255
+ alias inspect to_s
256
+ end
257
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Represents a vector with its associated data
5
+ #
6
+ # @example Create a vector
7
+ # vector = Vectra::Vector.new(
8
+ # id: 'vec1',
9
+ # values: [0.1, 0.2, 0.3],
10
+ # metadata: { text: 'Hello world', category: 'greeting' }
11
+ # )
12
+ #
13
+ class Vector
14
+ attr_reader :id, :values, :metadata, :sparse_values
15
+
16
+ # Initialize a new Vector
17
+ #
18
+ # @param id [String] unique identifier for the vector
19
+ # @param values [Array<Float>] the vector embedding values
20
+ # @param metadata [Hash] optional metadata associated with the vector
21
+ # @param sparse_values [Hash] optional sparse vector values
22
+ def initialize(id:, values:, metadata: nil, sparse_values: nil)
23
+ @id = validate_id!(id)
24
+ @values = validate_values!(values)
25
+ @metadata = metadata&.transform_keys(&:to_s) || {}
26
+ @sparse_values = sparse_values
27
+ end
28
+
29
+ # Get the dimension of the vector
30
+ #
31
+ # @return [Integer]
32
+ def dimension
33
+ values.length
34
+ end
35
+
36
+ # Check if vector has metadata
37
+ #
38
+ # @return [Boolean]
39
+ def metadata?
40
+ !metadata.empty?
41
+ end
42
+
43
+ # Check if vector has sparse values
44
+ #
45
+ # @return [Boolean]
46
+ def sparse?
47
+ !sparse_values.nil? && !sparse_values.empty?
48
+ end
49
+
50
+ # Convert vector to hash for API requests
51
+ #
52
+ # @return [Hash]
53
+ def to_h
54
+ hash = {
55
+ id: id,
56
+ values: values
57
+ }
58
+ hash[:metadata] = metadata unless metadata.empty?
59
+ hash[:sparse_values] = sparse_values if sparse?
60
+ hash
61
+ end
62
+
63
+ alias to_hash to_h
64
+
65
+ # Create a Vector from a hash
66
+ #
67
+ # @param hash [Hash] hash containing vector data
68
+ # @return [Vector]
69
+ def self.from_hash(hash)
70
+ hash = hash.transform_keys(&:to_sym)
71
+ new(
72
+ id: hash[:id],
73
+ values: hash[:values],
74
+ metadata: hash[:metadata],
75
+ sparse_values: hash[:sparse_values]
76
+ )
77
+ end
78
+
79
+ # Calculate cosine similarity with another vector
80
+ #
81
+ # @param other [Vector, Array<Float>] the other vector
82
+ # @return [Float] similarity score between -1 and 1
83
+ def cosine_similarity(other)
84
+ other_values = other.is_a?(Vector) ? other.values : other
85
+
86
+ raise ArgumentError, "Vectors must have the same dimension" if values.length != other_values.length
87
+
88
+ dot_product = values.zip(other_values).sum { |a, b| a * b }
89
+ magnitude_a = Math.sqrt(values.sum { |v| v**2 })
90
+ magnitude_b = Math.sqrt(other_values.sum { |v| v**2 })
91
+
92
+ return 0.0 if magnitude_a.zero? || magnitude_b.zero?
93
+
94
+ dot_product / (magnitude_a * magnitude_b)
95
+ end
96
+
97
+ # Calculate Euclidean distance to another vector
98
+ #
99
+ # @param other [Vector, Array<Float>] the other vector
100
+ # @return [Float] distance (0 = identical)
101
+ def euclidean_distance(other)
102
+ other_values = other.is_a?(Vector) ? other.values : other
103
+
104
+ raise ArgumentError, "Vectors must have the same dimension" if values.length != other_values.length
105
+
106
+ Math.sqrt(values.zip(other_values).sum { |a, b| (a - b)**2 })
107
+ end
108
+
109
+ # Check equality with another vector
110
+ #
111
+ # @param other [Vector] the other vector
112
+ # @return [Boolean]
113
+ def ==(other)
114
+ return false unless other.is_a?(Vector)
115
+
116
+ id == other.id && values == other.values && metadata == other.metadata
117
+ end
118
+
119
+ alias eql? ==
120
+
121
+ def hash
122
+ [id, values, metadata].hash
123
+ end
124
+
125
+ # String representation
126
+ #
127
+ # @return [String]
128
+ def to_s
129
+ "#<Vectra::Vector id=#{id.inspect} dimension=#{dimension} metadata_keys=#{metadata.keys}>"
130
+ end
131
+
132
+ alias inspect to_s
133
+
134
+ private
135
+
136
+ def validate_id!(id)
137
+ raise ValidationError, "Vector ID cannot be nil" if id.nil?
138
+ raise ValidationError, "Vector ID must be a string" unless id.is_a?(String) || id.is_a?(Symbol)
139
+
140
+ id.to_s
141
+ end
142
+
143
+ def validate_values!(values)
144
+ raise ValidationError, "Vector values cannot be nil" if values.nil?
145
+ raise ValidationError, "Vector values must be an array" unless values.is_a?(Array)
146
+ raise ValidationError, "Vector values cannot be empty" if values.empty?
147
+
148
+ unless values.all? { |v| v.is_a?(Numeric) }
149
+ raise ValidationError, "All vector values must be numeric"
150
+ end
151
+
152
+ values.map(&:to_f)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ VERSION = "0.1.2"
5
+ end
data/lib/vectra.rb ADDED
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vectra/version"
4
+ require_relative "vectra/errors"
5
+ require_relative "vectra/configuration"
6
+ require_relative "vectra/vector"
7
+ require_relative "vectra/query_result"
8
+ require_relative "vectra/providers/base"
9
+ require_relative "vectra/providers/pinecone"
10
+ require_relative "vectra/providers/qdrant"
11
+ require_relative "vectra/providers/weaviate"
12
+ require_relative "vectra/providers/pgvector"
13
+ require_relative "vectra/client"
14
+
15
+ # Vectra - Unified Ruby client for vector databases
16
+ #
17
+ # Vectra provides a simple, unified interface to work with multiple
18
+ # vector database providers including Pinecone, Qdrant, and Weaviate.
19
+ #
20
+ # @example Basic usage
21
+ # # Configure globally
22
+ # Vectra.configure do |config|
23
+ # config.provider = :pinecone
24
+ # config.api_key = ENV['PINECONE_API_KEY']
25
+ # config.environment = 'us-east-1'
26
+ # end
27
+ #
28
+ # # Create a client
29
+ # client = Vectra::Client.new
30
+ #
31
+ # # Upsert vectors
32
+ # client.upsert(
33
+ # index: 'my-index',
34
+ # vectors: [
35
+ # { id: 'vec1', values: [0.1, 0.2, 0.3], metadata: { text: 'Hello' } }
36
+ # ]
37
+ # )
38
+ #
39
+ # # Query vectors
40
+ # results = client.query(
41
+ # index: 'my-index',
42
+ # vector: [0.1, 0.2, 0.3],
43
+ # top_k: 5
44
+ # )
45
+ #
46
+ # # Process results
47
+ # results.each do |match|
48
+ # puts "ID: #{match.id}, Score: #{match.score}"
49
+ # end
50
+ #
51
+ # @see Vectra::Client
52
+ # @see Vectra::Configuration
53
+ #
54
+ module Vectra
55
+ class << self
56
+ # Create a new client with the given options
57
+ #
58
+ # @param options [Hash] client options
59
+ # @return [Client]
60
+ def client(**options)
61
+ Client.new(**options)
62
+ end
63
+
64
+ # Shortcut to create a Pinecone client
65
+ #
66
+ # @param api_key [String] Pinecone API key
67
+ # @param environment [String] Pinecone environment
68
+ # @param options [Hash] additional options
69
+ # @return [Client]
70
+ def pinecone(api_key:, environment: nil, host: nil, **options)
71
+ Client.new(
72
+ provider: :pinecone,
73
+ api_key: api_key,
74
+ environment: environment,
75
+ host: host,
76
+ **options
77
+ )
78
+ end
79
+
80
+ # Shortcut to create a Qdrant client
81
+ #
82
+ # @param api_key [String] Qdrant API key
83
+ # @param host [String] Qdrant host URL
84
+ # @param options [Hash] additional options
85
+ # @return [Client]
86
+ def qdrant(api_key:, host:, **options)
87
+ Client.new(
88
+ provider: :qdrant,
89
+ api_key: api_key,
90
+ host: host,
91
+ **options
92
+ )
93
+ end
94
+
95
+ # Shortcut to create a Weaviate client
96
+ #
97
+ # @param api_key [String] Weaviate API key
98
+ # @param host [String] Weaviate host URL
99
+ # @param options [Hash] additional options
100
+ # @return [Client]
101
+ def weaviate(api_key:, host:, **options)
102
+ Client.new(
103
+ provider: :weaviate,
104
+ api_key: api_key,
105
+ host: host,
106
+ **options
107
+ )
108
+ end
109
+
110
+ # Shortcut to create a pgvector client
111
+ #
112
+ # @param connection_url [String] PostgreSQL connection URL (postgres://user:pass@host/db)
113
+ # @param host [String] PostgreSQL host (alternative to connection_url)
114
+ # @param password [String] PostgreSQL password (used with host)
115
+ # @param options [Hash] additional options
116
+ # @return [Client]
117
+ #
118
+ # @example With connection URL
119
+ # Vectra.pgvector(connection_url: "postgres://user:pass@localhost/mydb")
120
+ #
121
+ # @example With host and password
122
+ # Vectra.pgvector(host: "localhost", password: "secret")
123
+ #
124
+ def pgvector(connection_url: nil, host: nil, password: nil, **options)
125
+ Client.new(
126
+ provider: :pgvector,
127
+ api_key: password,
128
+ host: connection_url || host,
129
+ **options
130
+ )
131
+ end
132
+ end
133
+ end