Replace Pinecone with Qdrant for ARM64 compatibility

- Migrate from Pinecone to Qdrant vector database for native ARM64 support
- Add Qdrant service with automatic collection initialization in docker-compose
- Implement QdrantService with UUID-based point IDs to meet Qdrant requirements
- Update all API routes and frontend components to use Qdrant
- Enhance Storage Connections UI with detailed stats (vectors, status, dimensions)
- Add icons and tooltips to Vector DB section matching Graph DB UX
This commit is contained in:
Santosh Bhavani 2025-10-24 23:16:44 -07:00
parent cfebbc7b04
commit de9c46e97e
14 changed files with 864 additions and 116 deletions

View File

@ -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"

View File

@ -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:

View File

@ -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 }
);
}

View File

@ -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()) {

View File

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

View File

@ -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;

View File

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

View File

@ -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,

View File

@ -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) {
<>
<div className="flex items-center gap-2 text-xs md:text-sm">
<span className="text-foreground font-medium">
Pinecone
Qdrant
</span>
<span className="text-foreground font-mono text-[11px] bg-secondary/50 px-2 py-0.5 rounded truncate max-w-full">
direct-http
{(vectorStats as any).url || 'http://qdrant:6333'}
</span>
</div>
{vectorStats.nodes > 0 && (
<div className="text-xs md:text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Database className="h-3.5 w-3.5" />
<span>{vectorStats.nodes.toLocaleString()} vectors</span>
</div>
<div className="text-xs md:text-sm text-muted-foreground space-y-1">
<div className="flex items-center gap-2">
<Database className="h-3.5 w-3.5" />
<span>{vectorStats.nodes.toLocaleString()} vectors indexed</span>
</div>
)}
{(vectorStats as any).status && (
<div className="flex items-center gap-2">
<Zap className="h-3.5 w-3.5" />
<span>Status: <span className="capitalize">{(vectorStats as any).status}</span></span>
</div>
)}
{(vectorStats as any).vectorSize && (
<div className="flex items-center gap-2">
<InfoIcon className="h-3.5 w-3.5" />
<span>{(vectorStats as any).vectorSize}d ({(vectorStats as any).distance})</span>
</div>
)}
</div>
</>
)}
@ -546,59 +563,85 @@ export function DatabaseConnection({ className }: DatabaseConnectionProps) {
)}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={checkVectorConnection}
disabled={vectorConnectionStatus === "checking"}
className="flex-1 text-xs h-7"
>
{vectorConnectionStatus === "checking" ? "Checking..." : "Refresh"}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={checkVectorConnection}
disabled={vectorConnectionStatus === "checking"}
className="flex-1 text-xs h-7 px-2"
>
<RefreshCw className={`h-3 w-3 ${vectorConnectionStatus === "checking" ? "animate-spin" : ""}`} />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{vectorConnectionStatus === "checking" ? "Checking..." : "Refresh connection"}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{vectorConnectionStatus === "connected" ? (
<>
<Button
variant="outline"
size="sm"
onClick={disconnectVector}
className="flex-1 text-xs h-7"
>
Disconnect
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={disconnectVector}
className="flex-1 text-xs h-7 px-2"
>
<LogOut className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Disconnect</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Dialog open={showClearVectorDialog} onOpenChange={setShowClearVectorDialog}>
<DialogTrigger asChild>
<Button
variant="destructive"
size="sm"
className="flex-1 text-xs h-7"
disabled={isClearingVectorDB}
>
<Trash2 className="h-3 w-3 mr-1" />
Clear
</Button>
</DialogTrigger>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
variant="destructive"
size="sm"
className="flex-1 text-xs h-7 px-2"
disabled={isClearingVectorDB}
>
<Trash2 className="h-3 w-3" />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Clear database</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-destructive">Clear Pinecone Database</DialogTitle>
<DialogTitle className="text-destructive">Clear Qdrant Database</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
<Alert variant="destructive" className="mt-2">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Warning</AlertTitle>
<AlertDescription>
This will permanently delete all vectors from the Pinecone database.
This will permanently delete all vectors from the Qdrant database.
</AlertDescription>
</Alert>
<DialogFooter className="gap-2 mt-4">
<DialogClose asChild>
<Button variant="outline" size="sm">Cancel</Button>
</DialogClose>
<Button
variant="destructive"
<Button
variant="destructive"
size="sm"
onClick={clearVectorDatabase}
disabled={isClearingVectorDB}
@ -610,13 +653,13 @@ export function DatabaseConnection({ className }: DatabaseConnectionProps) {
</Dialog>
</>
) : (
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
onClick={() => {
// Open Vector DB settings
const event = new CustomEvent('open-settings', {
detail: { tab: 'vectordb' }
const event = new CustomEvent('open-settings', {
detail: { tab: 'vectordb' }
});
window.dispatchEvent(event);
}}

View File

@ -87,7 +87,7 @@ export function PineconeConnection({ className }: PineconeConnectionProps) {
<InfoIcon className="h-5 w-5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Local Pinecone stores vector embeddings in memory for semantic search</p>
<p>Qdrant stores vector embeddings for semantic search</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -109,34 +109,34 @@ export function PineconeConnection({ className }: PineconeConnectionProps) {
<p className="whitespace-normal break-words">Error: {error}</p>
{error.includes('404') && (
<p className="mt-1 text-xs">
The Pinecone server is running but the index doesn't exist yet.
<button
The Qdrant server is running but the collection doesn't exist yet.
<button
onClick={async () => {
setConnectionStatus("checking");
setError(null);
try {
const response = await fetch('/api/pinecone-diag/create-index', { method: 'POST' });
if (response.ok) {
// Wait a bit for the index to be created
// Wait a bit for the collection to be created
await new Promise(resolve => setTimeout(resolve, 2000));
checkConnection();
} else {
const data = await response.json();
setError(data.error || 'Failed to create index');
setError(data.error || 'Failed to create collection');
setConnectionStatus("disconnected");
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Error creating index');
setError(err instanceof Error ? err.message : 'Error creating collection');
setConnectionStatus("disconnected");
}
}}
className="ml-1 text-blue-600 hover:text-blue-800 underline"
>
Click here to create the index
Click here to create the collection
</button>
<br />
<span className="text-xs text-gray-600">Or using Docker Compose: </span>
<code className="mx-1 px-1 bg-gray-100 rounded">docker-compose restart pinecone</code>
<code className="mx-1 px-1 bg-gray-100 rounded">docker compose restart qdrant</code>
</p>
)}
</div>
@ -144,13 +144,25 @@ export function PineconeConnection({ className }: PineconeConnectionProps) {
<div className="text-sm space-y-1 w-full">
<div className="flex justify-between">
<span className="text-muted-foreground">Vectors:</span>
<span>{stats.nodes}</span>
<span className="text-muted-foreground">Qdrant</span>
<span className="text-xs text-muted-foreground">{(stats as any).url || 'http://qdrant:6333'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Source:</span>
<span>{stats.source} local</span>
<span className="text-muted-foreground">Vectors:</span>
<span>{stats.nodes} indexed</span>
</div>
{(stats as any).status && (
<div className="flex justify-between">
<span className="text-muted-foreground">Status:</span>
<span className="capitalize">{(stats as any).status}</span>
</div>
)}
{(stats as any).vectorSize && (
<div className="flex justify-between">
<span className="text-muted-foreground">Dimensions:</span>
<span>{(stats as any).vectorSize}d ({(stats as any).distance})</span>
</div>
)}
</div>
<div className="flex space-x-2">

View File

@ -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) {

View File

@ -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<string, any>;
}
interface QdrantQueryResponse {
result: Array<{
id: string | number;
score: number;
payload?: Record<string, any>;
}>;
}
// Define interface for document search results
export interface DocumentSearchResult {
id: string;
score: number;
metadata?: Record<string, any>;
}
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<any> {
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<boolean> {
// 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<void> {
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<string, number[]>,
textContentMap?: Map<string, string>
): Promise<void> {
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<boolean> {
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<string, number[]>,
textContent: Map<string, string>,
metadata: Map<string, any>
): Promise<void> {
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<string, any> }> {
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<string, any>();
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<QdrantQueryResponse | null> {
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<string[]> {
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<string[]> {
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<boolean> {
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<any> {
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<boolean> {
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<DocumentSearchResult[]> {
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 [];
}
}
}

View File

@ -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<string, any>;
@ -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();
}

View File

@ -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();
}