diff --git a/nvidia/txt2kg/assets/deploy/app/qdrant-init.sh b/nvidia/txt2kg/assets/deploy/app/qdrant-init.sh new file mode 100644 index 0000000..050f215 --- /dev/null +++ b/nvidia/txt2kg/assets/deploy/app/qdrant-init.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +# Script to initialize Qdrant collection at container startup +echo "Initializing Qdrant collection..." + +# Wait for the Qdrant service to become available +echo "Waiting for Qdrant service to start..." +max_attempts=30 +attempt=1 + +while [ $attempt -le $max_attempts ]; do + if curl -s http://qdrant:6333/healthz > /dev/null; then + echo "Qdrant service is up!" + break + fi + echo "Waiting for Qdrant service (attempt $attempt/$max_attempts)..." + attempt=$((attempt + 1)) + sleep 2 +done + +if [ $attempt -gt $max_attempts ]; then + echo "Timed out waiting for Qdrant service" + exit 1 +fi + +# Check if collection already exists +echo "Checking if collection 'entity-embeddings' exists..." +COLLECTION_EXISTS=$(curl -s http://qdrant:6333/collections/entity-embeddings | grep -c '"status":"ok"' || echo "0") + +if [ "$COLLECTION_EXISTS" -gt "0" ]; then + echo "Collection 'entity-embeddings' already exists, skipping creation" +else + # Create the collection + echo "Creating collection 'entity-embeddings'..." + curl -X PUT "http://qdrant:6333/collections/entity-embeddings" \ + -H "Content-Type: application/json" \ + -d '{ + "vectors": { + "size": 384, + "distance": "Cosine" + } + }' + + if [ $? -eq 0 ]; then + echo "✅ Collection 'entity-embeddings' created successfully" + else + echo "❌ Failed to create collection" + exit 1 + fi +fi + +echo "Qdrant initialization complete" diff --git a/nvidia/txt2kg/assets/deploy/compose/docker-compose.yml b/nvidia/txt2kg/assets/deploy/compose/docker-compose.yml index 9980d7c..1aaa59e 100644 --- a/nvidia/txt2kg/assets/deploy/compose/docker-compose.yml +++ b/nvidia/txt2kg/assets/deploy/compose/docker-compose.yml @@ -9,10 +9,8 @@ services: environment: - ARANGODB_URL=http://arangodb:8529 - ARANGODB_DB=txt2kg - - PINECONE_HOST=entity-embeddings - - PINECONE_PORT=5081 - - PINECONE_API_KEY=pclocal - - PINECONE_ENVIRONMENT=local + - QDRANT_URL=http://qdrant:6333 + - VECTOR_DB_TYPE=qdrant - LANGCHAIN_TRACING_V2=true - SENTENCE_TRANSFORMER_URL=http://sentence-transformers:80 - MODEL_NAME=all-MiniLM-L6-v2 @@ -109,29 +107,52 @@ services: restart: unless-stopped profiles: - vector-search # Only start with: docker compose --profile vector-search up - entity-embeddings: - image: ghcr.io/pinecone-io/pinecone-index:latest - container_name: entity-embeddings - environment: - PORT: 5081 - INDEX_TYPE: serverless - VECTOR_TYPE: dense - DIMENSION: 384 - METRIC: cosine - INDEX_NAME: entity-embeddings + qdrant: + image: qdrant/qdrant:latest + container_name: qdrant ports: - - "5081:5081" - platform: linux/amd64 + - "6333:6333" + - "6334:6334" + volumes: + - qdrant_data:/qdrant/storage networks: - pinecone-net restart: unless-stopped profiles: - vector-search # Only start with: docker compose --profile vector-search up + qdrant-init: + image: curlimages/curl:latest + depends_on: + - qdrant + restart: "no" + entrypoint: /bin/sh + command: + - -c + - | + echo 'Waiting for Qdrant to start...' + sleep 5 + echo 'Checking if collection exists...' + RESPONSE=$(curl -s http://qdrant:6333/collections/entity-embeddings) + if echo "$RESPONSE" | grep -q '"status":"ok"'; then + echo 'Collection already exists' + else + echo 'Creating collection entity-embeddings...' + curl -X PUT http://qdrant:6333/collections/entity-embeddings \ + -H 'Content-Type: application/json' \ + -d '{"vectors":{"size":384,"distance":"Cosine"}}' + echo '' + echo 'Collection created successfully' + fi + networks: + - pinecone-net + profiles: + - vector-search volumes: arangodb_data: arangodb_apps_data: ollama_data: + qdrant_data: networks: default: diff --git a/nvidia/txt2kg/assets/frontend/app/api/embeddings/route.ts b/nvidia/txt2kg/assets/frontend/app/api/embeddings/route.ts index caf7bb3..fd40190 100644 --- a/nvidia/txt2kg/assets/frontend/app/api/embeddings/route.ts +++ b/nvidia/txt2kg/assets/frontend/app/api/embeddings/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { EmbeddingsService } from '@/lib/embeddings'; -import { PineconeService } from '@/lib/pinecone'; +import { QdrantService } from '@/lib/qdrant'; /** - * Generate embeddings for text chunks and store them in Pinecone + * Generate embeddings for text chunks and store them in Qdrant */ export async function POST(request: NextRequest) { try { @@ -38,15 +38,15 @@ export async function POST(request: NextRequest) { console.log('Generating embeddings for chunks...'); const embeddings = await embeddingsService.encode(chunks); console.log(`Generated ${embeddings.length} embeddings`); - - // Initialize PineconeService - const pineconeService = PineconeService.getInstance(); - - // Check if Pinecone server is running - const isPineconeRunning = await pineconeService.isPineconeRunning(); + + // Initialize QdrantService + const pineconeService = QdrantService.getInstance(); + + // Check if Qdrant server is running + const isPineconeRunning = await pineconeService.isQdrantRunning(); if (!isPineconeRunning) { return NextResponse.json( - { error: 'Pinecone server is not available. Please make sure it is running.' }, + { error: 'Qdrant server is not available. Please make sure it is running.' }, { status: 503 } ); } diff --git a/nvidia/txt2kg/assets/frontend/app/api/metrics/route.ts b/nvidia/txt2kg/assets/frontend/app/api/metrics/route.ts index dc4f343..ee81e2d 100644 --- a/nvidia/txt2kg/assets/frontend/app/api/metrics/route.ts +++ b/nvidia/txt2kg/assets/frontend/app/api/metrics/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import remoteBackendInstance from '@/lib/remote-backend'; import { getGraphDbService } from '@/lib/graph-db-util'; import { getGraphDbType } from '../settings/route'; -import { PineconeService } from '@/lib/pinecone'; +import { QdrantService } from '@/lib/qdrant'; import RAGService from '@/lib/rag'; import queryLoggerService, { QueryLogSummary } from '@/lib/query-logger'; @@ -14,7 +14,7 @@ export async function GET(request: NextRequest) { // Initialize services with the correct graph database type const graphDbType = getGraphDbType(); const graphDbService = getGraphDbService(graphDbType); - const pineconeService = PineconeService.getInstance(); + const pineconeService = QdrantService.getInstance(); // Initialize graph database if needed if (!graphDbService.isInitialized()) { diff --git a/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/clear/route.ts b/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/clear/route.ts index f73d7cf..f0a3973 100644 --- a/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/clear/route.ts +++ b/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/clear/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { PineconeService } from '@/lib/pinecone'; +import { QdrantService } from '@/lib/qdrant'; /** * Clear all data from the Pinecone vector database @@ -7,7 +7,7 @@ import { PineconeService } from '@/lib/pinecone'; */ export async function POST() { // Get the Pinecone service instance - const pineconeService = PineconeService.getInstance(); + const pineconeService = QdrantService.getInstance(); // Clear all vectors from the database const deleteSuccess = await pineconeService.deleteAllEntities(); diff --git a/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/create-index/route.ts b/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/create-index/route.ts index 7ce0f5c..516e71a 100644 --- a/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/create-index/route.ts +++ b/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/create-index/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { PineconeService } from '@/lib/pinecone'; +import { QdrantService } from '@/lib/qdrant'; /** * Create Pinecone index API endpoint @@ -8,7 +8,7 @@ import { PineconeService } from '@/lib/pinecone'; export async function POST() { try { // Get the Pinecone service instance - const pineconeService = PineconeService.getInstance(); + const pineconeService = QdrantService.getInstance(); // Force re-initialization to create the index (pineconeService as any).initialized = false; diff --git a/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/stats/route.ts b/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/stats/route.ts index a1aa129..5d148c4 100644 --- a/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/stats/route.ts +++ b/nvidia/txt2kg/assets/frontend/app/api/pinecone-diag/stats/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { PineconeService } from '@/lib/pinecone'; +import { QdrantService } from '@/lib/qdrant'; /** * Get Pinecone vector database stats @@ -7,7 +7,7 @@ import { PineconeService } from '@/lib/pinecone'; export async function GET() { try { // Initialize Pinecone service - const pineconeService = PineconeService.getInstance(); + const pineconeService = QdrantService.getInstance(); // We can now directly call getStats() which handles initialization and error recovery const stats = await pineconeService.getStats(); diff --git a/nvidia/txt2kg/assets/frontend/app/api/sentence-embeddings/route.ts b/nvidia/txt2kg/assets/frontend/app/api/sentence-embeddings/route.ts index c23c0bb..54fca25 100644 --- a/nvidia/txt2kg/assets/frontend/app/api/sentence-embeddings/route.ts +++ b/nvidia/txt2kg/assets/frontend/app/api/sentence-embeddings/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { processSentenceEmbeddings, SentenceEmbedding } from '@/lib/text-processor'; -import { PineconeService } from '@/lib/pinecone'; +import { QdrantService } from '@/lib/qdrant'; /** * API endpoint for splitting text into sentences and generating embeddings @@ -49,7 +49,7 @@ export async function POST(req: NextRequest) { }); // Store in Pinecone - const pineconeService = PineconeService.getInstance(); + const pineconeService = QdrantService.getInstance(); await pineconeService.storeEmbeddingsWithMetadata( embeddingsMap, textContentMap, diff --git a/nvidia/txt2kg/assets/frontend/components/database-connection.tsx b/nvidia/txt2kg/assets/frontend/components/database-connection.tsx index 0e1aa75..71aabfa 100644 --- a/nvidia/txt2kg/assets/frontend/components/database-connection.tsx +++ b/nvidia/txt2kg/assets/frontend/components/database-connection.tsx @@ -157,15 +157,20 @@ export function DatabaseConnection({ className }: DatabaseConnectionProps) { try { const response = await fetch('/api/pinecone-diag/stats'); const data = await response.json(); - + if (response.ok) { setVectorStats({ nodes: typeof data.totalVectorCount === 'number' ? data.totalVectorCount : 0, relationships: 0, // Vector DB doesn't store relationships source: data.source || 'unknown', - httpHealthy: data.httpHealthy - }); - + httpHealthy: data.httpHealthy, + // Store additional Qdrant stats + ...(data.status && { status: data.status }), + ...(data.vectorSize && { vectorSize: data.vectorSize }), + ...(data.distance && { distance: data.distance }), + ...(data.url && { url: data.url }), + } as any); + // If we have a healthy HTTP connection, we're connected if (data.httpHealthy) { setVectorConnectionStatus("connected"); @@ -513,21 +518,33 @@ export function DatabaseConnection({ className }: DatabaseConnectionProps) { <>
- Pinecone + Qdrant - direct-http + {(vectorStats as any).url || 'http://qdrant:6333'}
- - {vectorStats.nodes > 0 && ( -
-
- - {vectorStats.nodes.toLocaleString()} vectors -
+ +
+
+ + {vectorStats.nodes.toLocaleString()} vectors indexed
- )} + + {(vectorStats as any).status && ( +
+ + Status: {(vectorStats as any).status} +
+ )} + + {(vectorStats as any).vectorSize && ( +
+ + {(vectorStats as any).vectorSize}d ({(vectorStats as any).distance}) +
+ )} +
)} @@ -546,59 +563,85 @@ export function DatabaseConnection({ className }: DatabaseConnectionProps) { )}
- - + + + + + + +

