Source: backend/models/modelFolderSuggestion.js

/**
 * @fileoverview Diese Datei enthält die Implementierung einer Engine zur Ordner-Vorschlagserstellung.
 * Sie ermöglicht die Berechnung von Ähnlichkeiten zwischen Dokument- und Ordner-Embeddings 
 * und gibt passende Ordner-Vorschläge zurück.
 * 
 * @author Lennart, Luca
 * Die Funktionen wurden mit Unterstützung von KI-Tools angepasst und optimiert.
 * @module modelFolderSuggestion
 */

const { performance } = require('perf_hooks');
const db = require('../../ConnectPostgres');
const vectorOps = require('./modelVectorOperations');

/**
 * Eine Engine zur Berechnung von Ordner-Vorschlägen basierend auf Dokument-Embeddings.
 * 
 * @class FolderSuggestionEngine
 * @property {number} similarityThreshold - Der Ähnlichkeitsschwellenwert für Vorschläge.
 * @property {number} maxSuggestions - Maximale Anzahl an Ordner-Vorschlägen.
 * @property {Map} similarityCache - Cache zur Speicherung berechneter Ähnlichkeiten.
 * @property {Object} metrics - Statistik-Daten zur Performance-Messung.
 */
class FolderSuggestionEngine {
    constructor(options = {}) {
        this.similarityThreshold = options.similarityThreshold || 0.75;
        this.maxSuggestions = options.maxSuggestions || 3;
        this.similarityCache = new Map();

        this.metrics = {
            totalRequests: 0,
            successfulRequests: 0,
            failedRequests: 0,
            averageResponseTime: 0,
            lastError: null
        };
    }

    /**
     * Gibt eine Liste empfohlener Ordner für ein gegebenes Dokument-Embedding zurück.
     * 
     * @async
     * @method getSuggestedFolders
     * @memberof FolderSuggestionEngine
     * @param {Object} params - Parameter-Objekt.
     * @param {number[]} params.docEmbedding - Das Embedding des Dokuments.
     * @param {number} params.userId - Die Benutzer-ID.
     * @returns {Promise<Object>} Ein Objekt mit den empfohlenen Ordnern und der Verarbeitungszeit.
     * @throws {Error} Falls die Vorschlagsberechnung fehlschlägt.
     * @example
     * const suggestions = await folderEngine.getSuggestedFolders({ docEmbedding, userId: 123 });
     * console.log(suggestions);
     */
    async getSuggestedFolders({ docEmbedding, userId }) {
        const startTime = performance.now();
        this.metrics.totalRequests++;

        const cacheKey = `folder_suggestions:${userId}:${this._hashEmbedding(docEmbedding)}`;

        try {
            if (this.similarityCache.has(cacheKey)) {
                return this.similarityCache.get(cacheKey);
            }

            const foldersData = await this._getFolderData(userId);

            if (foldersData.rows.length === 0) {
                const result = {
                    suggestedFolders: [],
                    processingTime: performance.now() - startTime
                };
                this._cacheResult(cacheKey, result);
                return result;
            }

            const suggestions = await this._processSuggestions(
                docEmbedding,
                foldersData.rows
            );

            const result = {
                suggestedFolders: suggestions.slice(0, this.maxSuggestions),
                processingTime: performance.now() - startTime
            };

            this._cacheResult(cacheKey, result);

            this.metrics.successfulRequests++;
            this.metrics.averageResponseTime =
                (this.metrics.averageResponseTime * (this.metrics.successfulRequests - 1) +
                    (performance.now() - startTime)) / this.metrics.successfulRequests;

            return result;

        } catch (error) {
            this.metrics.failedRequests++;
            this.metrics.lastError = error.message;
            throw error;
        }
    }

    /**
     * Berechnet die Ähnlichkeit zwischen zwei Embeddings unter Verwendung vordefinierter Methoden.
     * 
     * @async
     * @method _calculateSimilarity
     * @memberof FolderSuggestionEngine
     * @param {number[]} embedding1 - Erstes Embedding.
     * @param {number[]} embedding2 - Zweites Embedding.
     * @param {string} [cacheKey=null] - Optionaler Cache-Schlüssel für schnelleren Zugriff.
     * @returns {Promise<number>} Der berechnete Ähnlichkeitswert zwischen 0 und 1.
     */
    async _calculateSimilarity(embedding1, embedding2, cacheKey = null) {
        if (cacheKey && this.similarityCache.has(cacheKey)) {
            return this.similarityCache.get(cacheKey);
        }

        try {
            const similarity = vectorOps.calculateSimilarity(embedding1, embedding2);

            if (cacheKey && similarity > this.similarityThreshold * 0.8) {
                this.similarityCache.set(cacheKey, similarity);
            }

            return similarity;
        } catch (error) {
            console.error('Error calculating similarity:', error);
            return 0;
        }
    }

