Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:54 +08:00
commit 7927519669
17 changed files with 4377 additions and 0 deletions

View 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();

View 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
View 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.
*/

View 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
View 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"
}
}

View 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
*/

View 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();