{vectorConnectionStatus === "checking" ? "Checking..." : "Refresh connection"}

+
+
+
+ {vectorConnectionStatus === "connected" ? ( <> - - + + + + + + +

Disconnect

+
+
+
+ - - - + + + + + + + + +

Clear database

+
+
+
- Clear Pinecone Database + Clear Qdrant Database - Are you sure you want to clear all data from the Pinecone database? This action cannot be undone. + Are you sure you want to clear all data from the Qdrant vector database? This action cannot be undone. Warning - This will permanently delete all vectors from the Pinecone database. + This will permanently delete all vectors from the Qdrant database. -
Or using Docker Compose: - docker-compose restart pinecone + docker compose restart qdrant

)}
@@ -144,13 +144,25 @@ export function PineconeConnection({ className }: PineconeConnectionProps) {
- Vectors: - {stats.nodes} + Qdrant + {(stats as any).url || 'http://qdrant:6333'}
- Source: - {stats.source} local + Vectors: + {stats.nodes} indexed
+ {(stats as any).status && ( +
+ Status: + {(stats as any).status} +
+ )} + {(stats as any).vectorSize && ( +
+ Dimensions: + {(stats as any).vectorSize}d ({(stats as any).distance}) +
+ )}
diff --git a/nvidia/txt2kg/assets/frontend/lib/backend-service.ts b/nvidia/txt2kg/assets/frontend/lib/backend-service.ts index b34f415..5e130ae 100644 --- a/nvidia/txt2kg/assets/frontend/lib/backend-service.ts +++ b/nvidia/txt2kg/assets/frontend/lib/backend-service.ts @@ -1,15 +1,15 @@ import axios from 'axios'; import { GraphDBService, GraphDBType } from './graph-db-service'; -import { PineconeService } from './pinecone'; +import { QdrantService } from './qdrant'; import { getGraphDbService } from './graph-db-util'; import type { Triple } from '@/types/graph'; /** - * Backend service that combines graph database for storage and Pinecone for embeddings + * Backend service that combines graph database for storage and Qdrant for embeddings */ export class BackendService { private graphDBService: GraphDBService; - private pineconeService: PineconeService; + private pineconeService: QdrantService; private sentenceTransformerUrl: string = 'http://sentence-transformers:80'; private modelName: string = 'all-MiniLM-L6-v2'; private static instance: BackendService; @@ -18,7 +18,7 @@ export class BackendService { private constructor() { this.graphDBService = GraphDBService.getInstance(); - this.pineconeService = PineconeService.getInstance(); + this.pineconeService = QdrantService.getInstance(); // Use environment variables if available if (process.env.SENTENCE_TRANSFORMER_URL) { diff --git a/nvidia/txt2kg/assets/frontend/lib/qdrant.ts b/nvidia/txt2kg/assets/frontend/lib/qdrant.ts new file mode 100644 index 0000000..8d12dd3 --- /dev/null +++ b/nvidia/txt2kg/assets/frontend/lib/qdrant.ts @@ -0,0 +1,620 @@ +/** + * Qdrant service for vector embeddings + * Drop-in replacement for PineconeService + */ +import { Document } from "@langchain/core/documents"; +import { randomUUID } from "crypto"; + +// Helper function to generate deterministic UUID from string +function stringToUUID(str: string): string { + // Create a simple hash-based UUID v4 + const hash = str.split('').reduce((acc, char) => { + return ((acc << 5) - acc) + char.charCodeAt(0) | 0; + }, 0); + + // Generate a deterministic UUID from the hash + const hex = Math.abs(hash).toString(16).padStart(32, '0').substring(0, 32); + return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-4${hex.substring(13, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}`; +} + +// Define types for Qdrant requests and responses +interface QdrantPoint { + id: string | number; + vector: number[]; + payload?: Record; +} + +interface QdrantQueryResponse { + result: Array<{ + id: string | number; + score: number; + payload?: Record; + }>; +} + +// Define interface for document search results +export interface DocumentSearchResult { + id: string; + score: number; + metadata?: Record; +} + +export class QdrantService { + private dimension: number = 384; // Dimension for MiniLM-L6-v2 + private static instance: QdrantService; + private initialized: boolean = false; + private collectionName: string = 'entity-embeddings'; + private hostUrl: string; + private isInitializing = false; + + private constructor() { + // Get environment variables with defaults + const qdrantUrl = process.env.QDRANT_URL || 'http://localhost:6333'; + this.hostUrl = qdrantUrl; + + console.log(`Initializing Qdrant service with host: ${this.hostUrl}`); + } + + /** + * Get singleton instance + */ + public static getInstance(): QdrantService { + if (!QdrantService.instance) { + QdrantService.instance = new QdrantService(); + } + return QdrantService.instance; + } + + /** + * Check if the service is initialized + */ + public isInitialized(): boolean { + return this.initialized; + } + + /** + * Make a request to the Qdrant API + */ + private async makeRequest(endpoint: string, method: string = 'GET', body?: any): Promise { + try { + const url = endpoint.startsWith('http') ? endpoint : `${this.hostUrl}${endpoint}`; + + console.log(`Making Qdrant request to: ${url}`); + + const options: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + } + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorText = await response.text(); + console.log(`Qdrant API error (${response.status}) for ${url}: ${errorText}`); + return null; + } + + // For HEAD requests or empty responses + if (method === 'HEAD' || response.headers.get('content-length') === '0') { + return { status: response.status }; + } + + return await response.json(); + } catch (error) { + console.log(`Error in Qdrant API request to ${endpoint} - request failed`); + return null; + } + } + + /** + * Check if the Qdrant server is up and running + */ + private isQdrantRunningCheck = false; + + public async isQdrantRunning(): Promise { + // Prevent concurrent checks that could cause loops + if (this.isQdrantRunningCheck) { + console.log('Already checking if Qdrant is running, returning true to break cycle'); + return true; + } + + this.isQdrantRunningCheck = true; + + try { + // Check Qdrant health endpoint + const response = await fetch(`${this.hostUrl}/healthz`, { + method: 'GET' + }); + + if (response.ok) { + console.log(`Qdrant server is up and healthy`); + this.isQdrantRunningCheck = false; + return true; + } + + console.log('Qdrant health check failed - server might not be running'); + this.isQdrantRunningCheck = false; + return false; + } catch (error) { + console.log('Error checking Qdrant server health - server appears to be down'); + this.isQdrantRunningCheck = false; + return false; + } + } + + /** + * Initialize Qdrant and create collection if needed + */ + public async initialize(forceCreateCollection: boolean = false): Promise { + if ((this.initialized && !forceCreateCollection) || this.isInitializing) { + return; + } + + this.isInitializing = true; + + try { + console.log('Qdrant service initializing...'); + + // Check if Qdrant server is running + const isRunning = await this.isQdrantRunning(); + if (!isRunning) { + console.log('Qdrant server does not appear to be running. Please ensure it is started in Docker.'); + this.isInitializing = false; + return; + } + + // Check if collection exists + const collectionInfo = await this.makeRequest(`/collections/${this.collectionName}`, 'GET'); + + if (!collectionInfo || collectionInfo.status === 'error') { + // Create collection + console.log(`Creating Qdrant collection: ${this.collectionName}`); + const createResult = await this.makeRequest(`/collections/${this.collectionName}`, 'PUT', { + vectors: { + size: this.dimension, + distance: 'Cosine' + } + }); + + if (createResult && createResult.result === true) { + console.log(`Created Qdrant collection with ${this.dimension} dimensions`); + this.initialized = true; + } else { + console.log('Failed to create Qdrant collection - continuing without initialization'); + } + } else { + // Collection exists + const vectorCount = collectionInfo.result?.points_count || 0; + console.log(`Connected to Qdrant collection with ${vectorCount} vectors`); + this.initialized = true; + } + + this.isInitializing = false; + console.log('Qdrant service initialization completed'); + } catch (error) { + console.log('Error during Qdrant service initialization - continuing without connection'); + this.isInitializing = false; + } + } + + /** + * Store embeddings for entities + */ + public async storeEmbeddings( + entityEmbeddings: Map, + textContentMap?: Map + ): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - skipping embedding storage'); + return; + } + + try { + const points: QdrantPoint[] = []; + + // Convert to Qdrant point format + for (const [entityName, embedding] of entityEmbeddings.entries()) { + const point: QdrantPoint = { + id: stringToUUID(entityName), // Convert string ID to UUID + vector: embedding, + payload: { + originalId: entityName, // Store original ID in payload for retrieval + text: textContentMap?.get(entityName) || entityName, + type: 'entity' + } + }; + points.push(point); + } + + // Use batching for efficient upserts + const batchSize = 100; + for (let i = 0; i < points.length; i += batchSize) { + const batch = points.slice(i, i + batchSize); + + const success = await this.upsertVectors(batch); + if (success) { + console.log(`Upserted batch ${Math.floor(i/batchSize) + 1} of ${Math.ceil(points.length/batchSize)}`); + } else { + console.log(`Failed to upsert batch ${Math.floor(i/batchSize) + 1} - continuing`); + } + } + + console.log(`Completed embedding storage attempt for ${points.length} embeddings`); + } catch (error) { + console.log('Error storing embeddings - continuing without storage'); + } + } + + /** + * Upsert vectors to Qdrant + */ + public async upsertVectors(points: QdrantPoint[]): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - skipping vector upsert'); + return false; + } + + try { + console.log(`Upserting ${points.length} vectors to Qdrant`); + + const response = await this.makeRequest(`/collections/${this.collectionName}/points`, 'PUT', { + points: points + }); + + if (!response || response.status === 'error') { + console.log(`Qdrant upsert failed`); + return false; + } + + console.log(`Successfully upserted ${points.length} vectors`); + return true; + } catch (error) { + console.log('Error upserting vectors to Qdrant - continuing without storage'); + return false; + } + } + + /** + * Store embeddings with metadata + */ + public async storeEmbeddingsWithMetadata( + embeddings: Map, + textContent: Map, + metadata: Map + ): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - skipping embedding storage with metadata'); + return; + } + + try { + const points: QdrantPoint[] = []; + + // Convert to Qdrant point format + for (const [key, embedding] of embeddings.entries()) { + const point: QdrantPoint = { + id: stringToUUID(key), // Convert string ID to UUID + vector: embedding, + payload: { + originalId: key, // Store original ID in payload for retrieval + text: textContent.get(key) || '', + ...metadata.get(key) || {} + } + }; + points.push(point); + } + + // Use batching for efficient upserts + const batchSize = 100; + for (let i = 0; i < points.length; i += batchSize) { + const batch = points.slice(i, i + batchSize); + + const success = await this.upsertVectors(batch); + if (success) { + console.log(`Upserted batch ${Math.floor(i/batchSize) + 1} of ${Math.ceil(points.length/batchSize)}`); + } else { + console.log(`Failed to upsert batch ${Math.floor(i/batchSize) + 1} - continuing`); + } + } + + console.log(`Completed embedding storage attempt for ${points.length} embeddings with metadata`); + } catch (error) { + console.log('Error storing embeddings with metadata - continuing without storage'); + } + } + + /** + * Find similar entities to a query embedding + */ + public async findSimilarEntitiesWithMetadata( + embedding: number[], + limit: number = 10 + ): Promise<{ entities: string[], metadata: Map }> { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - returning empty results'); + return { entities: [], metadata: new Map() }; + } + + try { + const queryResponse = await this.queryVectors(embedding, limit, true); + + if (!queryResponse) { + return { entities: [], metadata: new Map() }; + } + + // Extract entities and metadata, using originalId from payload + const entities = queryResponse.result.map(match => + match.payload?.originalId || String(match.id) + ); + const metadataMap = new Map(); + + queryResponse.result.forEach(match => { + const originalId = match.payload?.originalId || String(match.id); + metadataMap.set(originalId, { + ...match.payload, + score: match.score + }); + }); + + return { entities, metadata: metadataMap }; + } catch (error) { + console.log('Error finding similar entities - returning empty results'); + return { entities: [], metadata: new Map() }; + } + } + + /** + * Query vectors in Qdrant + */ + private async queryVectors( + vector: number[], + limit: number = 10, + withPayload: boolean = false + ): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - cannot query vectors'); + return null; + } + + try { + const response = await this.makeRequest(`/collections/${this.collectionName}/points/query`, 'POST', { + query: vector, + limit: limit, + with_payload: withPayload + }); + + if (!response || response.status === 'error') { + console.log(`Qdrant query failed`); + return null; + } + + return response; + } catch (error) { + console.log('Error querying vectors from Qdrant - returning null'); + return null; + } + } + + /** + * Find similar entities to a query embedding + */ + public async findSimilarEntities(queryEmbedding: number[], topK: number = 10): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - returning empty entity list'); + return []; + } + + try { + const queryResponse = await this.queryVectors(queryEmbedding, topK, true); + if (!queryResponse) { + return []; + } + return queryResponse.result.map(match => + match.payload?.originalId || String(match.id) + ); + } catch (error) { + console.log('Error finding similar entities - returning empty list'); + return []; + } + } + + /** + * Get all entities in the collection (up to limit) + */ + public async getAllEntities(limit: number = 1000): Promise { + if (!this.initialized) { + await this.initialize(); + } + + try { + // Qdrant doesn't have a direct "get all" like Pinecone + // We'll use scroll API to get points + const response = await this.makeRequest(`/collections/${this.collectionName}/points/scroll`, 'POST', { + limit: limit, + with_payload: false, + with_vector: false + }); + + if (!response || !response.result || !response.result.points) { + return []; + } + + return response.result.points.map((point: any) => String(point.id)); + } catch (error) { + console.error('Error getting all entities:', error); + return []; + } + } + + /** + * Delete entities from the collection + */ + public async deleteEntities(entityIds: string[]): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - cannot delete entities'); + return false; + } + + try { + console.log(`Deleting ${entityIds.length} entities from Qdrant`); + + const response = await this.makeRequest(`/collections/${this.collectionName}/points/delete`, 'POST', { + points: entityIds + }); + + if (!response || response.status === 'error') { + console.log(`Qdrant delete failed`); + return false; + } + + console.log(`Successfully deleted ${entityIds.length} entities`); + return true; + } catch (error) { + console.log('Error deleting entities from Qdrant - operation failed'); + return false; + } + } + + /** + * Get collection statistics from Qdrant + */ + public async getStats(): Promise { + try { + console.log('Getting stats from Qdrant...'); + const response = await this.makeRequest(`/collections/${this.collectionName}`, 'GET'); + + if (response && response.result) { + const stats = response.result; + console.log('Successfully retrieved stats from Qdrant'); + return { + totalVectorCount: stats.points_count || 0, + indexedVectorCount: stats.indexed_vectors_count || 0, + status: stats.status || 'unknown', + optimizerStatus: stats.optimizer_status || 'unknown', + vectorSize: stats.config?.params?.vectors?.size || this.dimension, + distance: stats.config?.params?.vectors?.distance || 'Cosine', + source: 'qdrant', + httpHealthy: true, + url: this.hostUrl + }; + } else { + console.log(`Qdrant stats request failed`); + return { + totalVectorCount: 0, + source: 'error', + httpHealthy: false, + error: 'Failed to get stats' + }; + } + } catch (error) { + console.log('Qdrant connection failed - server may not be running'); + return { + totalVectorCount: 0, + source: 'error', + httpHealthy: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Delete all entities in the collection + */ + public async deleteAllEntities(): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - cannot delete all entities'); + return false; + } + + try { + console.log('Deleting all entities from Qdrant'); + + // Delete the entire collection and recreate it + const deleteResult = await this.makeRequest(`/collections/${this.collectionName}`, 'DELETE'); + + if (!deleteResult || deleteResult.status === 'error') { + console.log(`Qdrant delete collection failed`); + return false; + } + + // Recreate the collection + await this.initialize(true); + + console.log('Successfully deleted all entities from Qdrant'); + return true; + } catch (error) { + console.log('Error deleting all entities from Qdrant - operation failed'); + return false; + } + } + + /** + * Find similar documents to a query embedding + * @param queryEmbedding Query embedding vector + * @param topK Number of results to return + * @returns Promise resolving to array of document search results + */ + public async findSimilarDocuments(queryEmbedding: number[], topK: number = 10): Promise { + if (!this.initialized) { + await this.initialize(); + } + + if (!this.initialized) { + console.log('Qdrant not available - returning empty document results'); + return []; + } + + try { + const queryResponse = await this.queryVectors(queryEmbedding, topK, true); + if (!queryResponse) { + return []; + } + return queryResponse.result.map(match => ({ + id: match.payload?.originalId || String(match.id), + score: match.score, + metadata: match.payload + })); + } catch (error) { + console.log('Error finding similar documents - returning empty results'); + return []; + } + } +} diff --git a/nvidia/txt2kg/assets/frontend/lib/rag.ts b/nvidia/txt2kg/assets/frontend/lib/rag.ts index 2165fc0..f9bb117 100644 --- a/nvidia/txt2kg/assets/frontend/lib/rag.ts +++ b/nvidia/txt2kg/assets/frontend/lib/rag.ts @@ -1,6 +1,6 @@ /** - * Retrieval Augmented Generation (RAG) implementation using Pinecone and LangChain - * This module provides a RetrievalQA chain using Pinecone as the vector store + * Retrieval Augmented Generation (RAG) implementation using Qdrant and LangChain + * This module provides a RetrievalQA chain using Qdrant as the vector store * Note: xAI integration has been removed - needs alternative LLM provider implementation */ @@ -9,11 +9,11 @@ import { Document } from "@langchain/core/documents"; import { RunnableSequence } from "@langchain/core/runnables"; import { StringOutputParser } from "@langchain/core/output_parsers"; import { PromptTemplate } from "@langchain/core/prompts"; -import { PineconeService, DocumentSearchResult } from './pinecone'; +import { QdrantService, DocumentSearchResult } from './qdrant'; import { EmbeddingsService } from './embeddings'; -// Interface for records to store in Pinecone -interface PineconeRecord { +// Interface for records to store in Qdrant +interface QdrantRecord { id: string; values: number[]; metadata?: Record; @@ -21,14 +21,14 @@ interface PineconeRecord { export class RAGService { private static instance: RAGService; - private pineconeService: PineconeService; + private pineconeService: QdrantService; private embeddingsService: EmbeddingsService; private llm: ChatOpenAI | null = null; private initialized: boolean = false; private isInitializing: boolean = false; private constructor() { - this.pineconeService = PineconeService.getInstance(); + this.pineconeService = QdrantService.getInstance(); this.embeddingsService = EmbeddingsService.getInstance(); } diff --git a/nvidia/txt2kg/assets/frontend/lib/remote-backend.ts b/nvidia/txt2kg/assets/frontend/lib/remote-backend.ts index 9b1d13e..e61be38 100644 --- a/nvidia/txt2kg/assets/frontend/lib/remote-backend.ts +++ b/nvidia/txt2kg/assets/frontend/lib/remote-backend.ts @@ -1,18 +1,18 @@ import { GraphDBService, GraphDBType } from './graph-db-service'; -import { PineconeService } from './pinecone'; +import { QdrantService } from './qdrant'; import { EmbeddingsService } from './embeddings'; import { TextProcessor } from './text-processor'; import type { Triple } from '@/types/graph'; /** * Remote backend implementation that uses a graph database for storage, - * Pinecone for vector embeddings, and SentenceTransformer for generating embeddings. + * Qdrant for vector embeddings, and SentenceTransformer for generating embeddings. * Follows the implementation in PyTorch Geometric's txt2kg.py * Enhanced with LangChain text processing for better extraction */ export class RemoteBackendService { private graphDBService: GraphDBService; - private pineconeService: PineconeService; + private pineconeService: QdrantService; private embeddingsService: EmbeddingsService; private textProcessor: TextProcessor; private initialized: boolean = false; @@ -20,7 +20,7 @@ export class RemoteBackendService { private constructor() { this.graphDBService = GraphDBService.getInstance(); - this.pineconeService = PineconeService.getInstance(); + this.pineconeService = QdrantService.getInstance(); this.embeddingsService = EmbeddingsService.getInstance(); this.textProcessor = TextProcessor.getInstance(); }