Source: backend/models/modelVectorOperations.js

/**
 * @fileoverview Diese Datei enthält die Definition der Klasse VectorOperations.
 * Sie ermöglicht die Berechnung von Ähnlichkeiten zwischen Vektoren und die Verwaltung eines Caches für diese Berechnungen.
 * 
 * @author Lennart, Miray
 * Die Funktionen wurden mit Unterstützung von KI vorgeneriert und angepasst.
 * @module modelVectorOperations
 */

const { performance } = require('perf_hooks');

/**
 * Klasse zur Berechnung von Ähnlichkeiten zwischen Vektoren und Verwaltung eines Cache-Mechanismus.
 * 
 * @class VectorOperations
 * @property {boolean} useCache - Gibt an, ob der Cache für Berechnungen verwendet wird.
 * @property {number} exactMatchWeight - Gewichtung der euklidischen Distanz in der Ähnlichkeitsberechnung.
 * @property {number} semanticWeight - Gewichtung der Kosinusähnlichkeit in der Berechnung.
 * @property {Map} similarityCache - Cache zur Speicherung von Ähnlichkeitsberechnungen.
 * @property {number} cacheMaxSize - Maximale Anzahl an Cache-Einträgen.
 * @property {number} cacheCleanupInterval - Intervall für die Cache-Bereinigung in Millisekunden.
 */
class VectorOperations {

    /**
 * Erstellt eine Instanz von VectorOperations mit den angegebenen Optionen.
 * 
 * @constructor
 * @param {Object} [options={}] - Konfigurationsoptionen für die Vektorrechnungen.
 * @param {boolean} [options.useCache=true] - Gibt an, ob ein Cache verwendet werden soll.
 * @param {number} [options.exactMatchWeight=0.9] - Gewichtung für euklidische Distanz.
 * @param {number} [options.semanticWeight=0.2] - Gewichtung für Kosinusähnlichkeit.
 * @param {number} [options.cacheMaxSize=1000] - Maximale Anzahl an Cache-Einträgen.
 * @param {number} [options.cacheCleanupInterval=300000] - Intervall für die Cache-Bereinigung in Millisekunden.
 */
    constructor(options = {}) {
        this.useCache = options.useCache ?? true;
        this.exactMatchWeight = options.exactMatchWeight || 0.9;
        this.semanticWeight = options.semanticWeight || 0.2;
        this.similarityCache = new Map();
        this.lastCacheClean = Date.now();
        this.cacheMaxSize = options.cacheMaxSize || 1000;
        this.cacheCleanupInterval = options.cacheCleanupInterval || 1000 * 60 * 5; // 5 minutes
    }

    /**
 * Berechnet die Ähnlichkeit zwischen zwei Vektoren unter Verwendung von Kosinus- und euklidischer Distanz.
 * 
 * @method calculateSimilarity
 * @memberof VectorOperations
 * @param {number[]} embedding1 - Der erste Vektor.
 * @param {number[]} embedding2 - Der zweite Vektor.
 * @returns {number} Ein Ähnlichkeitswert zwischen 0 und 1.
 * @example
 * const similarity = vectorOps.calculateSimilarity([0.1, 0.2], [0.1, 0.25]);
 * console.log(similarity);
 */
    calculateSimilarity(embedding1, embedding2) {
        // Parse and clean embeddings if needed
        const vec1 = this._prepareVector(embedding1);
        const vec2 = this._prepareVector(embedding2);

        // Calculate both similarity metrics
        const cosineSim = this._calculateCosineSimilarity(vec1, vec2);
        const euclideanSim = this._calculateEuclideanSimilarity(vec1, vec2);

        // Weighted combination of both metrics
        return (this.exactMatchWeight * euclideanSim) + (this.semanticWeight * cosineSim);
    }

    /**
 * Konvertiert einen Vektor in ein geeignetes Zahlenformat und filtert ungültige Werte.
 * 
 * @method _prepareVector
 * @memberof VectorOperations
 * @param {string|Array<number>} embedding - Der zu verarbeitende Vektor.
 * @returns {number[]} Ein Array mit validierten Zahlenwerten.
 */
    _prepareVector(embedding) {
        if (!embedding) return null;

        if (typeof embedding === 'string') {
            // Handle PostgreSQL array format and other string formats
            return embedding
                .replace(/[{\[\]}]/g, '') // Remove brackets and braces
                .split(',')
                .map(num => parseFloat(num.trim()))
                .filter(num => !isNaN(num)); // Filter out any invalid numbers
        }

        if (Array.isArray(embedding)) {
            return embedding.map(num => parseFloat(num)).filter(num => !isNaN(num));
        }

        return null;
    }

