prescient 0.3.0 ā 0.5.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/CHANGELOG.md +34 -0
- data/INTEGRATION_GUIDE.md +12 -1
- data/README.md +263 -14
- data/Rakefile +1 -1
- data/VECTOR_SEARCH_GUIDE.md +7 -3
- data/examples/README.md +2 -1
- data/examples/basic_usage.rb +1 -1
- data/examples/custom_contexts.rb +6 -21
- data/examples/vector_search.rb +69 -305
- data/exe/prescient +7 -0
- data/lib/prescient/cli.rb +479 -0
- data/lib/prescient/client.rb +16 -4
- data/lib/prescient/configuration_loader.rb +437 -0
- data/lib/prescient/provider/anthropic.rb +2 -2
- data/lib/prescient/provider/deepseek.rb +139 -0
- data/lib/prescient/provider/gemini.rb +173 -0
- data/lib/prescient/provider/huggingface.rb +8 -6
- data/lib/prescient/provider/mistral.rb +171 -0
- data/lib/prescient/provider/ollama.rb +3 -3
- data/lib/prescient/provider/openai.rb +10 -6
- data/lib/prescient/provider/xai.rb +139 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +117 -22
- data/schema/prescient.configuration.schema.json +153 -0
- data/sig/prescient.rbs +119 -2
- metadata +11 -2
data/examples/vector_search.rb
CHANGED
|
@@ -1,330 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Example: Vector similarity search with Prescient
|
|
5
|
-
#
|
|
4
|
+
# Example: Vector similarity search with Prescient and PostgreSQL pgvector.
|
|
5
|
+
# The Store owns only its embedding table; applications own their documents.
|
|
6
6
|
|
|
7
7
|
require_relative '../lib/prescient'
|
|
8
8
|
require 'pg'
|
|
9
|
-
require 'json'
|
|
10
9
|
|
|
11
|
-
puts
|
|
12
|
-
puts "This example shows how to use Prescient with PostgreSQL pgvector for semantic search."
|
|
10
|
+
puts '=== Vector Similarity Search Example ==='
|
|
13
11
|
|
|
14
|
-
# Database connection configuration
|
|
15
12
|
DB_CONFIG = {
|
|
16
|
-
host:
|
|
17
|
-
port:
|
|
18
|
-
dbname:
|
|
19
|
-
user:
|
|
20
|
-
password: ENV.fetch('DB_PASSWORD', 'prescient_password')
|
|
13
|
+
host: ENV.fetch('DB_HOST', 'localhost'),
|
|
14
|
+
port: ENV.fetch('DB_PORT', '5432'),
|
|
15
|
+
dbname: ENV.fetch('DB_NAME', 'prescient_development'),
|
|
16
|
+
user: ENV.fetch('DB_USER', 'prescient'),
|
|
17
|
+
password: ENV.fetch('DB_PASSWORD', 'prescient_password'),
|
|
21
18
|
}.freeze
|
|
22
19
|
|
|
23
20
|
class VectorSearchExample
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@client = Prescient.client(:ollama)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def run_example
|
|
30
|
-
puts "\n--- Setting up vector search example ---"
|
|
31
|
-
|
|
32
|
-
# Check if services are available
|
|
33
|
-
unless check_services_available
|
|
34
|
-
puts "ā Required services not available. Please start with: docker compose up -d"
|
|
35
|
-
return
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# 1. Generate and store embeddings for existing documents
|
|
39
|
-
puts "\nš Generating embeddings for sample documents..."
|
|
40
|
-
generate_document_embeddings
|
|
41
|
-
|
|
42
|
-
# 2. Perform similarity search
|
|
43
|
-
puts "\nš Performing similarity searches..."
|
|
44
|
-
search_examples
|
|
45
|
-
|
|
46
|
-
# 3. Advanced search with filtering
|
|
47
|
-
puts "\nšÆ Advanced search with metadata filtering..."
|
|
48
|
-
advanced_search_examples
|
|
49
|
-
|
|
50
|
-
# 4. Demonstrate different distance functions
|
|
51
|
-
puts "\nš Comparing different distance functions..."
|
|
52
|
-
compare_distance_functions
|
|
53
|
-
|
|
54
|
-
puts "\nš Vector search example completed!"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
21
|
+
EMBEDDING_DIMENSIONS = 768
|
|
22
|
+
EMBEDDING_MODEL = 'nomic-embed-text'
|
|
58
23
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return false
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Check pgvector extension
|
|
70
|
-
begin
|
|
71
|
-
result = @db.exec("SELECT * FROM pg_extension WHERE extname = 'vector'")
|
|
72
|
-
if result.ntuples > 0
|
|
73
|
-
puts "ā
pgvector extension available"
|
|
74
|
-
else
|
|
75
|
-
puts "ā pgvector extension not found"
|
|
76
|
-
return false
|
|
77
|
-
end
|
|
78
|
-
rescue PG::Error => e
|
|
79
|
-
puts "ā pgvector check failed: #{e.message}"
|
|
80
|
-
return false
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Check Ollama connection
|
|
84
|
-
if @client.available?
|
|
85
|
-
puts "ā
Ollama connected"
|
|
86
|
-
else
|
|
87
|
-
puts "ā Ollama not available"
|
|
88
|
-
return false
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
true
|
|
24
|
+
def initialize
|
|
25
|
+
@connection = PG.connect(DB_CONFIG)
|
|
26
|
+
@client = Prescient.client(:ollama, enable_fallback: false)
|
|
27
|
+
@store = Prescient::Pgvector::Store.new(
|
|
28
|
+
connection: @connection,
|
|
29
|
+
dimensions: EMBEDDING_DIMENSIONS,
|
|
30
|
+
)
|
|
92
31
|
end
|
|
93
32
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if result.ntuples == 0
|
|
109
|
-
puts " All documents already have embeddings"
|
|
110
|
-
return
|
|
33
|
+
def run
|
|
34
|
+
@store.install!
|
|
35
|
+
%i[cosine euclidean inner_product].each { |metric| @store.create_index!(metric:) }
|
|
36
|
+
|
|
37
|
+
documents.each do |document|
|
|
38
|
+
embedding = @client.generate_embedding(document[:content], model: EMBEDDING_MODEL)
|
|
39
|
+
@store.upsert(
|
|
40
|
+
id: document[:id],
|
|
41
|
+
embedding:,
|
|
42
|
+
provider: 'ollama',
|
|
43
|
+
model: EMBEDDING_MODEL,
|
|
44
|
+
content: document[:content],
|
|
45
|
+
metadata: { title: document[:title] },
|
|
46
|
+
)
|
|
111
47
|
end
|
|
112
48
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
rescue Prescient::Error => e
|
|
130
|
-
puts " ā Failed to generate embedding: #{e.message}"
|
|
131
|
-
end
|
|
49
|
+
query = 'How do I improve database performance?'
|
|
50
|
+
embedding = @client.generate_embedding(query, model: EMBEDDING_MODEL)
|
|
51
|
+
results = @store.search(
|
|
52
|
+
embedding:,
|
|
53
|
+
limit: 3,
|
|
54
|
+
metric: :cosine,
|
|
55
|
+
provider: 'ollama',
|
|
56
|
+
model: EMBEDDING_MODEL,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
puts "\nQuery: #{query}"
|
|
60
|
+
results.each_with_index do |result, index|
|
|
61
|
+
title = result[:metadata].fetch('title')
|
|
62
|
+
puts "#{index + 1}. #{title} (distance: #{result[:distance].round(4)})"
|
|
63
|
+
puts " #{result[:content]}"
|
|
132
64
|
end
|
|
65
|
+
rescue Prescient::Error, PG::Error => e
|
|
66
|
+
warn "Vector search failed: #{e.message}"
|
|
67
|
+
ensure
|
|
68
|
+
@connection&.close
|
|
133
69
|
end
|
|
134
70
|
|
|
135
|
-
|
|
136
|
-
# Convert Ruby array to PostgreSQL vector format
|
|
137
|
-
vector_str = "[#{embedding.join(',')}]"
|
|
138
|
-
|
|
139
|
-
query = <<~SQL
|
|
140
|
-
INSERT INTO document_embeddings
|
|
141
|
-
(document_id, embedding_provider, embedding_model, embedding_dimensions, embedding, embedding_text)
|
|
142
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
143
|
-
SQL
|
|
144
|
-
|
|
145
|
-
@db.exec_params(query, [document_id, provider, model, dimensions, vector_str, text])
|
|
146
|
-
end
|
|
71
|
+
private
|
|
147
72
|
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
73
|
+
def documents
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
id: 'postgres-indexes',
|
|
77
|
+
title: 'PostgreSQL indexes',
|
|
78
|
+
content: 'Indexes can reduce query latency when their columns match common filters and ordering.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'ruby-performance',
|
|
82
|
+
title: 'Ruby performance',
|
|
83
|
+
content: 'Measure allocations and repeated work before optimizing a Ruby application.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'api-security',
|
|
87
|
+
title: 'API security',
|
|
88
|
+
content: 'Protect API credentials, validate inputs, and avoid exposing provider response bodies.',
|
|
89
|
+
},
|
|
154
90
|
]
|
|
155
|
-
|
|
156
|
-
search_queries.each do |query_text|
|
|
157
|
-
puts "\nš Searching for: '#{query_text}'"
|
|
158
|
-
perform_similarity_search(query_text, limit: 3)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def perform_similarity_search(query_text, limit: 5, distance_function: 'cosine')
|
|
163
|
-
begin
|
|
164
|
-
# Generate embedding for query
|
|
165
|
-
query_embedding = @client.generate_embedding(query_text)
|
|
166
|
-
query_vector = "[#{query_embedding.join(',')}]"
|
|
167
|
-
|
|
168
|
-
# Choose distance operator based on function
|
|
169
|
-
distance_op = case distance_function
|
|
170
|
-
when 'cosine' then '<=>'
|
|
171
|
-
when 'l2' then '<->'
|
|
172
|
-
when 'inner_product' then '<#>'
|
|
173
|
-
else '<=>'
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Perform similarity search
|
|
177
|
-
search_query = <<~SQL
|
|
178
|
-
SELECT
|
|
179
|
-
d.title,
|
|
180
|
-
d.content,
|
|
181
|
-
d.metadata,
|
|
182
|
-
de.embedding #{distance_op} $1::vector AS distance,
|
|
183
|
-
1 - (de.embedding <=> $1::vector) AS cosine_similarity
|
|
184
|
-
FROM documents d
|
|
185
|
-
JOIN document_embeddings de ON d.id = de.document_id
|
|
186
|
-
WHERE de.embedding_provider = 'ollama'
|
|
187
|
-
AND de.embedding_model = 'nomic-embed-text'
|
|
188
|
-
ORDER BY de.embedding #{distance_op} $1::vector
|
|
189
|
-
LIMIT $2
|
|
190
|
-
SQL
|
|
191
|
-
|
|
192
|
-
result = @db.exec_params(search_query, [query_vector, limit])
|
|
193
|
-
|
|
194
|
-
if result.ntuples == 0
|
|
195
|
-
puts " No results found"
|
|
196
|
-
return
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
result.each_with_index do |row, index|
|
|
200
|
-
similarity = (row['cosine_similarity'].to_f * 100).round(1)
|
|
201
|
-
puts " #{index + 1}. #{row['title']} (#{similarity}% similar)"
|
|
202
|
-
puts " #{row['content'][0..100]}..."
|
|
203
|
-
|
|
204
|
-
# Show metadata if available
|
|
205
|
-
if row['metadata'] && !row['metadata'].empty?
|
|
206
|
-
metadata = JSON.parse(row['metadata'])
|
|
207
|
-
tags = metadata['tags']&.join(', ')
|
|
208
|
-
puts " Tags: #{tags}" if tags
|
|
209
|
-
end
|
|
210
|
-
puts
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
rescue Prescient::Error => e
|
|
214
|
-
puts " ā Search failed: #{e.message}"
|
|
215
|
-
rescue PG::Error => e
|
|
216
|
-
puts " ā Database error: #{e.message}"
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def advanced_search_examples
|
|
221
|
-
# Search with metadata filtering
|
|
222
|
-
puts "\nšÆ Search for programming content with beginner difficulty:"
|
|
223
|
-
advanced_search("programming basics", tags: ["programming"], difficulty: "beginner")
|
|
224
|
-
|
|
225
|
-
puts "\nšÆ Search for AI/ML content:"
|
|
226
|
-
advanced_search("artificial intelligence", tags: ["ai", "machine-learning"])
|
|
227
91
|
end
|
|
228
|
-
|
|
229
|
-
def advanced_search(query_text, filters = {})
|
|
230
|
-
begin
|
|
231
|
-
query_embedding = @client.generate_embedding(query_text)
|
|
232
|
-
query_vector = "[#{query_embedding.join(',')}]"
|
|
233
|
-
|
|
234
|
-
# Build WHERE clause for metadata filtering
|
|
235
|
-
where_conditions = ["de.embedding_provider = 'ollama'", "de.embedding_model = 'nomic-embed-text'"]
|
|
236
|
-
params = [query_vector]
|
|
237
|
-
param_index = 2
|
|
238
|
-
|
|
239
|
-
filters.each do |key, value|
|
|
240
|
-
case key
|
|
241
|
-
when :tags
|
|
242
|
-
# Filter by tags array overlap
|
|
243
|
-
where_conditions << "d.metadata->'tags' ?| $#{param_index}::text[]"
|
|
244
|
-
params << value
|
|
245
|
-
param_index += 1
|
|
246
|
-
when :difficulty
|
|
247
|
-
# Filter by exact difficulty match
|
|
248
|
-
where_conditions << "d.metadata->>'difficulty' = $#{param_index}"
|
|
249
|
-
params << value
|
|
250
|
-
param_index += 1
|
|
251
|
-
when :source_type
|
|
252
|
-
# Filter by source type
|
|
253
|
-
where_conditions << "d.source_type = $#{param_index}"
|
|
254
|
-
params << value
|
|
255
|
-
param_index += 1
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
search_query = <<~SQL
|
|
260
|
-
SELECT
|
|
261
|
-
d.title,
|
|
262
|
-
d.content,
|
|
263
|
-
d.metadata,
|
|
264
|
-
de.embedding <=> $1::vector AS cosine_distance,
|
|
265
|
-
1 - (de.embedding <=> $1::vector) AS cosine_similarity
|
|
266
|
-
FROM documents d
|
|
267
|
-
JOIN document_embeddings de ON d.id = de.document_id
|
|
268
|
-
WHERE #{where_conditions.join(' AND ')}
|
|
269
|
-
ORDER BY de.embedding <=> $1::vector
|
|
270
|
-
LIMIT 3
|
|
271
|
-
SQL
|
|
272
|
-
|
|
273
|
-
result = @db.exec_params(search_query, params)
|
|
274
|
-
|
|
275
|
-
if result.ntuples == 0
|
|
276
|
-
puts " No results found with the specified filters"
|
|
277
|
-
return
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
result.each_with_index do |row, index|
|
|
281
|
-
similarity = (row['cosine_similarity'].to_f * 100).round(1)
|
|
282
|
-
puts " #{index + 1}. #{row['title']} (#{similarity}% similar)"
|
|
283
|
-
|
|
284
|
-
metadata = JSON.parse(row['metadata'])
|
|
285
|
-
puts " Difficulty: #{metadata['difficulty']}"
|
|
286
|
-
puts " Tags: #{metadata['tags']&.join(', ')}"
|
|
287
|
-
puts " #{row['content'][0..80]}..."
|
|
288
|
-
puts
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
rescue Prescient::Error => e
|
|
292
|
-
puts " ā Search failed: #{e.message}"
|
|
293
|
-
rescue PG::Error => e
|
|
294
|
-
puts " ā Database error: #{e.message}"
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def compare_distance_functions
|
|
299
|
-
query_text = "programming languages and development"
|
|
300
|
-
|
|
301
|
-
puts "\nš Comparing distance functions for: '#{query_text}'"
|
|
302
|
-
|
|
303
|
-
%w[cosine l2 inner_product].each do |distance_func|
|
|
304
|
-
puts "\n #{distance_func.upcase} Distance:"
|
|
305
|
-
perform_similarity_search(query_text, limit: 2, distance_function: distance_func)
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def cleanup
|
|
310
|
-
@db.close if @db
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
# Run the example
|
|
315
|
-
begin
|
|
316
|
-
example = VectorSearchExample.new
|
|
317
|
-
example.run_example
|
|
318
|
-
rescue StandardError => e
|
|
319
|
-
puts "ā Example failed: #{e.message}"
|
|
320
|
-
puts e.backtrace.first(5).join("\n")
|
|
321
|
-
ensure
|
|
322
|
-
example&.cleanup
|
|
323
92
|
end
|
|
324
93
|
|
|
325
|
-
|
|
326
|
-
puts " - Try different embedding models (OpenAI, HuggingFace)"
|
|
327
|
-
puts " - Implement hybrid search (vector + keyword)"
|
|
328
|
-
puts " - Add document chunking for large texts"
|
|
329
|
-
puts " - Experiment with different similarity thresholds"
|
|
330
|
-
puts " - Add result re-ranking and filtering"
|
|
94
|
+
VectorSearchExample.new.run
|