@vfarcic/dot-ai
Version:
Universal Kubernetes application deployment agent with CLI and MCP interfaces
300 lines (299 loc) • 10.6 kB
JavaScript
"use strict";
/**
* Vector DB Service
*
* Handles Qdrant Vector Database integration for semantic search and storage
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VectorDBService = void 0;
const js_client_rest_1 = require("@qdrant/js-client-rest");
class VectorDBService {
client = null;
config;
collectionName;
constructor(config = {}) {
this.config = {
url: config.url !== undefined ? config.url : (process.env.QDRANT_URL || 'http://localhost:6333'),
apiKey: config.apiKey || process.env.QDRANT_API_KEY,
collectionName: config.collectionName || 'patterns'
};
this.collectionName = this.config.collectionName;
this.validateConfig();
if (this.shouldInitializeClient()) {
this.client = new js_client_rest_1.QdrantClient({
url: this.config.url,
apiKey: this.config.apiKey
});
}
}
validateConfig() {
// Allow test-friendly initialization
if (this.config.url === 'test-url' || this.config.url === 'mock-url') {
return; // Allow test configurations
}
if (!this.config.url || this.config.url.trim() === '') {
throw new Error('Qdrant URL is required for Vector DB integration');
}
}
shouldInitializeClient() {
// Don't initialize for test configurations
const testUrls = ['test-url', 'mock-url'];
return !testUrls.includes(this.config.url || '');
}
/**
* Initialize the collection if it doesn't exist
*/
async initializeCollection(vectorSize = 384) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
// Check if collection exists
const collections = await this.client.getCollections();
const collectionExists = collections.collections.some(col => col.name === this.collectionName);
if (collectionExists) {
// Verify existing collection has correct vector dimensions
try {
const collectionInfo = await this.client.getCollection(this.collectionName);
const existingVectorSize = collectionInfo.config?.params?.vectors?.size;
if (existingVectorSize && existingVectorSize !== vectorSize) {
// Dimension mismatch - recreate collection
console.warn(`Vector dimension mismatch: existing collection has ${existingVectorSize} dimensions, but ${vectorSize} expected. Recreating collection.`);
await this.client.deleteCollection(this.collectionName);
await this.createCollection(vectorSize);
}
}
catch (error) {
// If we can't get collection info, assume it's corrupted and recreate
console.warn(`Failed to get collection info, recreating collection: ${error}`);
await this.client.deleteCollection(this.collectionName);
await this.createCollection(vectorSize);
}
}
else {
// Create new collection
await this.createCollection(vectorSize);
}
}
catch (error) {
throw new Error(`Failed to initialize collection: ${error}`);
}
}
/**
* Create collection with specified vector size
*/
async createCollection(vectorSize) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
await this.client.createCollection(this.collectionName, {
vectors: {
size: vectorSize,
distance: 'Cosine',
on_disk: true // Enable on-disk storage for better performance with large collections
},
// Enable payload indexing for better keyword search performance
optimizers_config: {
default_segment_number: 2
}
});
}
/**
* Store a document with optional vector
*/
async upsertDocument(document) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
const point = {
id: document.id,
payload: document.payload
};
// For documents without vectors, use a zero vector as placeholder
// This allows us to store documents in collections configured for vectors
if (document.vector && document.vector.length > 0) {
point.vector = document.vector;
}
else {
// Create a zero vector with the expected dimensions (384)
point.vector = new Array(384).fill(0);
}
await this.client.upsert(this.collectionName, {
wait: true,
points: [point]
});
}
catch (error) {
throw new Error(`Failed to upsert document: ${error}`);
}
}
/**
* Search for similar documents using vector similarity
*/
async searchSimilar(vector, options = {}) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
const searchResult = await this.client.search(this.collectionName, {
vector,
limit: options.limit || 10,
score_threshold: options.scoreThreshold || 0.5,
with_payload: true
});
return searchResult.map(result => ({
id: result.id.toString(),
score: result.score,
payload: result.payload || {}
}));
}
catch (error) {
throw new Error(`Failed to search documents: ${error}`);
}
}
/**
* Search for documents using payload filtering (keyword search)
*/
async searchByKeywords(keywords, options = {}) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
// Fallback to JavaScript-based filtering due to Qdrant filter syntax issues
// Get all documents and filter in JavaScript for keyword matching
const scrollResult = await this.client.scroll(this.collectionName, {
limit: 1000, // Get all documents for filtering
with_payload: true,
with_vector: false
});
// Filter documents by checking if any keyword matches any trigger
const matchedPoints = scrollResult.points.filter(point => {
if (!point.payload || !point.payload.triggers || !Array.isArray(point.payload.triggers)) {
return false;
}
const triggers = point.payload.triggers.map((t) => t.toLowerCase());
return keywords.some(keyword => triggers.some(trigger => trigger.includes(keyword.toLowerCase()) ||
keyword.toLowerCase().includes(trigger)));
});
// Apply limit after filtering
const limitedResults = matchedPoints.slice(0, options.limit || 10);
return limitedResults.map(point => ({
id: point.id.toString(),
score: 1.0, // Keyword matches get full score
payload: point.payload || {}
}));
}
catch (error) {
throw new Error(`Failed to search by keywords: ${error}`);
}
}
/**
* Get a document by ID
*/
async getDocument(id) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
const result = await this.client.retrieve(this.collectionName, {
ids: [id],
with_payload: true,
with_vector: true
});
if (result.length === 0) {
return null;
}
const point = result[0];
return {
id: point.id.toString(),
payload: point.payload || {},
vector: point.vector || undefined
};
}
catch (error) {
throw new Error(`Failed to get document: ${error}`);
}
}
/**
* Delete a document by ID
*/
async deleteDocument(id) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
await this.client.delete(this.collectionName, {
wait: true,
points: [id]
});
}
catch (error) {
throw new Error(`Failed to delete document: ${error}`);
}
}
/**
* Get all documents (for listing)
*/
async getAllDocuments(limit = 100) {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
const scrollResult = await this.client.scroll(this.collectionName, {
limit,
with_payload: true,
with_vector: false
});
return scrollResult.points.map(point => ({
id: point.id.toString(),
payload: point.payload || {}
}));
}
catch (error) {
throw new Error(`Failed to get all documents: ${error}`);
}
}
/**
* Get collection info and statistics
*/
async getCollectionInfo() {
if (!this.client) {
throw new Error('Vector DB client not initialized');
}
try {
return await this.client.getCollection(this.collectionName);
}
catch (error) {
throw new Error(`Failed to get collection info: ${error}`);
}
}
/**
* Check if Vector DB is available and responsive
*/
async healthCheck() {
if (!this.client) {
return false;
}
try {
await this.client.getCollections();
return true;
}
catch (error) {
return false;
}
}
/**
* Check if client is initialized
*/
isInitialized() {
return this.client !== null;
}
/**
* Get configuration (for debugging)
*/
getConfig() {
return { ...this.config };
}
}
exports.VectorDBService = VectorDBService;