Initial commit
This commit is contained in:
99
templates/basic-embeddings.ts
Normal file
99
templates/basic-embeddings.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Basic Gemini Embeddings Example (SDK)
|
||||
*
|
||||
* Demonstrates single text embedding generation using the @google/genai SDK.
|
||||
*
|
||||
* Setup:
|
||||
* 1. npm install @google/genai@^1.27.0
|
||||
* 2. export GEMINI_API_KEY="your-api-key"
|
||||
* 3. Get API key from: https://aistudio.google.com/apikey
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx basic-embeddings.ts
|
||||
*/
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
async function generateEmbedding(text: string) {
|
||||
// Initialize client with API key
|
||||
const ai = new GoogleGenAI({
|
||||
apiKey: process.env.GEMINI_API_KEY
|
||||
});
|
||||
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
throw new Error('GEMINI_API_KEY environment variable not set');
|
||||
}
|
||||
|
||||
console.log(`\nGenerating embedding for: "${text}"\n`);
|
||||
|
||||
// Generate embedding
|
||||
const response = await ai.models.embedContent({
|
||||
model: 'gemini-embedding-001', // Stable production model
|
||||
content: text,
|
||||
config: {
|
||||
taskType: 'SEMANTIC_SIMILARITY', // Optimize for similarity comparison
|
||||
outputDimensionality: 768 // Recommended for most use cases
|
||||
}
|
||||
});
|
||||
|
||||
const embedding = response.embedding.values;
|
||||
|
||||
console.log(`✅ Embedding generated successfully!`);
|
||||
console.log(`Dimensions: ${embedding.length}`);
|
||||
console.log(`First 10 values: [${embedding.slice(0, 10).map(v => v.toFixed(4)).join(', ')}...]`);
|
||||
console.log(`\nVector magnitude: ${Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0)).toFixed(4)}`);
|
||||
|
||||
return embedding;
|
||||
}
|
||||
|
||||
// Example usage
|
||||
async function main() {
|
||||
try {
|
||||
const text = "What is the meaning of life?";
|
||||
const embedding = await generateEmbedding(text);
|
||||
|
||||
// Compare with another text
|
||||
const text2 = "What is the purpose of existence?";
|
||||
console.log(`\nGenerating embedding for: "${text2}"\n`);
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
||||
const response2 = await ai.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
content: text2,
|
||||
config: {
|
||||
taskType: 'SEMANTIC_SIMILARITY',
|
||||
outputDimensionality: 768
|
||||
}
|
||||
});
|
||||
|
||||
const embedding2 = response2.embedding.values;
|
||||
|
||||
// Calculate cosine similarity
|
||||
const cosineSimilarity = (a: number[], b: number[]): number => {
|
||||
let dotProduct = 0, magA = 0, magB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dotProduct += a[i] * b[i];
|
||||
magA += a[i] * a[i];
|
||||
magB += b[i] * b[i];
|
||||
}
|
||||
return dotProduct / (Math.sqrt(magA) * Math.sqrt(magB));
|
||||
};
|
||||
|
||||
const similarity = cosineSimilarity(embedding, embedding2);
|
||||
console.log(`\n🔗 Similarity between texts: ${(similarity * 100).toFixed(2)}%`);
|
||||
console.log('(1.0 = identical, 0.0 = completely different)\n');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error:', error.message);
|
||||
|
||||
if (error.status === 401) {
|
||||
console.error('\nCheck that GEMINI_API_KEY is set correctly');
|
||||
} else if (error.status === 429) {
|
||||
console.error('\nRate limit exceeded. Free tier: 100 requests/minute');
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
240
templates/batch-embeddings.ts
Normal file
240
templates/batch-embeddings.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Batch Embeddings with Rate Limiting
|
||||
*
|
||||
* Demonstrates processing multiple texts with proper rate limiting
|
||||
* and exponential backoff for production use.
|
||||
*
|
||||
* Setup:
|
||||
* 1. npm install @google/genai@^1.27.0
|
||||
* 2. export GEMINI_API_KEY="your-api-key"
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx batch-embeddings.ts
|
||||
*/
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
interface RateLimitConfig {
|
||||
requestsPerMinute: number;
|
||||
maxRetries: number;
|
||||
initialDelayMs: number;
|
||||
}
|
||||
|
||||
class EmbeddingService {
|
||||
private ai: GoogleGenAI;
|
||||
private config: RateLimitConfig;
|
||||
private requestTimes: number[] = [];
|
||||
|
||||
constructor(apiKey: string, config?: Partial<RateLimitConfig>) {
|
||||
this.ai = new GoogleGenAI({ apiKey });
|
||||
this.config = {
|
||||
requestsPerMinute: config?.requestsPerMinute || 100, // Free tier limit
|
||||
maxRetries: config?.maxRetries || 3,
|
||||
initialDelayMs: config?.initialDelayMs || 1000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait if needed to respect rate limits
|
||||
*/
|
||||
private async enforceRateLimit(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const oneMinuteAgo = now - 60000;
|
||||
|
||||
// Remove requests older than 1 minute
|
||||
this.requestTimes = this.requestTimes.filter(time => time > oneMinuteAgo);
|
||||
|
||||
// If at limit, wait until oldest request expires
|
||||
if (this.requestTimes.length >= this.config.requestsPerMinute) {
|
||||
const oldestRequest = this.requestTimes[0];
|
||||
const waitTime = 60000 - (now - oldestRequest) + 100; // +100ms buffer
|
||||
|
||||
if (waitTime > 0) {
|
||||
console.log(`⏳ Rate limit reached. Waiting ${(waitTime / 1000).toFixed(1)}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
}
|
||||
}
|
||||
|
||||
this.requestTimes.push(Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate embedding with retry logic
|
||||
*/
|
||||
async embedText(
|
||||
text: string,
|
||||
options: {
|
||||
taskType?: string;
|
||||
outputDimensionality?: number;
|
||||
} = {}
|
||||
): Promise<number[]> {
|
||||
const {
|
||||
taskType = 'SEMANTIC_SIMILARITY',
|
||||
outputDimensionality = 768
|
||||
} = options;
|
||||
|
||||
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
||||
try {
|
||||
await this.enforceRateLimit();
|
||||
|
||||
const response = await this.ai.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
content: text,
|
||||
config: { taskType, outputDimensionality }
|
||||
});
|
||||
|
||||
return response.embedding.values;
|
||||
|
||||
} catch (error: any) {
|
||||
const isLastAttempt = attempt === this.config.maxRetries - 1;
|
||||
|
||||
// Retry on rate limit errors
|
||||
if (error.status === 429 && !isLastAttempt) {
|
||||
const delay = this.config.initialDelayMs * Math.pow(2, attempt);
|
||||
console.log(`⚠️ Rate limit error. Retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${this.config.maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed after ${this.config.maxRetries} retries`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch embed multiple texts
|
||||
*/
|
||||
async embedBatch(
|
||||
texts: string[],
|
||||
options: {
|
||||
taskType?: string;
|
||||
outputDimensionality?: number;
|
||||
onProgress?: (current: number, total: number) => void;
|
||||
} = {}
|
||||
): Promise<number[][]> {
|
||||
const {
|
||||
taskType = 'RETRIEVAL_DOCUMENT',
|
||||
outputDimensionality = 768,
|
||||
onProgress
|
||||
} = options;
|
||||
|
||||
console.log(`\n📊 Embedding ${texts.length} texts...`);
|
||||
console.log(`Rate limit: ${this.config.requestsPerMinute} RPM\n`);
|
||||
|
||||
const embeddings: number[][] = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
const text = texts[i];
|
||||
const embedding = await this.embedText(text, { taskType, outputDimensionality });
|
||||
embeddings.push(embedding);
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(i + 1, texts.length);
|
||||
}
|
||||
|
||||
// Progress logging
|
||||
if ((i + 1) % 10 === 0 || i === texts.length - 1) {
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const rate = (i + 1) / elapsed;
|
||||
const remaining = texts.length - (i + 1);
|
||||
const eta = remaining / rate;
|
||||
|
||||
console.log(`✅ ${i + 1}/${texts.length} (${rate.toFixed(1)} texts/sec, ETA: ${eta.toFixed(1)}s)`);
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = (Date.now() - startTime) / 1000;
|
||||
console.log(`\n✨ Completed in ${totalTime.toFixed(1)}s (avg: ${(texts.length / totalTime).toFixed(1)} texts/sec)\n`);
|
||||
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use batch API for multiple texts at once (more efficient)
|
||||
*/
|
||||
async embedBatchAPI(
|
||||
texts: string[],
|
||||
options: {
|
||||
taskType?: string;
|
||||
outputDimensionality?: number;
|
||||
} = {}
|
||||
): Promise<number[][]> {
|
||||
const {
|
||||
taskType = 'RETRIEVAL_DOCUMENT',
|
||||
outputDimensionality = 768
|
||||
} = options;
|
||||
|
||||
await this.enforceRateLimit();
|
||||
|
||||
const response = await this.ai.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
contents: texts, // Array of strings
|
||||
config: { taskType, outputDimensionality }
|
||||
});
|
||||
|
||||
return response.embeddings.map(e => e.values);
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
async function main() {
|
||||
try {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('GEMINI_API_KEY environment variable not set');
|
||||
}
|
||||
|
||||
const service = new EmbeddingService(apiKey, {
|
||||
requestsPerMinute: 100, // Free tier
|
||||
maxRetries: 3
|
||||
});
|
||||
|
||||
// Sample documents
|
||||
const documents = [
|
||||
"What is the meaning of life?",
|
||||
"How does photosynthesis work?",
|
||||
"Explain quantum mechanics in simple terms",
|
||||
"What is the history of artificial intelligence?",
|
||||
"How do neural networks learn?",
|
||||
"What is the difference between machine learning and deep learning?",
|
||||
"Explain the theory of relativity",
|
||||
"What is climate change?",
|
||||
"How does the human brain work?",
|
||||
"What is the future of technology?"
|
||||
];
|
||||
|
||||
console.log('🚀 Method 1: Sequential with rate limiting');
|
||||
const embeddings1 = await service.embedBatch(documents, {
|
||||
taskType: 'RETRIEVAL_DOCUMENT',
|
||||
outputDimensionality: 768,
|
||||
onProgress: (current, total) => {
|
||||
// Optional: Update progress bar, database, etc.
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n🚀 Method 2: Batch API (single request)');
|
||||
const startTime = Date.now();
|
||||
const embeddings2 = await service.embedBatchAPI(documents, {
|
||||
taskType: 'RETRIEVAL_DOCUMENT',
|
||||
outputDimensionality: 768
|
||||
});
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
|
||||
console.log(`✨ Completed in ${elapsed.toFixed(1)}s (${documents.length} texts in 1 request)\n`);
|
||||
|
||||
// Verify results
|
||||
console.log('📈 Results:');
|
||||
console.log(`Embeddings generated: ${embeddings2.length}`);
|
||||
console.log(`Dimensions per embedding: ${embeddings2[0].length}`);
|
||||
console.log(`Total vectors: ${embeddings2.length * embeddings2[0].length}`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
311
templates/clustering.ts
Normal file
311
templates/clustering.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Document Clustering with Gemini Embeddings
|
||||
*
|
||||
* Demonstrates automatic grouping of similar documents using K-means clustering.
|
||||
* Useful for topic modeling, content organization, and duplicate detection.
|
||||
*
|
||||
* Setup:
|
||||
* 1. npm install @google/genai@^1.27.0
|
||||
* 2. export GEMINI_API_KEY="your-api-key"
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx clustering.ts
|
||||
*/
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
text: string;
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
interface Cluster {
|
||||
id: number;
|
||||
centroid: number[];
|
||||
documents: Document[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cosine similarity
|
||||
*/
|
||||
function cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Vector dimensions must match');
|
||||
}
|
||||
|
||||
let dotProduct = 0, magA = 0, magB = 0;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dotProduct += a[i] * b[i];
|
||||
magA += a[i] * a[i];
|
||||
magB += b[i] * b[i];
|
||||
}
|
||||
|
||||
return dotProduct / (Math.sqrt(magA) * Math.sqrt(magB));
|
||||
}
|
||||
|
||||
/**
|
||||
* K-means clustering algorithm
|
||||
*/
|
||||
function kMeansClustering(
|
||||
documents: Document[],
|
||||
k: number = 3,
|
||||
maxIterations: number = 100
|
||||
): Cluster[] {
|
||||
if (documents.length === 0 || !documents[0].embedding) {
|
||||
throw new Error('Documents must have embeddings');
|
||||
}
|
||||
|
||||
const embeddings = documents.map(d => d.embedding!);
|
||||
|
||||
// 1. Initialize centroids randomly
|
||||
const centroids: number[][] = [];
|
||||
const usedIndices = new Set<number>();
|
||||
|
||||
for (let i = 0; i < k; i++) {
|
||||
let randomIndex: number;
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * embeddings.length);
|
||||
} while (usedIndices.has(randomIndex));
|
||||
|
||||
usedIndices.add(randomIndex);
|
||||
centroids.push([...embeddings[randomIndex]]);
|
||||
}
|
||||
|
||||
console.log(`🔄 Starting K-means clustering (k=${k}, max iterations=${maxIterations})\n`);
|
||||
|
||||
// 2. Iterate until convergence
|
||||
let iteration = 0;
|
||||
let converged = false;
|
||||
|
||||
while (iteration < maxIterations && !converged) {
|
||||
// Assign each document to nearest centroid
|
||||
const clusters: Document[][] = Array(k).fill(null).map(() => []);
|
||||
|
||||
documents.forEach((doc, idx) => {
|
||||
const embedding = embeddings[idx];
|
||||
let maxSimilarity = -Infinity;
|
||||
let closestCluster = 0;
|
||||
|
||||
centroids.forEach((centroid, i) => {
|
||||
const similarity = cosineSimilarity(embedding, centroid);
|
||||
if (similarity > maxSimilarity) {
|
||||
maxSimilarity = similarity;
|
||||
closestCluster = i;
|
||||
}
|
||||
});
|
||||
|
||||
clusters[closestCluster].push(doc);
|
||||
});
|
||||
|
||||
// Update centroids (average of cluster members)
|
||||
converged = true;
|
||||
clusters.forEach((cluster, i) => {
|
||||
if (cluster.length === 0) return;
|
||||
|
||||
const newCentroid = cluster[0].embedding!.map((_, dim) =>
|
||||
cluster.reduce((sum, doc) => sum + doc.embedding![dim], 0) / cluster.length
|
||||
);
|
||||
|
||||
// Check if centroid changed significantly
|
||||
const similarity = cosineSimilarity(centroids[i], newCentroid);
|
||||
if (similarity < 0.9999) {
|
||||
converged = false;
|
||||
}
|
||||
|
||||
centroids[i] = newCentroid;
|
||||
});
|
||||
|
||||
iteration++;
|
||||
|
||||
if (iteration % 10 === 0) {
|
||||
console.log(`Iteration ${iteration}...`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Converged after ${iteration} iterations\n`);
|
||||
|
||||
// Build final clusters
|
||||
const finalClusters: Cluster[] = centroids.map((centroid, i) => ({
|
||||
id: i,
|
||||
centroid,
|
||||
documents: documents.filter((doc) => {
|
||||
const similarities = centroids.map(c => cosineSimilarity(doc.embedding!, c));
|
||||
return similarities.indexOf(Math.max(...similarities)) === i;
|
||||
})
|
||||
}));
|
||||
|
||||
return finalClusters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clustering by similarity threshold (alternative to K-means)
|
||||
*/
|
||||
function clusterByThreshold(
|
||||
documents: Document[],
|
||||
threshold: number = 0.8
|
||||
): Cluster[] {
|
||||
if (documents.length === 0 || !documents[0].embedding) {
|
||||
throw new Error('Documents must have embeddings');
|
||||
}
|
||||
|
||||
const clusters: Cluster[] = [];
|
||||
const assigned = new Set<number>();
|
||||
|
||||
documents.forEach((doc, idx) => {
|
||||
if (assigned.has(idx)) return;
|
||||
|
||||
const clusterDocs = [doc];
|
||||
assigned.add(idx);
|
||||
|
||||
documents.forEach((otherDoc, otherIdx) => {
|
||||
if (idx !== otherIdx && !assigned.has(otherIdx)) {
|
||||
const similarity = cosineSimilarity(doc.embedding!, otherDoc.embedding!);
|
||||
|
||||
if (similarity >= threshold) {
|
||||
clusterDocs.push(otherDoc);
|
||||
assigned.add(otherIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clusters.push({
|
||||
id: clusters.length,
|
||||
centroid: doc.embedding!,
|
||||
documents: clusterDocs
|
||||
});
|
||||
});
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print cluster summary
|
||||
*/
|
||||
function printClusters(clusters: Cluster[], method: string): void {
|
||||
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||
console.log(`${method} Results`);
|
||||
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
||||
|
||||
clusters.forEach(cluster => {
|
||||
console.log(`📁 Cluster ${cluster.id + 1} (${cluster.documents.length} documents):`);
|
||||
console.log(`${'─'.repeat(50)}`);
|
||||
|
||||
cluster.documents.forEach(doc => {
|
||||
const preview = doc.text.substring(0, 80) + (doc.text.length > 80 ? '...' : '');
|
||||
console.log(` • [${doc.id}] ${preview}`);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
});
|
||||
|
||||
console.log(`Total clusters: ${clusters.length}\n`);
|
||||
}
|
||||
|
||||
// Example usage
|
||||
async function main() {
|
||||
try {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('GEMINI_API_KEY environment variable not set');
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
|
||||
// Sample documents (3 topics: Geography, AI/ML, Food)
|
||||
const documents: Document[] = [
|
||||
// Geography
|
||||
{ id: 'doc1', text: 'Paris is the capital of France. It is known for the Eiffel Tower and the Louvre Museum.' },
|
||||
{ id: 'doc2', text: 'London is the capital of the United Kingdom and home to Big Ben and Buckingham Palace.' },
|
||||
{ id: 'doc3', text: 'Rome is the capital of Italy and famous for the Colosseum and Vatican City.' },
|
||||
|
||||
// AI/ML
|
||||
{ id: 'doc4', text: 'Machine learning is a subset of artificial intelligence that enables computers to learn from data.' },
|
||||
{ id: 'doc5', text: 'Deep learning uses neural networks with multiple layers to learn complex patterns in data.' },
|
||||
{ id: 'doc6', text: 'Natural language processing is a branch of AI that helps computers understand human language.' },
|
||||
|
||||
// Food
|
||||
{ id: 'doc7', text: 'Pizza originated in Italy and is now popular worldwide. It typically has a tomato base and cheese.' },
|
||||
{ id: 'doc8', text: 'Sushi is a Japanese dish made with vinegared rice and various ingredients like raw fish.' },
|
||||
{ id: 'doc9', text: 'Tacos are a traditional Mexican food consisting of a tortilla filled with various ingredients.' }
|
||||
];
|
||||
|
||||
console.log(`\n📚 Generating embeddings for ${documents.length} documents...\n`);
|
||||
|
||||
// Generate embeddings
|
||||
for (const doc of documents) {
|
||||
const response = await ai.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
content: doc.text,
|
||||
config: {
|
||||
taskType: 'CLUSTERING', // ← Optimized for clustering
|
||||
outputDimensionality: 768
|
||||
}
|
||||
});
|
||||
|
||||
doc.embedding = response.embedding.values;
|
||||
console.log(`✅ Embedded: ${doc.id}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Method 1: K-means clustering
|
||||
const kMeansClusters = kMeansClustering(documents, 3, 100);
|
||||
printClusters(kMeansClusters, 'K-Means Clustering (k=3)');
|
||||
|
||||
// Method 2: Threshold-based clustering
|
||||
console.log('🔄 Running threshold-based clustering (threshold=0.7)...\n');
|
||||
const thresholdClusters = clusterByThreshold(documents, 0.7);
|
||||
printClusters(thresholdClusters, 'Threshold-Based Clustering (≥70% similarity)');
|
||||
|
||||
// Example: Find intra-cluster similarities
|
||||
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||
console.log('Cluster Quality Analysis');
|
||||
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
||||
|
||||
kMeansClusters.forEach(cluster => {
|
||||
if (cluster.documents.length < 2) return;
|
||||
|
||||
const similarities: number[] = [];
|
||||
|
||||
for (let i = 0; i < cluster.documents.length; i++) {
|
||||
for (let j = i + 1; j < cluster.documents.length; j++) {
|
||||
const sim = cosineSimilarity(
|
||||
cluster.documents[i].embedding!,
|
||||
cluster.documents[j].embedding!
|
||||
);
|
||||
similarities.push(sim);
|
||||
}
|
||||
}
|
||||
|
||||
const avgSimilarity = similarities.reduce((a, b) => a + b, 0) / similarities.length;
|
||||
const minSimilarity = Math.min(...similarities);
|
||||
const maxSimilarity = Math.max(...similarities);
|
||||
|
||||
console.log(`Cluster ${cluster.id + 1}:`);
|
||||
console.log(` Documents: ${cluster.documents.map(d => d.id).join(', ')}`);
|
||||
console.log(` Avg similarity: ${(avgSimilarity * 100).toFixed(1)}%`);
|
||||
console.log(` Min similarity: ${(minSimilarity * 100).toFixed(1)}%`);
|
||||
console.log(` Max similarity: ${(maxSimilarity * 100).toFixed(1)}%`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
/**
|
||||
* Expected output:
|
||||
*
|
||||
* Cluster 1: Geography documents (Paris, London, Rome)
|
||||
* Cluster 2: AI/ML documents (Machine learning, Deep learning, NLP)
|
||||
* Cluster 3: Food documents (Pizza, Sushi, Tacos)
|
||||
*
|
||||
* This demonstrates how embeddings capture semantic meaning,
|
||||
* allowing automatic topic discovery without manual labeling.
|
||||
*/
|
||||
157
templates/embeddings-fetch.ts
Normal file
157
templates/embeddings-fetch.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Gemini Embeddings with Fetch (Cloudflare Workers)
|
||||
*
|
||||
* Demonstrates embedding generation using fetch API instead of SDK.
|
||||
* Perfect for Cloudflare Workers and edge environments.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Add GEMINI_API_KEY to wrangler.jsonc secrets
|
||||
* 2. npx wrangler secret put GEMINI_API_KEY
|
||||
* 3. Deploy: npx wrangler deploy
|
||||
*
|
||||
* Usage:
|
||||
* GET /?text=your+text+here
|
||||
*/
|
||||
|
||||
interface Env {
|
||||
GEMINI_API_KEY: string;
|
||||
}
|
||||
|
||||
interface EmbeddingRequest {
|
||||
content: {
|
||||
parts: Array<{ text: string }>;
|
||||
};
|
||||
taskType?: string;
|
||||
outputDimensionality?: number;
|
||||
}
|
||||
|
||||
interface EmbeddingResponse {
|
||||
embedding: {
|
||||
values: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// CORS headers for browser access
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
};
|
||||
|
||||
// Handle CORS preflight
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get text from query param or request body
|
||||
const url = new URL(request.url);
|
||||
let text: string;
|
||||
|
||||
if (request.method === 'POST') {
|
||||
const body = await request.json<{ text: string }>();
|
||||
text = body.text;
|
||||
} else {
|
||||
text = url.searchParams.get('text') || 'What is the meaning of life?';
|
||||
}
|
||||
|
||||
console.log(`Generating embedding for: "${text}"`);
|
||||
|
||||
// Prepare request
|
||||
const embeddingRequest: EmbeddingRequest = {
|
||||
content: {
|
||||
parts: [{ text }]
|
||||
},
|
||||
taskType: 'SEMANTIC_SIMILARITY',
|
||||
outputDimensionality: 768
|
||||
};
|
||||
|
||||
// Call Gemini API
|
||||
const response = await fetch(
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-goog-api-key': env.GEMINI_API_KEY,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(embeddingRequest)
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Gemini API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json<EmbeddingResponse>();
|
||||
const embedding = data.embedding.values;
|
||||
|
||||
// Calculate vector magnitude
|
||||
const magnitude = Math.sqrt(
|
||||
embedding.reduce((sum, v) => sum + v * v, 0)
|
||||
);
|
||||
|
||||
// Return formatted response
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
text,
|
||||
embedding: {
|
||||
dimensions: embedding.length,
|
||||
magnitude: magnitude.toFixed(4),
|
||||
firstValues: embedding.slice(0, 10).map(v => parseFloat(v.toFixed(4))),
|
||||
fullVector: embedding
|
||||
}
|
||||
}, null, 2), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...corsHeaders
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error:', error.message);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message,
|
||||
hint: error.message.includes('401')
|
||||
? 'Check GEMINI_API_KEY secret is set'
|
||||
: error.message.includes('429')
|
||||
? 'Rate limit exceeded (Free tier: 100 RPM)'
|
||||
: 'Check error message for details'
|
||||
}, null, 2), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...corsHeaders
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Example wrangler.jsonc configuration:
|
||||
*
|
||||
* {
|
||||
* "name": "gemini-embeddings-worker",
|
||||
* "main": "src/index.ts",
|
||||
* "compatibility_date": "2025-10-25",
|
||||
* "vars": {
|
||||
* "ENVIRONMENT": "production"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Set secret:
|
||||
* npx wrangler secret put GEMINI_API_KEY
|
||||
*
|
||||
* Test locally:
|
||||
* npx wrangler dev
|
||||
* curl "http://localhost:8787/?text=Hello+world"
|
||||
*
|
||||
* Deploy:
|
||||
* npx wrangler deploy
|
||||
*/
|
||||
22
templates/package.json
Normal file
22
templates/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "gemini-embeddings-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Google Gemini embeddings API examples",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
361
templates/rag-with-vectorize.ts
Normal file
361
templates/rag-with-vectorize.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Complete RAG Implementation with Gemini Embeddings + Cloudflare Vectorize
|
||||
*
|
||||
* Demonstrates end-to-end RAG (Retrieval Augmented Generation):
|
||||
* 1. Document ingestion (chunking + embedding + storage)
|
||||
* 2. Query processing (embedding + vector search)
|
||||
* 3. Response generation (context + LLM)
|
||||
*
|
||||
* Setup:
|
||||
* 1. Create Vectorize index:
|
||||
* npx wrangler vectorize create gemini-embeddings --dimensions 768 --metric cosine
|
||||
*
|
||||
* 2. Add to wrangler.jsonc:
|
||||
* {
|
||||
* "vectorize": {
|
||||
* "bindings": [{
|
||||
* "binding": "VECTORIZE",
|
||||
* "index_name": "gemini-embeddings"
|
||||
* }]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 3. Set secret:
|
||||
* npx wrangler secret put GEMINI_API_KEY
|
||||
*
|
||||
* 4. Deploy:
|
||||
* npx wrangler deploy
|
||||
*
|
||||
* Usage:
|
||||
* POST /ingest - Upload documents
|
||||
* POST /query - Ask questions
|
||||
* GET /health - Check status
|
||||
*/
|
||||
|
||||
interface Env {
|
||||
GEMINI_API_KEY: string;
|
||||
VECTORIZE: VectorizeIndex;
|
||||
}
|
||||
|
||||
interface VectorizeVector {
|
||||
id: string;
|
||||
values: number[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface VectorizeMatch {
|
||||
id: string;
|
||||
score: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface VectorizeIndex {
|
||||
insert(vectors: VectorizeVector[]): Promise<{ count: number }>;
|
||||
query(
|
||||
vector: number[],
|
||||
options: { topK: number; returnMetadata?: boolean }
|
||||
): Promise<{ matches: VectorizeMatch[] }>;
|
||||
getByIds(ids: string[]): Promise<VectorizeVector[]>;
|
||||
deleteByIds(ids: string[]): Promise<{ count: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Document chunking for better retrieval
|
||||
*/
|
||||
function chunkDocument(
|
||||
text: string,
|
||||
chunkSize: number = 500,
|
||||
overlap: number = 50
|
||||
): string[] {
|
||||
const words = text.split(/\s+/);
|
||||
const chunks: string[] = [];
|
||||
|
||||
for (let i = 0; i < words.length; i += chunkSize - overlap) {
|
||||
const chunk = words.slice(i, i + chunkSize).join(' ');
|
||||
if (chunk.trim().length > 0) {
|
||||
chunks.push(chunk.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate embedding using Gemini API
|
||||
*/
|
||||
async function generateEmbedding(
|
||||
text: string,
|
||||
apiKey: string,
|
||||
taskType: string = 'RETRIEVAL_DOCUMENT'
|
||||
): Promise<number[]> {
|
||||
const response = await fetch(
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-goog-api-key': apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: { parts: [{ text }] },
|
||||
taskType,
|
||||
outputDimensionality: 768 // MUST match Vectorize index dimensions
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Embedding API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json<{ embedding: { values: number[] } }>();
|
||||
return data.embedding.values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate response using Gemini API
|
||||
*/
|
||||
async function generateResponse(
|
||||
context: string,
|
||||
query: string,
|
||||
apiKey: string
|
||||
): Promise<string> {
|
||||
const response = await fetch(
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-goog-api-key': apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{
|
||||
parts: [{
|
||||
text: `You are a helpful assistant. Answer the question based ONLY on the provided context.
|
||||
|
||||
Context:
|
||||
${context}
|
||||
|
||||
Question: ${query}
|
||||
|
||||
Answer:`
|
||||
}]
|
||||
}]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Generation API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json<{
|
||||
candidates: Array<{
|
||||
content: { parts: Array<{ text: string }> };
|
||||
}>;
|
||||
}>();
|
||||
|
||||
return data.candidates[0]?.content?.parts[0]?.text || 'No response generated';
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
};
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
// Health check
|
||||
if (url.pathname === '/health') {
|
||||
return new Response(JSON.stringify({
|
||||
status: 'ok',
|
||||
vectorize: 'connected',
|
||||
gemini: 'ready'
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
// Document ingestion
|
||||
if (url.pathname === '/ingest' && request.method === 'POST') {
|
||||
const { documents } = await request.json<{ documents: Array<{ id: string; text: string }> }>();
|
||||
|
||||
if (!documents || !Array.isArray(documents)) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid request: documents array required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📥 Ingesting ${documents.length} documents...`);
|
||||
|
||||
const vectors: VectorizeVector[] = [];
|
||||
|
||||
for (const doc of documents) {
|
||||
// Chunk document
|
||||
const chunks = chunkDocument(doc.text, 500, 50);
|
||||
console.log(`📄 Document ${doc.id}: ${chunks.length} chunks`);
|
||||
|
||||
// Generate embeddings for each chunk
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const embedding = await generateEmbedding(
|
||||
chunks[i],
|
||||
env.GEMINI_API_KEY,
|
||||
'RETRIEVAL_DOCUMENT' // ← Documents for indexing
|
||||
);
|
||||
|
||||
vectors.push({
|
||||
id: `${doc.id}-chunk-${i}`,
|
||||
values: embedding,
|
||||
metadata: {
|
||||
documentId: doc.id,
|
||||
chunkIndex: i,
|
||||
text: chunks[i],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Insert into Vectorize
|
||||
const result = await env.VECTORIZE.insert(vectors);
|
||||
|
||||
console.log(`✅ Ingested ${result.count} vectors`);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
documentsProcessed: documents.length,
|
||||
chunksCreated: vectors.length,
|
||||
vectorsInserted: result.count
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
// Query processing (RAG)
|
||||
if (url.pathname === '/query' && request.method === 'POST') {
|
||||
const { query, topK = 5 } = await request.json<{ query: string; topK?: number }>();
|
||||
|
||||
if (!query) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid request: query required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 Query: "${query}"`);
|
||||
|
||||
// 1. Generate query embedding
|
||||
const queryEmbedding = await generateEmbedding(
|
||||
query,
|
||||
env.GEMINI_API_KEY,
|
||||
'RETRIEVAL_QUERY' // ← Query, not document
|
||||
);
|
||||
|
||||
// 2. Search Vectorize for similar chunks
|
||||
const results = await env.VECTORIZE.query(queryEmbedding, {
|
||||
topK,
|
||||
returnMetadata: true
|
||||
});
|
||||
|
||||
if (results.matches.length === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
answer: 'No relevant information found in the knowledge base.',
|
||||
sources: []
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📚 Found ${results.matches.length} relevant chunks`);
|
||||
|
||||
// 3. Extract context from top matches
|
||||
const context = results.matches
|
||||
.map((match, i) => `[${i + 1}] ${match.metadata?.text || ''}`)
|
||||
.join('\n\n');
|
||||
|
||||
// 4. Generate response with context
|
||||
const answer = await generateResponse(context, query, env.GEMINI_API_KEY);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
query,
|
||||
answer,
|
||||
sources: results.matches.map(match => ({
|
||||
documentId: match.metadata?.documentId,
|
||||
chunkIndex: match.metadata?.chunkIndex,
|
||||
similarity: match.score,
|
||||
text: match.metadata?.text
|
||||
}))
|
||||
}, null, 2), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
// 404 for unknown routes
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Not found',
|
||||
routes: {
|
||||
'POST /ingest': 'Upload documents',
|
||||
'POST /query': 'Ask questions',
|
||||
'GET /health': 'Health check'
|
||||
}
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error:', error.message);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Example requests:
|
||||
*
|
||||
* 1. Ingest documents:
|
||||
* curl -X POST https://your-worker.workers.dev/ingest \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -d '{
|
||||
* "documents": [
|
||||
* {
|
||||
* "id": "doc1",
|
||||
* "text": "Paris is the capital of France. It is known for the Eiffel Tower..."
|
||||
* },
|
||||
* {
|
||||
* "id": "doc2",
|
||||
* "text": "Machine learning is a subset of artificial intelligence..."
|
||||
* }
|
||||
* ]
|
||||
* }'
|
||||
*
|
||||
* 2. Query:
|
||||
* curl -X POST https://your-worker.workers.dev/query \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -d '{
|
||||
* "query": "What is the capital of France?",
|
||||
* "topK": 5
|
||||
* }'
|
||||
*
|
||||
* 3. Health check:
|
||||
* curl https://your-worker.workers.dev/health
|
||||
*/
|
||||
289
templates/semantic-search.ts
Normal file
289
templates/semantic-search.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Semantic Search with Gemini Embeddings
|
||||
*
|
||||
* Demonstrates semantic similarity search using cosine similarity.
|
||||
* Finds documents based on meaning, not just keyword matching.
|
||||
*
|
||||
* Setup:
|
||||
* 1. npm install @google/genai@^1.27.0
|
||||
* 2. export GEMINI_API_KEY="your-api-key"
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx semantic-search.ts
|
||||
*/
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
text: string;
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
document: Document;
|
||||
similarity: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cosine similarity between two vectors
|
||||
* Returns value between -1 and 1, where 1 = identical
|
||||
*/
|
||||
function cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error(`Vector dimensions must match: ${a.length} vs ${b.length}`);
|
||||
}
|
||||
|
||||
let dotProduct = 0;
|
||||
let magnitudeA = 0;
|
||||
let magnitudeB = 0;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dotProduct += a[i] * b[i];
|
||||
magnitudeA += a[i] * a[i];
|
||||
magnitudeB += b[i] * b[i];
|
||||
}
|
||||
|
||||
if (magnitudeA === 0 || magnitudeB === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return dotProduct / (Math.sqrt(magnitudeA) * Math.sqrt(magnitudeB));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize vector to unit length
|
||||
* Useful for faster similarity calculations
|
||||
*/
|
||||
function normalizeVector(vector: number[]): number[] {
|
||||
const magnitude = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
||||
|
||||
if (magnitude === 0) {
|
||||
return vector;
|
||||
}
|
||||
|
||||
return vector.map(v => v / magnitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dot product (for normalized vectors only)
|
||||
*/
|
||||
function dotProduct(a: number[], b: number[]): number {
|
||||
return a.reduce((sum, val, i) => sum + val * b[i], 0);
|
||||
}
|
||||
|
||||
class SemanticSearch {
|
||||
private ai: GoogleGenAI;
|
||||
private documents: Document[] = [];
|
||||
private normalized: boolean = false;
|
||||
|
||||
constructor(apiKey: string, normalized: boolean = false) {
|
||||
this.ai = new GoogleGenAI({ apiKey });
|
||||
this.normalized = normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index documents (generate and store embeddings)
|
||||
*/
|
||||
async indexDocuments(documents: Array<{ id: string; text: string }>): Promise<void> {
|
||||
console.log(`\n📚 Indexing ${documents.length} documents...\n`);
|
||||
|
||||
for (const doc of documents) {
|
||||
const response = await this.ai.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
content: doc.text,
|
||||
config: {
|
||||
taskType: 'RETRIEVAL_DOCUMENT', // ← Documents for indexing
|
||||
outputDimensionality: 768
|
||||
}
|
||||
});
|
||||
|
||||
let embedding = response.embedding.values;
|
||||
|
||||
// Normalize if requested (faster similarity calculation)
|
||||
if (this.normalized) {
|
||||
embedding = normalizeVector(embedding);
|
||||
}
|
||||
|
||||
this.documents.push({
|
||||
id: doc.id,
|
||||
text: doc.text,
|
||||
embedding
|
||||
});
|
||||
|
||||
console.log(`✅ Indexed: ${doc.id}`);
|
||||
}
|
||||
|
||||
console.log(`\n✨ Indexing complete! ${this.documents.length} documents ready.\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for similar documents
|
||||
*/
|
||||
async search(query: string, topK: number = 5): Promise<SearchResult[]> {
|
||||
if (this.documents.length === 0) {
|
||||
throw new Error('No documents indexed. Call indexDocuments() first.');
|
||||
}
|
||||
|
||||
console.log(`🔍 Searching for: "${query}"\n`);
|
||||
|
||||
// Generate query embedding
|
||||
const response = await this.ai.models.embedContent({
|
||||
model: 'gemini-embedding-001',
|
||||
content: query,
|
||||
config: {
|
||||
taskType: 'RETRIEVAL_QUERY', // ← Query, not document
|
||||
outputDimensionality: 768
|
||||
}
|
||||
});
|
||||
|
||||
let queryEmbedding = response.embedding.values;
|
||||
|
||||
if (this.normalized) {
|
||||
queryEmbedding = normalizeVector(queryEmbedding);
|
||||
}
|
||||
|
||||
// Calculate similarity for each document
|
||||
const results: SearchResult[] = this.documents.map(doc => ({
|
||||
document: doc,
|
||||
similarity: this.normalized
|
||||
? dotProduct(queryEmbedding, doc.embedding!)
|
||||
: cosineSimilarity(queryEmbedding, doc.embedding!)
|
||||
}));
|
||||
|
||||
// Sort by similarity (descending) and return top K
|
||||
return results
|
||||
.sort((a, b) => b.similarity - a.similarity)
|
||||
.slice(0, topK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find similar documents to a given document
|
||||
*/
|
||||
findSimilar(documentId: string, topK: number = 5): SearchResult[] {
|
||||
const doc = this.documents.find(d => d.id === documentId);
|
||||
|
||||
if (!doc || !doc.embedding) {
|
||||
throw new Error(`Document not found: ${documentId}`);
|
||||
}
|
||||
|
||||
const results: SearchResult[] = this.documents
|
||||
.filter(d => d.id !== documentId) // Exclude the document itself
|
||||
.map(d => ({
|
||||
document: d,
|
||||
similarity: this.normalized
|
||||
? dotProduct(doc.embedding!, d.embedding!)
|
||||
: cosineSimilarity(doc.embedding!, d.embedding!)
|
||||
}));
|
||||
|
||||
return results
|
||||
.sort((a, b) => b.similarity - a.similarity)
|
||||
.slice(0, topK);
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
async function main() {
|
||||
try {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('GEMINI_API_KEY environment variable not set');
|
||||
}
|
||||
|
||||
// Initialize search engine
|
||||
const search = new SemanticSearch(apiKey, false); // Set true for normalized vectors
|
||||
|
||||
// Sample documents
|
||||
const documents = [
|
||||
{
|
||||
id: 'doc1',
|
||||
text: 'Paris is the capital of France. It is known for the Eiffel Tower and the Louvre Museum.'
|
||||
},
|
||||
{
|
||||
id: 'doc2',
|
||||
text: 'Machine learning is a subset of artificial intelligence that enables computers to learn from data.'
|
||||
},
|
||||
{
|
||||
id: 'doc3',
|
||||
text: 'The Eiffel Tower is an iconic landmark in Paris, France, built in 1889.'
|
||||
},
|
||||
{
|
||||
id: 'doc4',
|
||||
text: 'Deep learning uses neural networks with multiple layers to learn complex patterns in data.'
|
||||
},
|
||||
{
|
||||
id: 'doc5',
|
||||
text: 'London is the capital of the United Kingdom and home to Big Ben and Buckingham Palace.'
|
||||
},
|
||||
{
|
||||
id: 'doc6',
|
||||
text: 'Natural language processing is a branch of AI that helps computers understand human language.'
|
||||
}
|
||||
];
|
||||
|
||||
// Index documents
|
||||
await search.indexDocuments(documents);
|
||||
|
||||
// Example 1: Search by query
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Example 1: Search by Query');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const query1 = "What is the capital of France?";
|
||||
const results1 = await search.search(query1, 3);
|
||||
|
||||
results1.forEach((result, i) => {
|
||||
console.log(`${i + 1}. [${(result.similarity * 100).toFixed(1)}%] ${result.document.id}`);
|
||||
console.log(` ${result.document.text}\n`);
|
||||
});
|
||||
|
||||
// Example 2: Different query
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Example 2: AI-related Query');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const query2 = "Tell me about artificial intelligence";
|
||||
const results2 = await search.search(query2, 3);
|
||||
|
||||
results2.forEach((result, i) => {
|
||||
console.log(`${i + 1}. [${(result.similarity * 100).toFixed(1)}%] ${result.document.id}`);
|
||||
console.log(` ${result.document.text}\n`);
|
||||
});
|
||||
|
||||
// Example 3: Find similar documents
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Example 3: Find Similar to doc1 (Paris)');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const similar = search.findSimilar('doc1', 3);
|
||||
|
||||
similar.forEach((result, i) => {
|
||||
console.log(`${i + 1}. [${(result.similarity * 100).toFixed(1)}%] ${result.document.id}`);
|
||||
console.log(` ${result.document.text}\n`);
|
||||
});
|
||||
|
||||
// Example 4: Demonstrate semantic vs keyword matching
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('Example 4: Semantic Understanding');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('Query: "neural networks" (no exact keyword match in any document)\n');
|
||||
|
||||
const query3 = "neural networks";
|
||||
const results3 = await search.search(query3, 3);
|
||||
|
||||
results3.forEach((result, i) => {
|
||||
const hasKeyword = result.document.text.toLowerCase().includes('neural');
|
||||
console.log(`${i + 1}. [${(result.similarity * 100).toFixed(1)}%] ${result.document.id} ${hasKeyword ? '✓ keyword' : '✗ no keyword'}`);
|
||||
console.log(` ${result.document.text}\n`);
|
||||
});
|
||||
|
||||
console.log('📊 Note: High similarity even without exact keyword match!');
|
||||
console.log('This demonstrates semantic understanding.\n');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user