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,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Unified client for vector database operations
5
+ #
6
+ # The Client class provides a unified interface to interact with various
7
+ # vector database providers. It automatically routes operations to the
8
+ # configured provider.
9
+ #
10
+ # @example Using global configuration
11
+ # Vectra.configure do |config|
12
+ # config.provider = :pinecone
13
+ # config.api_key = ENV['PINECONE_API_KEY']
14
+ # config.environment = 'us-east-1'
15
+ # end
16
+ #
17
+ # client = Vectra::Client.new
18
+ # client.upsert(index: 'my-index', vectors: [...])
19
+ #
20
+ # @example Using instance configuration
21
+ # client = Vectra::Client.new(
22
+ # provider: :pinecone,
23
+ # api_key: ENV['PINECONE_API_KEY'],
24
+ # environment: 'us-east-1'
25
+ # )
26
+ #
27
+ class Client
28
+ attr_reader :config, :provider
29
+
30
+ # Initialize a new Client
31
+ #
32
+ # @param provider [Symbol, nil] provider name (:pinecone, :qdrant, :weaviate)
33
+ # @param api_key [String, nil] API key
34
+ # @param environment [String, nil] environment/region
35
+ # @param host [String, nil] custom host URL
36
+ # @param options [Hash] additional options
37
+ def initialize(provider: nil, api_key: nil, environment: nil, host: nil, **options)
38
+ @config = build_config(provider, api_key, environment, host, options)
39
+ @config.validate!
40
+ @provider = build_provider
41
+ end
42
+
43
+ # Upsert vectors into an index
44
+ #
45
+ # @param index [String] the index/collection name
46
+ # @param vectors [Array<Hash, Vector>] vectors to upsert
47
+ # @param namespace [String, nil] optional namespace (provider-specific)
48
+ # @return [Hash] upsert response with :upserted_count
49
+ #
50
+ # @example Upsert vectors
51
+ # client.upsert(
52
+ # index: 'my-index',
53
+ # vectors: [
54
+ # { id: 'vec1', values: [0.1, 0.2, 0.3], metadata: { text: 'Hello' } },
55
+ # { id: 'vec2', values: [0.4, 0.5, 0.6], metadata: { text: 'World' } }
56
+ # ]
57
+ # )
58
+ #
59
+ def upsert(index:, vectors:, namespace: nil)
60
+ validate_index!(index)
61
+ validate_vectors!(vectors)
62
+
63
+ provider.upsert(index: index, vectors: vectors, namespace: namespace)
64
+ end
65
+
66
+ # Query vectors by similarity
67
+ #
68
+ # @param index [String] the index/collection name
69
+ # @param vector [Array<Float>] query vector
70
+ # @param top_k [Integer] number of results to return (default: 10)
71
+ # @param namespace [String, nil] optional namespace
72
+ # @param filter [Hash, nil] metadata filter
73
+ # @param include_values [Boolean] include vector values in response
74
+ # @param include_metadata [Boolean] include metadata in response
75
+ # @return [QueryResult] query results
76
+ #
77
+ # @example Simple query
78
+ # results = client.query(
79
+ # index: 'my-index',
80
+ # vector: [0.1, 0.2, 0.3],
81
+ # top_k: 5
82
+ # )
83
+ #
84
+ # @example Query with filter
85
+ # results = client.query(
86
+ # index: 'my-index',
87
+ # vector: [0.1, 0.2, 0.3],
88
+ # top_k: 10,
89
+ # filter: { category: 'programming' }
90
+ # )
91
+ #
92
+ def query(index:, vector:, top_k: 10, namespace: nil, filter: nil,
93
+ include_values: false, include_metadata: true)
94
+ validate_index!(index)
95
+ validate_query_vector!(vector)
96
+
97
+ provider.query(
98
+ index: index,
99
+ vector: vector,
100
+ top_k: top_k,
101
+ namespace: namespace,
102
+ filter: filter,
103
+ include_values: include_values,
104
+ include_metadata: include_metadata
105
+ )
106
+ end
107
+
108
+ # Fetch vectors by IDs
109
+ #
110
+ # @param index [String] the index/collection name
111
+ # @param ids [Array<String>] vector IDs to fetch
112
+ # @param namespace [String, nil] optional namespace
113
+ # @return [Hash<String, Vector>] hash of ID to Vector
114
+ #
115
+ # @example Fetch vectors
116
+ # vectors = client.fetch(index: 'my-index', ids: ['vec1', 'vec2'])
117
+ # vectors['vec1'].values # => [0.1, 0.2, 0.3]
118
+ #
119
+ def fetch(index:, ids:, namespace: nil)
120
+ validate_index!(index)
121
+ validate_ids!(ids)
122
+
123
+ provider.fetch(index: index, ids: ids, namespace: namespace)
124
+ end
125
+
126
+ # Update a vector's metadata or values
127
+ #
128
+ # @param index [String] the index/collection name
129
+ # @param id [String] vector ID
130
+ # @param metadata [Hash, nil] new metadata (merged with existing)
131
+ # @param values [Array<Float>, nil] new vector values
132
+ # @param namespace [String, nil] optional namespace
133
+ # @return [Hash] update response
134
+ #
135
+ # @example Update metadata
136
+ # client.update(
137
+ # index: 'my-index',
138
+ # id: 'vec1',
139
+ # metadata: { category: 'updated' }
140
+ # )
141
+ #
142
+ def update(index:, id:, metadata: nil, values: nil, namespace: nil)
143
+ validate_index!(index)
144
+ validate_id!(id)
145
+
146
+ raise ValidationError, "Must provide metadata or values to update" if metadata.nil? && values.nil?
147
+
148
+ provider.update(
149
+ index: index,
150
+ id: id,
151
+ metadata: metadata,
152
+ values: values,
153
+ namespace: namespace
154
+ )
155
+ end
156
+
157
+ # Delete vectors
158
+ #
159
+ # @param index [String] the index/collection name
160
+ # @param ids [Array<String>, nil] vector IDs to delete
161
+ # @param namespace [String, nil] optional namespace
162
+ # @param filter [Hash, nil] delete by metadata filter
163
+ # @param delete_all [Boolean] delete all vectors in namespace
164
+ # @return [Hash] delete response
165
+ #
166
+ # @example Delete by IDs
167
+ # client.delete(index: 'my-index', ids: ['vec1', 'vec2'])
168
+ #
169
+ # @example Delete by filter
170
+ # client.delete(index: 'my-index', filter: { category: 'old' })
171
+ #
172
+ # @example Delete all
173
+ # client.delete(index: 'my-index', delete_all: true)
174
+ #
175
+ def delete(index:, ids: nil, namespace: nil, filter: nil, delete_all: false)
176
+ validate_index!(index)
177
+
178
+ if ids.nil? && filter.nil? && !delete_all
179
+ raise ValidationError, "Must provide ids, filter, or delete_all"
180
+ end
181
+
182
+ provider.delete(
183
+ index: index,
184
+ ids: ids,
185
+ namespace: namespace,
186
+ filter: filter,
187
+ delete_all: delete_all
188
+ )
189
+ end
190
+
191
+ # List all indexes
192
+ #
193
+ # @return [Array<Hash>] list of index information
194
+ #
195
+ # @example
196
+ # indexes = client.list_indexes
197
+ # indexes.each { |idx| puts idx[:name] }
198
+ #
199
+ def list_indexes
200
+ provider.list_indexes
201
+ end
202
+
203
+ # Describe an index
204
+ #
205
+ # @param index [String] the index name
206
+ # @return [Hash] index details
207
+ #
208
+ # @example
209
+ # info = client.describe_index(index: 'my-index')
210
+ # puts info[:dimension]
211
+ #
212
+ def describe_index(index:)
213
+ validate_index!(index)
214
+ provider.describe_index(index: index)
215
+ end
216
+
217
+ # Get index statistics
218
+ #
219
+ # @param index [String] the index name
220
+ # @param namespace [String, nil] optional namespace
221
+ # @return [Hash] index statistics
222
+ #
223
+ # @example
224
+ # stats = client.stats(index: 'my-index')
225
+ # puts "Total vectors: #{stats[:total_vector_count]}"
226
+ #
227
+ def stats(index:, namespace: nil)
228
+ validate_index!(index)
229
+ provider.stats(index: index, namespace: namespace)
230
+ end
231
+
232
+ # Get the provider name
233
+ #
234
+ # @return [Symbol]
235
+ def provider_name
236
+ provider.provider_name
237
+ end
238
+
239
+ private
240
+
241
+ def build_config(provider_name, api_key, environment, host, options)
242
+ # Start with global config or new config
243
+ cfg = Vectra.configuration.dup
244
+
245
+ # Override with provided values
246
+ cfg.provider = provider_name if provider_name
247
+ cfg.api_key = api_key if api_key
248
+ cfg.environment = environment if environment
249
+ cfg.host = host if host
250
+ cfg.timeout = options[:timeout] if options[:timeout]
251
+ cfg.open_timeout = options[:open_timeout] if options[:open_timeout]
252
+ cfg.max_retries = options[:max_retries] if options[:max_retries]
253
+ cfg.retry_delay = options[:retry_delay] if options[:retry_delay]
254
+ cfg.logger = options[:logger] if options[:logger]
255
+
256
+ cfg
257
+ end
258
+
259
+ def build_provider
260
+ case config.provider
261
+ when :pinecone
262
+ Providers::Pinecone.new(config)
263
+ when :qdrant
264
+ Providers::Qdrant.new(config)
265
+ when :weaviate
266
+ Providers::Weaviate.new(config)
267
+ when :pgvector
268
+ Providers::Pgvector.new(config)
269
+ else
270
+ raise UnsupportedProviderError, "Provider '#{config.provider}' is not supported"
271
+ end
272
+ end
273
+
274
+ def validate_index!(index)
275
+ raise ValidationError, "Index name cannot be nil" if index.nil?
276
+ raise ValidationError, "Index name must be a string" unless index.is_a?(String)
277
+ raise ValidationError, "Index name cannot be empty" if index.empty?
278
+ end
279
+
280
+ def validate_vectors!(vectors)
281
+ raise ValidationError, "Vectors cannot be nil" if vectors.nil?
282
+ raise ValidationError, "Vectors must be an array" unless vectors.is_a?(Array)
283
+ raise ValidationError, "Vectors cannot be empty" if vectors.empty?
284
+ end
285
+
286
+ def validate_query_vector!(vector)
287
+ raise ValidationError, "Query vector cannot be nil" if vector.nil?
288
+ raise ValidationError, "Query vector must be an array" unless vector.is_a?(Array)
289
+ raise ValidationError, "Query vector cannot be empty" if vector.empty?
290
+ end
291
+
292
+ def validate_ids!(ids)
293
+ raise ValidationError, "IDs cannot be nil" if ids.nil?
294
+ raise ValidationError, "IDs must be an array" unless ids.is_a?(Array)
295
+ raise ValidationError, "IDs cannot be empty" if ids.empty?
296
+ end
297
+
298
+ def validate_id!(id)
299
+ raise ValidationError, "ID cannot be nil" if id.nil?
300
+ raise ValidationError, "ID must be a string" unless id.is_a?(String)
301
+ raise ValidationError, "ID cannot be empty" if id.empty?
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Configuration class for Vectra
5
+ #
6
+ # @example Configure Vectra globally
7
+ # Vectra.configure do |config|
8
+ # config.provider = :pinecone
9
+ # config.api_key = ENV['PINECONE_API_KEY']
10
+ # config.environment = 'us-east-1'
11
+ # end
12
+ #
13
+ class Configuration
14
+ SUPPORTED_PROVIDERS = %i[pinecone qdrant weaviate pgvector].freeze
15
+
16
+ attr_accessor :api_key, :environment, :host, :timeout, :open_timeout,
17
+ :max_retries, :retry_delay, :logger
18
+
19
+ attr_reader :provider
20
+
21
+ def initialize
22
+ @provider = nil
23
+ @api_key = nil
24
+ @environment = nil
25
+ @host = nil
26
+ @timeout = 30
27
+ @open_timeout = 10
28
+ @max_retries = 3
29
+ @retry_delay = 1
30
+ @logger = nil
31
+ end
32
+
33
+ # Set the provider
34
+ #
35
+ # @param value [Symbol, String] the provider name
36
+ # @raise [UnsupportedProviderError] if provider is not supported
37
+ def provider=(value)
38
+ provider_sym = value.to_sym
39
+
40
+ unless SUPPORTED_PROVIDERS.include?(provider_sym)
41
+ raise UnsupportedProviderError,
42
+ "Provider '#{value}' is not supported. " \
43
+ "Supported providers: #{SUPPORTED_PROVIDERS.join(', ')}"
44
+ end
45
+
46
+ @provider = provider_sym
47
+ end
48
+
49
+ # Validate the configuration
50
+ #
51
+ # @raise [ConfigurationError] if configuration is invalid
52
+ def validate!
53
+ raise ConfigurationError, "Provider must be configured" if provider.nil?
54
+ raise ConfigurationError, "API key must be configured" if api_key.nil? || api_key.empty?
55
+
56
+ validate_provider_specific!
57
+ end
58
+
59
+ # Check if configuration is valid
60
+ #
61
+ # @return [Boolean]
62
+ def valid?
63
+ validate!
64
+ true
65
+ rescue ConfigurationError
66
+ false
67
+ end
68
+
69
+ # Create a duplicate configuration
70
+ #
71
+ # @return [Configuration]
72
+ def dup
73
+ config = Configuration.new
74
+ config.instance_variable_set(:@provider, @provider)
75
+ config.api_key = @api_key
76
+ config.environment = @environment
77
+ config.host = @host
78
+ config.timeout = @timeout
79
+ config.open_timeout = @open_timeout
80
+ config.max_retries = @max_retries
81
+ config.retry_delay = @retry_delay
82
+ config.logger = @logger
83
+ config
84
+ end
85
+
86
+ # Convert configuration to hash
87
+ #
88
+ # @return [Hash]
89
+ def to_h
90
+ {
91
+ provider: provider,
92
+ api_key: api_key,
93
+ environment: environment,
94
+ host: host,
95
+ timeout: timeout,
96
+ open_timeout: open_timeout,
97
+ max_retries: max_retries,
98
+ retry_delay: retry_delay
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ def validate_provider_specific!
105
+ case provider
106
+ when :pinecone
107
+ validate_pinecone!
108
+ when :qdrant
109
+ validate_qdrant!
110
+ when :weaviate
111
+ validate_weaviate!
112
+ when :pgvector
113
+ validate_pgvector!
114
+ end
115
+ end
116
+
117
+ def validate_pinecone!
118
+ return unless environment.nil? && host.nil?
119
+
120
+ raise ConfigurationError,
121
+ "Pinecone requires either 'environment' or 'host' to be configured"
122
+ end
123
+
124
+ def validate_qdrant!
125
+ return unless host.nil?
126
+
127
+ raise ConfigurationError, "Qdrant requires 'host' to be configured"
128
+ end
129
+
130
+ def validate_weaviate!
131
+ return unless host.nil?
132
+
133
+ raise ConfigurationError, "Weaviate requires 'host' to be configured"
134
+ end
135
+
136
+ def validate_pgvector!
137
+ return unless host.nil?
138
+
139
+ raise ConfigurationError, "pgvector requires 'host' (connection URL or hostname) to be configured"
140
+ end
141
+ end
142
+
143
+ class << self
144
+ attr_writer :configuration
145
+
146
+ # Get the current configuration
147
+ #
148
+ # @return [Configuration]
149
+ def configuration
150
+ @configuration ||= Configuration.new
151
+ end
152
+
153
+ # Configure Vectra
154
+ #
155
+ # @yield [Configuration] the configuration object
156
+ # @return [Configuration]
157
+ def configure
158
+ yield(configuration)
159
+ configuration
160
+ end
161
+
162
+ # Reset configuration to defaults
163
+ #
164
+ # @return [Configuration]
165
+ def reset_configuration!
166
+ @configuration = Configuration.new
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Base error class for all Vectra errors
5
+ class Error < StandardError
6
+ attr_reader :original_error, :response
7
+
8
+ def initialize(message = nil, original_error: nil, response: nil)
9
+ @original_error = original_error
10
+ @response = response
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ # Raised when configuration is invalid or missing
16
+ class ConfigurationError < Error; end
17
+
18
+ # Raised when authentication fails
19
+ class AuthenticationError < Error; end
20
+
21
+ # Raised when a resource is not found
22
+ class NotFoundError < Error; end
23
+
24
+ # Raised when rate limit is exceeded
25
+ class RateLimitError < Error
26
+ attr_reader :retry_after
27
+
28
+ def initialize(message = nil, retry_after: nil, **kwargs)
29
+ @retry_after = retry_after
30
+ super(message, **kwargs)
31
+ end
32
+ end
33
+
34
+ # Raised when request validation fails
35
+ class ValidationError < Error
36
+ attr_reader :errors
37
+
38
+ def initialize(message = nil, errors: [], **kwargs)
39
+ @errors = errors
40
+ super(message, **kwargs)
41
+ end
42
+ end
43
+
44
+ # Raised when there's a connection problem
45
+ class ConnectionError < Error; end
46
+
47
+ # Raised when the server returns an error
48
+ class ServerError < Error
49
+ attr_reader :status_code
50
+
51
+ def initialize(message = nil, status_code: nil, **kwargs)
52
+ @status_code = status_code
53
+ super(message, **kwargs)
54
+ end
55
+ end
56
+
57
+ # Raised when the provider is not supported
58
+ class UnsupportedProviderError < Error; end
59
+
60
+ # Raised when an operation times out
61
+ class TimeoutError < Error; end
62
+
63
+ # Raised when batch operation partially fails
64
+ class BatchError < Error
65
+ attr_reader :succeeded, :failed
66
+
67
+ def initialize(message = nil, succeeded: [], failed: [], **kwargs)
68
+ @succeeded = succeeded
69
+ @failed = failed
70
+ super(message, **kwargs)
71
+ end
72
+ end
73
+ end