    /**
 * Berechnet die Kosinusähnlichkeit zwischen zwei Vektoren.
 * 
 * @method _calculateCosineSimilarity
 * @memberof VectorOperations
 * @param {number[]} vec1 - Der erste Vektor.
 * @param {number[]} vec2 - Der zweite Vektor.
 * @returns {number} Die Kosinusähnlichkeit zwischen -1 und 1.
 */
    _calculateCosineSimilarity(vec1, vec2) {
        if (!vec1 || !vec2 || vec1.length !== vec2.length) return 0;

        let dotProduct = 0;
        let norm1 = 0;
        let norm2 = 0;
        const len = vec1.length;
        const blockSize = 8; // Process 8 elements at a time

        // Process in blocks for better performance
        for (let i = 0; i < len - (len % blockSize); i += blockSize) {
            // Dot product
            dotProduct += vec1[i] * vec2[i] +
                vec1[i + 1] * vec2[i + 1] +
                vec1[i + 2] * vec2[i + 2] +
                vec1[i + 3] * vec2[i + 3] +
                vec1[i + 4] * vec2[i + 4] +
                vec1[i + 5] * vec2[i + 5] +
                vec1[i + 6] * vec2[i + 6] +
                vec1[i + 7] * vec2[i + 7];

            // Norms
            norm1 += vec1[i] * vec1[i] +
                vec1[i + 1] * vec1[i + 1] +
                vec1[i + 2] * vec1[i + 2] +
                vec1[i + 3] * vec1[i + 3] +
                vec1[i + 4] * vec1[i + 4] +
                vec1[i + 5] * vec1[i + 5] +
                vec1[i + 6] * vec1[i + 6] +
                vec1[i + 7] * vec1[i + 7];

            norm2 += vec2[i] * vec2[i] +
                vec2[i + 1] * vec2[i + 1] +
                vec2[i + 2] * vec2[i + 2] +
                vec2[i + 3] * vec2[i + 3] +
                vec2[i + 4] * vec2[i + 4] +
                vec2[i + 5] * vec2[i + 5] +
                vec2[i + 6] * vec2[i + 6] +
                vec2[i + 7] * vec2[i + 7];
        }

        // Handle remaining elements
        for (let i = len - (len % blockSize); i < len; i++) {
            dotProduct += vec1[i] * vec2[i];
            norm1 += vec1[i] * vec1[i];
            norm2 += vec2[i] * vec2[i];
        }

        norm1 = Math.sqrt(norm1);
        norm2 = Math.sqrt(norm2);

        return norm1 && norm2 ? dotProduct / (norm1 * norm2) : 0;
    }

    /**
 * Berechnet die euklidische Distanz zwischen zwei Vektoren und konvertiert sie in einen Ähnlichkeitswert.
 * 
 * @method _calculateEuclideanSimilarity
 * @memberof VectorOperations
 * @param {number[]} vec1 - Der erste Vektor.
 * @param {number[]} vec2 - Der zweite Vektor.
 * @returns {number} Ein Ähnlichkeitswert zwischen 0 und 1.
 */
    _calculateEuclideanSimilarity(vec1, vec2) {
        if (!vec1 || !vec2 || vec1.length !== vec2.length) return 0;

        let squaredDistance = 0;
        const len = vec1.length;
        const blockSize = 8;

        // Process in blocks
        for (let i = 0; i < len - (len % blockSize); i += blockSize) {
            let diff0 = vec1[i] - vec2[i];
            let diff1 = vec1[i + 1] - vec2[i + 1];
            let diff2 = vec1[i + 2] - vec2[i + 2];
            let diff3 = vec1[i + 3] - vec2[i + 3];
            let diff4 = vec1[i + 4] - vec2[i + 4];
            let diff5 = vec1[i + 5] - vec2[i + 5];
            let diff6 = vec1[i + 6] - vec2[i + 6];
            let diff7 = vec1[i + 7] - vec2[i + 7];

            squaredDistance += diff0 * diff0 + diff1 * diff1 +
                diff2 * diff2 + diff3 * diff3 +
                diff4 * diff4 + diff5 * diff5 +
                diff6 * diff6 + diff7 * diff7;
        }

        // Handle remaining elements
        for (let i = len - (len % blockSize); i < len; i++) {
            let diff = vec1[i] - vec2[i];
            squaredDistance += diff * diff;
        }

        // Convert distance to similarity score (0 to 1)
        return Math.exp(-Math.sqrt(squaredDistance));
    }

    /**
 * Bereinigt den Cache, wenn die maximale Größe überschritten wird.
 * 
 * @method _cleanCache
 * @memberof VectorOperations
 */
    _cleanCache() {
        if (this.similarityCache.size > this.cacheMaxSize) {
            const entriesToKeep = Array.from(this.similarityCache.entries())
                .sort((a, b) => b[1].timestamp - a[1].timestamp)
                .slice(0, this.cacheMaxSize / 2);

            this.similarityCache.clear();
            entriesToKeep.forEach(([key, value]) => {
                this.similarityCache.set(key, value);
            });
        }
    }
}

module.exports = new VectorOperations({
    useCache: true,
    exactMatchWeight: 0.9,
    semanticWeight: 0.2,
    cacheMaxSize: 1000,
    cacheCleanupInterval: 300000 // 5 minutes
});