vectra-client 0.1.3 → 0.2.0
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/.rubocop.yml +23 -3
- data/CHANGELOG.md +23 -0
- data/IMPLEMENTATION_GUIDE.md +686 -0
- data/NEW_FEATURES_v0.2.0.md +459 -0
- data/RELEASE_CHECKLIST_v0.2.0.md +383 -0
- data/Rakefile +12 -0
- data/USAGE_EXAMPLES.md +787 -0
- data/benchmarks/batch_operations_benchmark.rb +117 -0
- data/benchmarks/connection_pooling_benchmark.rb +93 -0
- data/examples/active_record_demo.rb +227 -0
- data/examples/instrumentation_demo.rb +157 -0
- data/lib/generators/vectra/install_generator.rb +115 -0
- data/lib/generators/vectra/templates/enable_pgvector_extension.rb +11 -0
- data/lib/generators/vectra/templates/vectra.rb +79 -0
- data/lib/vectra/active_record.rb +195 -0
- data/lib/vectra/client.rb +60 -22
- data/lib/vectra/configuration.rb +6 -1
- data/lib/vectra/instrumentation/datadog.rb +82 -0
- data/lib/vectra/instrumentation/new_relic.rb +70 -0
- data/lib/vectra/instrumentation.rb +143 -0
- data/lib/vectra/providers/pgvector/connection.rb +5 -1
- data/lib/vectra/retry.rb +156 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +11 -0
- metadata +45 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "vectra"
|
|
5
|
+
require "benchmark"
|
|
6
|
+
|
|
7
|
+
# Benchmark for batch operations
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ruby benchmarks/batch_operations_benchmark.rb
|
|
11
|
+
|
|
12
|
+
puts "=" * 80
|
|
13
|
+
puts "VECTRA BATCH OPERATIONS BENCHMARK"
|
|
14
|
+
puts "=" * 80
|
|
15
|
+
puts
|
|
16
|
+
|
|
17
|
+
# Setup
|
|
18
|
+
DB_URL = ENV.fetch("DATABASE_URL", "postgres://postgres:password@localhost/vectra_benchmark")
|
|
19
|
+
DIMENSION = 384
|
|
20
|
+
ITERATIONS = 5
|
|
21
|
+
|
|
22
|
+
client = Vectra.pgvector(connection_url: DB_URL)
|
|
23
|
+
|
|
24
|
+
# Create test index
|
|
25
|
+
puts "Creating test index..."
|
|
26
|
+
begin
|
|
27
|
+
client.provider.delete_index(name: "benchmark_test")
|
|
28
|
+
rescue Vectra::NotFoundError
|
|
29
|
+
# Index doesn't exist, that's fine
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
client.provider.create_index(
|
|
33
|
+
name: "benchmark_test",
|
|
34
|
+
dimension: DIMENSION,
|
|
35
|
+
metric: "cosine"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Generate test vectors
|
|
39
|
+
def generate_vectors(count, dimension)
|
|
40
|
+
count.times.map do |i|
|
|
41
|
+
{
|
|
42
|
+
id: "vec_#{i}",
|
|
43
|
+
values: Array.new(dimension) { rand },
|
|
44
|
+
metadata: { index: i, category: "cat_#{i % 10}" }
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
puts "\nRunning benchmarks (#{ITERATIONS} iterations each)..."
|
|
50
|
+
puts "-" * 80
|
|
51
|
+
|
|
52
|
+
# Test different vector counts
|
|
53
|
+
[100, 500, 1000, 5000, 10_000].each do |count|
|
|
54
|
+
puts "\n#{count} vectors:"
|
|
55
|
+
|
|
56
|
+
vectors = generate_vectors(count, DIMENSION)
|
|
57
|
+
|
|
58
|
+
# Test different batch sizes
|
|
59
|
+
[50, 100, 250, 500].each do |batch_size|
|
|
60
|
+
next if batch_size > count
|
|
61
|
+
|
|
62
|
+
client.config.batch_size = batch_size
|
|
63
|
+
|
|
64
|
+
times = []
|
|
65
|
+
ITERATIONS.times do
|
|
66
|
+
time = Benchmark.realtime do
|
|
67
|
+
client.upsert(index: "benchmark_test", vectors: vectors)
|
|
68
|
+
end
|
|
69
|
+
times << time
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
avg_time = times.sum / times.size
|
|
73
|
+
vectors_per_sec = count / avg_time
|
|
74
|
+
batches = (count.to_f / batch_size).ceil
|
|
75
|
+
|
|
76
|
+
puts " Batch size #{batch_size.to_s.rjust(3)}: " \
|
|
77
|
+
"#{avg_time.round(3)}s avg " \
|
|
78
|
+
"(#{vectors_per_sec.round(0)} vectors/sec, " \
|
|
79
|
+
"#{batches} batches)"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Query benchmarks
|
|
84
|
+
puts "\n#{"=" * 80}"
|
|
85
|
+
puts "QUERY BENCHMARKS"
|
|
86
|
+
puts "=" * 80
|
|
87
|
+
|
|
88
|
+
query_vector = Array.new(DIMENSION) { rand }
|
|
89
|
+
|
|
90
|
+
puts "\nQuery performance (#{ITERATIONS} iterations):"
|
|
91
|
+
[10, 20, 50, 100].each do |top_k|
|
|
92
|
+
times = []
|
|
93
|
+
ITERATIONS.times do
|
|
94
|
+
time = Benchmark.realtime do
|
|
95
|
+
client.query(
|
|
96
|
+
index: "benchmark_test",
|
|
97
|
+
vector: query_vector,
|
|
98
|
+
top_k: top_k
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
times << time
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
avg_time = times.sum / times.size
|
|
105
|
+
queries_per_sec = 1 / avg_time
|
|
106
|
+
|
|
107
|
+
puts " top_k=#{top_k.to_s.rjust(3)}: " \
|
|
108
|
+
"#{(avg_time * 1000).round(1)}ms avg " \
|
|
109
|
+
"(#{queries_per_sec.round(1)} queries/sec)"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Cleanup
|
|
113
|
+
puts "\nCleaning up..."
|
|
114
|
+
client.provider.delete_index(name: "benchmark_test")
|
|
115
|
+
client.provider.shutdown!
|
|
116
|
+
|
|
117
|
+
puts "\n✅ Benchmark complete!"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "vectra"
|
|
5
|
+
require "benchmark"
|
|
6
|
+
|
|
7
|
+
# Benchmark for connection pooling under concurrent load
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ruby benchmarks/connection_pooling_benchmark.rb
|
|
11
|
+
|
|
12
|
+
puts "=" * 80
|
|
13
|
+
puts "VECTRA CONNECTION POOLING BENCHMARK"
|
|
14
|
+
puts "=" * 80
|
|
15
|
+
puts
|
|
16
|
+
|
|
17
|
+
DB_URL = ENV.fetch("DATABASE_URL", "postgres://postgres:password@localhost/vectra_benchmark")
|
|
18
|
+
DIMENSION = 384
|
|
19
|
+
THREAD_COUNTS = [1, 2, 5, 10, 20].freeze
|
|
20
|
+
OPERATIONS_PER_THREAD = 50
|
|
21
|
+
|
|
22
|
+
# Test different pool sizes
|
|
23
|
+
[5, 10, 20].each do |pool_size|
|
|
24
|
+
puts "\n#{"=" * 80}"
|
|
25
|
+
puts "Pool Size: #{pool_size}"
|
|
26
|
+
puts "=" * 80
|
|
27
|
+
|
|
28
|
+
client = Vectra.pgvector(
|
|
29
|
+
connection_url: DB_URL,
|
|
30
|
+
pool_size: pool_size,
|
|
31
|
+
pool_timeout: 10
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Create test index
|
|
35
|
+
begin
|
|
36
|
+
client.provider.create_index(name: "benchmark_pool", dimension: DIMENSION)
|
|
37
|
+
rescue StandardError
|
|
38
|
+
# Already exists
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Pre-populate some data
|
|
42
|
+
vectors = 100.times.map do |i|
|
|
43
|
+
{
|
|
44
|
+
id: "vec_#{i}",
|
|
45
|
+
values: Array.new(DIMENSION) { rand },
|
|
46
|
+
metadata: { index: i }
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
client.upsert(index: "benchmark_pool", vectors: vectors)
|
|
50
|
+
|
|
51
|
+
THREAD_COUNTS.each do |thread_count|
|
|
52
|
+
# Skip if threads > pool size (will timeout)
|
|
53
|
+
next if thread_count > pool_size + 5
|
|
54
|
+
|
|
55
|
+
total_time = Benchmark.realtime do
|
|
56
|
+
threads = thread_count.times.map do |_thread_idx|
|
|
57
|
+
Thread.new do
|
|
58
|
+
query_vector = Array.new(DIMENSION) { rand }
|
|
59
|
+
|
|
60
|
+
OPERATIONS_PER_THREAD.times do
|
|
61
|
+
client.query(
|
|
62
|
+
index: "benchmark_pool",
|
|
63
|
+
vector: query_vector,
|
|
64
|
+
top_k: 10
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
threads.each(&:join)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
total_operations = thread_count * OPERATIONS_PER_THREAD
|
|
74
|
+
ops_per_sec = total_operations / total_time
|
|
75
|
+
|
|
76
|
+
# Get pool stats
|
|
77
|
+
stats = client.provider.pool_stats
|
|
78
|
+
|
|
79
|
+
puts " #{thread_count.to_s.rjust(2)} threads: " \
|
|
80
|
+
"#{total_time.round(2)}s total " \
|
|
81
|
+
"(#{ops_per_sec.round(1)} ops/sec) " \
|
|
82
|
+
"Pool: #{stats[:available]}/#{stats[:size]} available"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Cleanup
|
|
86
|
+
client.provider.shutdown!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
puts "\n✅ Benchmark complete!"
|
|
90
|
+
puts "\nKey takeaways:"
|
|
91
|
+
puts " • Pool size should match max concurrent threads"
|
|
92
|
+
puts " • More threads than pool size causes waiting/timeouts"
|
|
93
|
+
puts " • Monitor pool_stats in production for optimal sizing"
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demo of Vectra ActiveRecord integration
|
|
5
|
+
#
|
|
6
|
+
# Usage: ruby examples/active_record_demo.rb
|
|
7
|
+
|
|
8
|
+
require "bundler/setup"
|
|
9
|
+
require "active_record"
|
|
10
|
+
require "vectra"
|
|
11
|
+
|
|
12
|
+
puts "=" * 80
|
|
13
|
+
puts "VECTRA ACTIVERECORD INTEGRATION DEMO"
|
|
14
|
+
puts "=" * 80
|
|
15
|
+
puts
|
|
16
|
+
|
|
17
|
+
# Setup database connection
|
|
18
|
+
ActiveRecord::Base.establish_connection(
|
|
19
|
+
adapter: "postgresql",
|
|
20
|
+
database: ENV.fetch("DATABASE_NAME", "vectra_demo"),
|
|
21
|
+
host: ENV.fetch("DATABASE_HOST", "localhost"),
|
|
22
|
+
username: ENV.fetch("DATABASE_USER", "postgres"),
|
|
23
|
+
password: ENV.fetch("DATABASE_PASSWORD", "password")
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Configure Vectra
|
|
27
|
+
Vectra.configure do |config|
|
|
28
|
+
config.provider = :pgvector
|
|
29
|
+
config.host = ENV.fetch("DATABASE_URL", "postgres://postgres:password@localhost/vectra_demo")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Create documents table if not exists
|
|
33
|
+
ActiveRecord::Schema.define do
|
|
34
|
+
unless ActiveRecord::Base.connection.table_exists?("documents")
|
|
35
|
+
enable_extension "vector"
|
|
36
|
+
|
|
37
|
+
create_table :documents do |t|
|
|
38
|
+
t.string :title
|
|
39
|
+
t.text :content
|
|
40
|
+
t.string :category
|
|
41
|
+
t.string :status
|
|
42
|
+
t.column :embedding, :vector, limit: 3 # 3-dimensional for demo
|
|
43
|
+
t.timestamps
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Define Document model with vector search
|
|
49
|
+
class Document < ActiveRecord::Base
|
|
50
|
+
include Vectra::ActiveRecord
|
|
51
|
+
|
|
52
|
+
has_vector :embedding,
|
|
53
|
+
dimension: 3,
|
|
54
|
+
provider: :pgvector,
|
|
55
|
+
index: "documents",
|
|
56
|
+
auto_index: true,
|
|
57
|
+
metadata_fields: [:title, :category, :status]
|
|
58
|
+
|
|
59
|
+
# Generate embedding before validation
|
|
60
|
+
# In production, use OpenAI/Cohere/etc.
|
|
61
|
+
before_validation :generate_embedding, if: -> { content.present? && embedding.nil? }
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def generate_embedding
|
|
66
|
+
# Simple deterministic embedding for demo
|
|
67
|
+
# In production: self.embedding = OpenAI.embed(content)
|
|
68
|
+
hash = content.hash.abs
|
|
69
|
+
self.embedding = [
|
|
70
|
+
(hash % 1000) / 1000.0,
|
|
71
|
+
((hash / 1000) % 1000) / 1000.0,
|
|
72
|
+
((hash / 1_000_000) % 1000) / 1000.0
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create pgvector index
|
|
78
|
+
puts "Creating vector index..."
|
|
79
|
+
begin
|
|
80
|
+
Vectra::Client.new.provider.create_index(
|
|
81
|
+
name: "documents",
|
|
82
|
+
dimension: 3,
|
|
83
|
+
metric: "cosine"
|
|
84
|
+
)
|
|
85
|
+
puts "✅ Index created\n"
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
puts "⚠️ Index might already exist: #{e.message}\n"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
puts "\n#{"=" * 80}"
|
|
91
|
+
puts "TESTING ACTIVERECORD INTEGRATION"
|
|
92
|
+
puts "=" * 80
|
|
93
|
+
puts
|
|
94
|
+
|
|
95
|
+
# Clean up existing data
|
|
96
|
+
Document.delete_all
|
|
97
|
+
|
|
98
|
+
# Test 1: Create document (auto-indexes)
|
|
99
|
+
puts "1. Creating documents (auto-indexes on save)...\n"
|
|
100
|
+
|
|
101
|
+
doc1 = Document.create!(
|
|
102
|
+
title: "Getting Started Guide",
|
|
103
|
+
content: "This guide will help you get started with our platform.",
|
|
104
|
+
category: "tutorial",
|
|
105
|
+
status: "published"
|
|
106
|
+
)
|
|
107
|
+
puts " Created: #{doc1.title} (ID: #{doc1.id})"
|
|
108
|
+
puts " Embedding: #{doc1.embedding.map { |v| v.round(3) }}"
|
|
109
|
+
puts " ✅ Automatically indexed in Vectra\n\n"
|
|
110
|
+
|
|
111
|
+
doc2 = Document.create!(
|
|
112
|
+
title: "Advanced Features",
|
|
113
|
+
content: "Learn about advanced features and best practices.",
|
|
114
|
+
category: "tutorial",
|
|
115
|
+
status: "published"
|
|
116
|
+
)
|
|
117
|
+
puts " Created: #{doc2.title} (ID: #{doc2.id})\n\n"
|
|
118
|
+
|
|
119
|
+
doc3 = Document.create!(
|
|
120
|
+
title: "API Reference",
|
|
121
|
+
content: "Complete API documentation for developers.",
|
|
122
|
+
category: "reference",
|
|
123
|
+
status: "published"
|
|
124
|
+
)
|
|
125
|
+
puts " Created: #{doc3.title} (ID: #{doc3.id})\n\n"
|
|
126
|
+
|
|
127
|
+
sleep 0.5
|
|
128
|
+
|
|
129
|
+
# Test 2: Vector search
|
|
130
|
+
puts "2. Vector search (finds similar documents)...\n"
|
|
131
|
+
|
|
132
|
+
query_embedding = [0.5, 0.5, 0.5]
|
|
133
|
+
results = Document.vector_search(query_embedding, limit: 5)
|
|
134
|
+
|
|
135
|
+
puts " Query: #{query_embedding.inspect}"
|
|
136
|
+
puts " Found #{results.size} results:\n\n"
|
|
137
|
+
|
|
138
|
+
results.each_with_index do |doc, idx|
|
|
139
|
+
puts " #{idx + 1}. #{doc.title}"
|
|
140
|
+
puts " Score: #{doc.vector_score.round(3)}"
|
|
141
|
+
puts " Category: #{doc.category}"
|
|
142
|
+
puts
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Test 3: Search with filters
|
|
146
|
+
puts "3. Vector search with metadata filter...\n"
|
|
147
|
+
|
|
148
|
+
results = Document.vector_search(
|
|
149
|
+
query_embedding,
|
|
150
|
+
limit: 10,
|
|
151
|
+
filter: { category: "tutorial" }
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
puts " Filter: category='tutorial'"
|
|
155
|
+
puts " Found #{results.size} results:\n"
|
|
156
|
+
results.each { |doc| puts " • #{doc.title}" }
|
|
157
|
+
puts
|
|
158
|
+
|
|
159
|
+
# Test 4: Find similar documents
|
|
160
|
+
puts "4. Find similar to specific document...\n"
|
|
161
|
+
|
|
162
|
+
similar = doc1.similar(limit: 2)
|
|
163
|
+
|
|
164
|
+
puts " Document: '#{doc1.title}'"
|
|
165
|
+
puts " Similar documents:\n"
|
|
166
|
+
similar.each do |doc|
|
|
167
|
+
puts " • #{doc.title} (score: #{doc.vector_score.round(3)})"
|
|
168
|
+
end
|
|
169
|
+
puts
|
|
170
|
+
|
|
171
|
+
# Test 5: Update triggers re-indexing
|
|
172
|
+
puts "5. Update document (triggers re-indexing)...\n"
|
|
173
|
+
|
|
174
|
+
doc1.update!(content: "Updated content about getting started.")
|
|
175
|
+
puts " Updated: #{doc1.title}"
|
|
176
|
+
puts " New embedding: #{doc1.embedding.map { |v| v.round(3) }}"
|
|
177
|
+
puts " ✅ Automatically re-indexed\n\n"
|
|
178
|
+
|
|
179
|
+
# Test 6: Manual index control
|
|
180
|
+
puts "6. Manual index control...\n"
|
|
181
|
+
|
|
182
|
+
doc4 = Document.new(
|
|
183
|
+
title: "Draft Article",
|
|
184
|
+
content: "This is a draft article.",
|
|
185
|
+
category: "blog",
|
|
186
|
+
status: "draft"
|
|
187
|
+
)
|
|
188
|
+
doc4.save!(validate: false) # Skip auto-index
|
|
189
|
+
|
|
190
|
+
puts " Created without auto-index: #{doc4.title}"
|
|
191
|
+
puts " Manually indexing..."
|
|
192
|
+
|
|
193
|
+
doc4.index_vector!
|
|
194
|
+
puts " ✅ Manually indexed\n\n"
|
|
195
|
+
|
|
196
|
+
# Test 7: Delete removes from index
|
|
197
|
+
puts "7. Delete document (removes from index)...\n"
|
|
198
|
+
|
|
199
|
+
doc4.destroy!
|
|
200
|
+
puts " ✅ Deleted and removed from vector index\n\n"
|
|
201
|
+
|
|
202
|
+
# Cleanup
|
|
203
|
+
puts "=" * 80
|
|
204
|
+
puts "SUMMARY"
|
|
205
|
+
puts "=" * 80
|
|
206
|
+
puts
|
|
207
|
+
|
|
208
|
+
puts "ActiveRecord Integration Features:"
|
|
209
|
+
puts " ✅ Automatic indexing on create/update"
|
|
210
|
+
puts " ✅ Automatic removal on delete"
|
|
211
|
+
puts " ✅ Vector search with AR object loading"
|
|
212
|
+
puts " ✅ Metadata filtering"
|
|
213
|
+
puts " ✅ Find similar documents"
|
|
214
|
+
puts " ✅ Manual index control"
|
|
215
|
+
puts " ✅ Custom embedding generation"
|
|
216
|
+
puts
|
|
217
|
+
|
|
218
|
+
puts "Total documents in database: #{Document.count}"
|
|
219
|
+
puts "Total documents in vector index: (same, auto-synced)"
|
|
220
|
+
puts
|
|
221
|
+
|
|
222
|
+
puts "✅ Demo complete!"
|
|
223
|
+
puts "\nNext steps:"
|
|
224
|
+
puts " • Replace embedding generation with real model (OpenAI, Cohere, etc.)"
|
|
225
|
+
puts " • Add background job for async indexing"
|
|
226
|
+
puts " • Use higher dimensions (384, 768, 1536)"
|
|
227
|
+
puts " • Add score threshold filtering"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demo of Vectra instrumentation features
|
|
5
|
+
#
|
|
6
|
+
# Usage: ruby examples/instrumentation_demo.rb
|
|
7
|
+
|
|
8
|
+
require "bundler/setup"
|
|
9
|
+
require "vectra"
|
|
10
|
+
|
|
11
|
+
puts "=" * 80
|
|
12
|
+
puts "VECTRA INSTRUMENTATION DEMO"
|
|
13
|
+
puts "=" * 80
|
|
14
|
+
puts
|
|
15
|
+
|
|
16
|
+
# Configure Vectra with instrumentation
|
|
17
|
+
Vectra.configure do |config|
|
|
18
|
+
config.provider = :pgvector
|
|
19
|
+
config.host = ENV.fetch("DATABASE_URL", "postgres://postgres:password@localhost/vectra_demo")
|
|
20
|
+
config.instrumentation = true # Enable instrumentation
|
|
21
|
+
config.pool_size = 5
|
|
22
|
+
config.batch_size = 100
|
|
23
|
+
config.max_retries = 3
|
|
24
|
+
config.retry_delay = 0.5
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Register custom instrumentation handler
|
|
28
|
+
puts "Registering custom instrumentation handler...\n"
|
|
29
|
+
|
|
30
|
+
Vectra.on_operation do |event|
|
|
31
|
+
status = event.success? ? "✅ SUCCESS" : "❌ ERROR"
|
|
32
|
+
duration_color = event.duration > 100 ? "\e[31m" : "\e[32m" # Red if > 100ms, green otherwise
|
|
33
|
+
reset_color = "\e[0m"
|
|
34
|
+
|
|
35
|
+
puts "#{status} | #{event.operation.to_s.upcase.ljust(10)} | " \
|
|
36
|
+
"#{event.provider}/#{event.index.ljust(15)} | " \
|
|
37
|
+
"#{duration_color}#{event.duration.round(1)}ms#{reset_color}"
|
|
38
|
+
|
|
39
|
+
if event.metadata.any?
|
|
40
|
+
puts " Metadata: #{event.metadata.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if event.failure?
|
|
44
|
+
puts " Error: #{event.error.class} - #{event.error.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Create client
|
|
51
|
+
client = Vectra::Client.new
|
|
52
|
+
|
|
53
|
+
puts "Creating test index...\n"
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
client.provider.delete_index(name: "demo_index")
|
|
57
|
+
rescue Vectra::NotFoundError
|
|
58
|
+
# Doesn't exist, that's fine
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
client.provider.create_index(name: "demo_index", dimension: 3, metric: "cosine")
|
|
62
|
+
sleep 0.5 # Give it a moment
|
|
63
|
+
|
|
64
|
+
puts "\n#{"=" * 80}"
|
|
65
|
+
puts "TESTING OPERATIONS"
|
|
66
|
+
puts "=" * 80
|
|
67
|
+
puts
|
|
68
|
+
|
|
69
|
+
# Test 1: Upsert
|
|
70
|
+
puts "1. UPSERT (3 vectors):"
|
|
71
|
+
client.upsert(
|
|
72
|
+
index: "demo_index",
|
|
73
|
+
vectors: [
|
|
74
|
+
{ id: "vec1", values: [0.1, 0.2, 0.3], metadata: { text: "Hello" } },
|
|
75
|
+
{ id: "vec2", values: [0.4, 0.5, 0.6], metadata: { text: "World" } },
|
|
76
|
+
{ id: "vec3", values: [0.7, 0.8, 0.9], metadata: { text: "Test" } }
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
sleep 0.5
|
|
81
|
+
|
|
82
|
+
# Test 2: Query
|
|
83
|
+
puts "2. QUERY (top_k=2):"
|
|
84
|
+
client.query(
|
|
85
|
+
index: "demo_index",
|
|
86
|
+
vector: [0.1, 0.2, 0.3],
|
|
87
|
+
top_k: 2
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
sleep 0.5
|
|
91
|
+
|
|
92
|
+
# Test 3: Fetch
|
|
93
|
+
puts "3. FETCH (2 IDs):"
|
|
94
|
+
client.fetch(
|
|
95
|
+
index: "demo_index",
|
|
96
|
+
ids: ["vec1", "vec2"]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
sleep 0.5
|
|
100
|
+
|
|
101
|
+
# Test 4: Update
|
|
102
|
+
puts "4. UPDATE (metadata):"
|
|
103
|
+
client.update(
|
|
104
|
+
index: "demo_index",
|
|
105
|
+
id: "vec1",
|
|
106
|
+
metadata: { text: "Updated", processed: true }
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
sleep 0.5
|
|
110
|
+
|
|
111
|
+
# Test 5: Delete
|
|
112
|
+
puts "5. DELETE (1 ID):"
|
|
113
|
+
client.delete(
|
|
114
|
+
index: "demo_index",
|
|
115
|
+
ids: ["vec3"]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
sleep 0.5
|
|
119
|
+
|
|
120
|
+
# Test 6: Bulk operations
|
|
121
|
+
puts "6. BULK UPSERT (100 vectors):"
|
|
122
|
+
bulk_vectors = 100.times.map do |i|
|
|
123
|
+
{ id: "bulk_#{i}", values: [rand, rand, rand], metadata: { index: i } }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
client.upsert(index: "demo_index", vectors: bulk_vectors)
|
|
127
|
+
|
|
128
|
+
sleep 0.5
|
|
129
|
+
|
|
130
|
+
# Test 7: Large query
|
|
131
|
+
puts "7. LARGE QUERY (top_k=50):"
|
|
132
|
+
client.query(
|
|
133
|
+
index: "demo_index",
|
|
134
|
+
vector: [rand, rand, rand],
|
|
135
|
+
top_k: 50
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Cleanup
|
|
139
|
+
puts "\n#{"=" * 80}"
|
|
140
|
+
puts "CLEANUP"
|
|
141
|
+
puts "=" * 80
|
|
142
|
+
puts
|
|
143
|
+
|
|
144
|
+
puts "Deleting test index..."
|
|
145
|
+
client.provider.delete_index(name: "demo_index")
|
|
146
|
+
|
|
147
|
+
puts "\n✅ Demo complete!"
|
|
148
|
+
puts "\nYou can see:"
|
|
149
|
+
puts " • Operation names (UPSERT, QUERY, FETCH, UPDATE, DELETE)"
|
|
150
|
+
puts " • Provider and index"
|
|
151
|
+
puts " • Duration in milliseconds (color-coded)"
|
|
152
|
+
puts " • Metadata (vector counts, filters, etc.)"
|
|
153
|
+
puts " • Success/error status"
|
|
154
|
+
puts "\nThis data can be sent to:"
|
|
155
|
+
puts " • New Relic (require 'vectra/instrumentation/new_relic')"
|
|
156
|
+
puts " • Datadog (require 'vectra/instrumentation/datadog')"
|
|
157
|
+
puts " • Custom monitoring systems"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Vectra
|
|
6
|
+
module Generators
|
|
7
|
+
# Rails generator for installing Vectra
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# rails generate vectra:install
|
|
11
|
+
# rails generate vectra:install --provider=pinecone
|
|
12
|
+
# rails generate vectra:install --provider=pgvector --database-url=postgres://localhost/mydb
|
|
13
|
+
#
|
|
14
|
+
class InstallGenerator < Rails::Generators::Base
|
|
15
|
+
source_root File.expand_path("templates", __dir__)
|
|
16
|
+
|
|
17
|
+
class_option :provider, type: :string, default: "pgvector",
|
|
18
|
+
desc: "Vector database provider (pinecone, pgvector, qdrant, weaviate)"
|
|
19
|
+
class_option :database_url, type: :string, default: nil,
|
|
20
|
+
desc: "PostgreSQL connection URL (for pgvector)"
|
|
21
|
+
class_option :api_key, type: :string, default: nil,
|
|
22
|
+
desc: "API key for the provider"
|
|
23
|
+
class_option :instrumentation, type: :boolean, default: false,
|
|
24
|
+
desc: "Enable instrumentation"
|
|
25
|
+
|
|
26
|
+
def create_initializer_file
|
|
27
|
+
template "vectra.rb", "config/initializers/vectra.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_migration
|
|
31
|
+
return unless options[:provider] == "pgvector"
|
|
32
|
+
|
|
33
|
+
generate :migration, "EnablePgvectorExtension"
|
|
34
|
+
|
|
35
|
+
migration_template(
|
|
36
|
+
"enable_pgvector_extension.rb",
|
|
37
|
+
"db/migrate/enable_pgvector_extension.rb",
|
|
38
|
+
migration_version: migration_version
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_readme
|
|
43
|
+
say "\n"
|
|
44
|
+
say "Vectra has been installed!", :green
|
|
45
|
+
say "\n"
|
|
46
|
+
say "Next steps:", :yellow
|
|
47
|
+
say " 1. Add your #{options[:provider]} credentials to Rails credentials:"
|
|
48
|
+
say " $ rails credentials:edit", :cyan
|
|
49
|
+
say "\n"
|
|
50
|
+
|
|
51
|
+
case options[:provider]
|
|
52
|
+
when "pinecone"
|
|
53
|
+
show_pinecone_instructions
|
|
54
|
+
when "pgvector"
|
|
55
|
+
show_pgvector_instructions
|
|
56
|
+
when "qdrant"
|
|
57
|
+
show_qdrant_instructions
|
|
58
|
+
when "weaviate"
|
|
59
|
+
show_weaviate_instructions
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return unless options[:instrumentation]
|
|
63
|
+
|
|
64
|
+
say "\n"
|
|
65
|
+
say " 📊 Instrumentation is enabled!", :green
|
|
66
|
+
say " Add New Relic or Datadog setup to config/initializers/vectra.rb"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def show_pinecone_instructions
|
|
72
|
+
say " 2. Add to credentials:", :yellow
|
|
73
|
+
say " pinecone:", :cyan
|
|
74
|
+
say " api_key: your_api_key_here", :cyan
|
|
75
|
+
say " environment: us-east-1", :cyan
|
|
76
|
+
say "\n"
|
|
77
|
+
say " 3. Create an index in Pinecone dashboard"
|
|
78
|
+
say "\n"
|
|
79
|
+
say " 4. Use in your app:", :yellow
|
|
80
|
+
say " @client = Vectra::Client.new", :cyan
|
|
81
|
+
say " @client.upsert(index: 'my-index', vectors: [...])", :cyan
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def show_pgvector_instructions
|
|
85
|
+
say " 2. Run migrations:", :yellow
|
|
86
|
+
say " $ rails db:migrate", :cyan
|
|
87
|
+
say "\n"
|
|
88
|
+
say " 3. Create a vector index:", :yellow
|
|
89
|
+
say " $ rails runner 'Vectra::Client.new.provider.create_index(name: \"documents\", dimension: 384)'", :cyan
|
|
90
|
+
say "\n"
|
|
91
|
+
say " 4. Use in your app:", :yellow
|
|
92
|
+
say " @client = Vectra::Client.new", :cyan
|
|
93
|
+
say " @client.upsert(index: 'documents', vectors: [...])", :cyan
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def show_qdrant_instructions
|
|
97
|
+
say " 2. Add to credentials:", :yellow
|
|
98
|
+
say " qdrant:", :cyan
|
|
99
|
+
say " api_key: your_api_key_here", :cyan
|
|
100
|
+
say " host: https://your-cluster.qdrant.io", :cyan
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def show_weaviate_instructions
|
|
104
|
+
say " 2. Add to credentials:", :yellow
|
|
105
|
+
say " weaviate:", :cyan
|
|
106
|
+
say " api_key: your_api_key_here", :cyan
|
|
107
|
+
say " host: https://your-cluster.weaviate.io", :cyan
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def migration_version
|
|
111
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|