    /**
     * Verarbeitet die Ordner-Daten und berechnet passende Vorschläge basierend auf Ähnlichkeiten.
     * 
     * @async
     * @method _processSuggestions
     * @memberof FolderSuggestionEngine
     * @param {number[]} docEmbedding - Das Embedding des Dokuments.
     * @param {Array<Object>} folders - Liste der verfügbaren Ordner mit ihren Embeddings.
     * @returns {Promise<Array<Object>>} Eine sortierte Liste der besten Ordner-Vorschläge.
     */
    async _processSuggestions(docEmbedding, folders) {
        const suggestions = [];

        try {
            for (const folder of folders) {
                const similarity = await this._calculateSimilarity(
                    docEmbedding,
                    folder.embedding,
                    folder.folder_id
                );

                // Remove the similarity threshold check to get all suggestions
                suggestions.push({
                    folderId: folder.folder_id,
                    folderName: folder.folder_name,
                    similarity: parseFloat(similarity.toFixed(4)),
                    fileCount: folder.file_count,
                    parentId: folder.parent_folder_id,
                    recentFiles: folder.recent_files,
                    confidence: this._calculateConfidence(similarity, folder)
                });
            }

            // Nach Ähnlichkeit sortieren und die besten Vorschläge auswählen
            return suggestions
                .sort((a, b) => b.similarity - a.similarity)
                .slice(0, this.maxSuggestions);  // immer maxSuggestions number of folders zurückgeben
        } catch (error) {
            console.error('Error processing suggestions:', error);
            return [];
        }
    }

    /**
     * Ruft die verfügbaren Ordnerdaten für einen bestimmten Benutzer aus der Datenbank ab.
     * 
     * @async
     * @method _getFolderData
     * @memberof FolderSuggestionEngine
     * @param {number} userId - Die Benutzer-ID.
     * @returns {Promise<Object>} Ordnerdaten inklusive Embeddings, Datei-Anzahl und letzte Dateien.
     */
    async _getFolderData(userId) {
        const query = `
            WITH RankedFiles AS (
                SELECT 
                    f.folder_id,
                    f.folder_name,
                    f.embedding,
                    f.parent_folder_id,
                    fl.file_name,
                    ROW_NUMBER() OVER (PARTITION BY f.folder_id ORDER BY fl.created_at DESC) as rn
                FROM main.folders f
                LEFT JOIN main.files fl ON f.folder_id = fl.folder_id
                WHERE f.user_id = $1 
                AND f.embedding IS NOT NULL
            )
            SELECT 
                folder_id,
                folder_name,
                embedding,
                parent_folder_id,
                COUNT(file_name) as file_count,
                array_agg(
                    CASE WHEN rn <= 5 THEN file_name ELSE NULL END
                ) FILTER (WHERE rn <= 5) as recent_files
            FROM RankedFiles
            GROUP BY folder_id, folder_name, embedding, parent_folder_id
        `;

        return await db.query(query, [userId]);
    }

    /**
     * Berechnet einen Konfidenzwert für einen Ordner-Vorschlag basierend auf mehreren Faktoren.
     * 
     * @method _calculateConfidence
     * @memberof FolderSuggestionEngine
     * @param {number} similarity - Der berechnete Ähnlichkeitswert.
     * @param {Object} folder - Das Ordner-Objekt mit Metadaten.
     * @returns {number} Der berechnete Konfidenzwert (zwischen 0 und 1).
     */
    _calculateConfidence(similarity, folder) {
        const baseFactor = 0.7;
        const fileCountFactor = Math.min(folder.file_count / 10, 0.2);
        const recentFilesFactor = folder.recent_files?.length ? 0.1 : 0;

        return similarity;
    }

    /**
 * Erstellt einen eindeutigen Hash-Wert für ein Embedding, um es im Cache zu speichern.
 * 
 * @method _hashEmbedding
 * @memberof FolderSuggestionEngine
 * @param {number[]} embedding - Das Embedding als Array von Zahlen.
 * @returns {string} Ein Base64-gekürzter Hash-String.
 */
    _hashEmbedding(embedding) {
        return Buffer.from(embedding.join(',')).toString('base64').slice(0, 10);
    }

    /**
 * Speichert ein Ergebnis im Cache und entfernt alte Einträge, falls der Cache zu groß wird.
 * 
 * @method _cacheResult
 * @memberof FolderSuggestionEngine
 * @param {string} key - Der Cache-Schlüssel.
 * @param {Object} value - Das zu speichernde Ergebnis.
 */
    _cacheResult(key, value) {
        this.similarityCache.set(key, value);
        if (this.similarityCache.size > 1000) {
            const firstKey = this.similarityCache.keys().next().value;
            this.similarityCache.delete(firstKey);
        }
    }
}

module.exports = new FolderSuggestionEngine({
    similarityThreshold: 0.75,
    maxSuggestions: 3
});