Source: backend/models/modelClustering.js

/**
 * @fileoverview Diese Datei enthält Funktionen zur Durchführung von Clustering-Operationen 
 * auf Dokument- und Ordner-Embeddings. Sie ermöglicht das Abrufen von Ordnerdaten, das 
 * Zuordnen von Dokumenten zu Ordnern und das Ausführen eines Clustering-Skripts.
 * 
 * @author Lennart
 * Die Funktionen wurden mit Unterstützung von KI-Tools angepasst und optimiert.
 * @module modelClustering
 */


const { exec } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
const db = require('../../ConnectPostgres');

/**
 * Ruft die Ordnerdaten eines Benutzers ab, einschließlich Embeddings, Namen und Hierarchie.
 *
 * @async
 * @function getFolderData
 * @param {number} userId - Die ID des Benutzers, für den die Ordnerdaten abgerufen werden.
 * @returns {Promise<Object>} Ein Objekt mit `embeddings`, `names` und `hierarchy`.
 */
async function getFolderData(userId) {
    const query = `
        SELECT 
            folder_id,
            folder_name,
            embedding,
            parent_folder_id
        FROM main.folders 
        WHERE user_id = $1 
        AND embedding IS NOT NULL
    `;

    const result = await db.query(query, [userId]);

    const embeddings = {};
    const names = {};
    const hierarchy = {};

    result.rows.forEach(row => {
        // Zeichenketten-Embedding bei Bedarf in ein Array konvertieren
        let embedding = row.embedding;
        if (typeof embedding === 'string') {
            embedding = embedding.replace(/[\[\]]/g, '').split(',').map(Number);
        }
        embeddings[row.folder_id] = embedding;
        names[row.folder_id] = row.folder_name;
        hierarchy[row.folder_id] = row.parent_folder_id;
    });

    return { embeddings, names, hierarchy };
}

/**
 * Erstellt eine Zuordnung von Dokumenten zu ihren Ordnern für einen bestimmten Benutzer.
 *
 * @async
 * @function getDocumentFolderMap
 * @param {number} userId - Die ID des Benutzers, dessen Dokument-Ordner-Zuordnung abgerufen wird.
 * @returns {Promise<Object>} Ein Objekt, das die Datei-IDs den Ordner-IDs zuordnet.
 */
async function getDocumentFolderMap(userId) {
    const query = `
        SELECT file_id, folder_id 
        FROM main.files 
        WHERE user_id = $1 
        AND folder_id IS NOT NULL
    `;

    const result = await db.query(query, [userId]);

    return result.rows.reduce((acc, row) => {
        acc[row.file_id] = row.folder_id.toString();
        return acc;
    }, {});
}

/**
 * Führt ein Clustering von Dokument- und Ordner-Embeddings durch.
 *
 * @async
 * @function runClustering
 * @param {Array<Object>} embeddings - Eine Liste von Dokument-Embeddings.
 * @param {Object} [config={}] - Konfigurationsoptionen für das Clustering.
 * @param {number} userId - Die Benutzer-ID für Sicherheitszwecke.
 * @returns {Promise<Object>} Ein Objekt mit Clustering-Ergebnissen einschließlich Labels und Statistiken.
 * @throws {Error} Falls ein ungültiges Embedding-Format oder ein Fehler während der Ausführung auftritt.
 * @example
 * const result = await runClustering(docEmbeddings, { semanticThreshold: 0.8 }, 123);
 * console.log(result);
 */
async function runClustering(embeddings, config = {}, userId) {
    if (!userId) {
        throw new Error('userId is required for security purposes');
    }

    return new Promise((resolve, reject) => {
        const processingFunction = async () => {
            try {
                // Format document embeddings
                const formattedDocEmbeddings = embeddings.map(emb => {
                    if (typeof emb === 'string') {
                        return emb.replace(/[\[\]]/g, '').split(',').map(Number);
                    }
                    if (Array.isArray(emb)) {
                        return emb.map(Number);
                    }
                    if (emb.embedding) {
                        if (typeof emb.embedding === 'string') {
                            return emb.embedding.replace(/[\[\]]/g, '').split(',').map(Number);
                        }
                        return emb.embedding.map(Number);
                    }
                    throw new Error('Invalid embedding format');
                });

                // Erweiterte Ordnerdaten abrufen, wenn userId angegeben ist
                let clusteringData = { doc_embeddings: formattedDocEmbeddings };
                let folderData = null;

                if (userId) {
                    const [folders, docToFolderMap] = await Promise.all([
                        getFolderData(userId),
                        getDocumentFolderMap(userId)
                    ]);

                    folderData = folders; // speichrt die Ordnerdaten für die spätere Verwendung

                    clusteringData = {
                        doc_embeddings: formattedDocEmbeddings,
                        folder_embeddings: folders.embeddings,
                        folder_names: folders.names,
                        folder_hierarchy: folders.hierarchy,
                        doc_to_folder_map: docToFolderMap
                    };
                }

                // erstellt temporary files
                const tempEmbeddingsPath = path.join(os.tmpdir(), `embeddings_${Date.now()}.json`);
                const tempConfigPath = path.join(os.tmpdir(), `config_${Date.now()}.json`);

                // enhanced config
                const enhancedConfig = {
                    ...config,
                    anchorInfluence: config.anchorInfluence || 0.45,
                    semanticThreshold: config.semanticThreshold || 0.7
                };

                // Save data and config
                fs.writeFileSync(tempEmbeddingsPath, JSON.stringify(clusteringData));
                fs.writeFileSync(tempConfigPath, JSON.stringify(enhancedConfig));

                const scriptPath = path.join(__dirname, 'cluster.py');

                // Execute Python clustering script
                const pythonProcess = exec(
                    `python "${scriptPath}" "${tempEmbeddingsPath}" "${tempConfigPath}"`,
                    { maxBuffer: 1024 * 1024 * 10 },
                    async (error, stdout, stderr) => {
                        // löscht die temporären Dateien
                        try {
                            fs.unlinkSync(tempEmbeddingsPath);
                            fs.unlinkSync(tempConfigPath);
                        } catch (cleanupError) {
                            console.error('Error cleaning up temp files:', cleanupError);
                        }

                        if (stderr) {
                            console.error(`Clustering output: ${stderr}`);
                        }

                        if (error) {
                            console.error(`Clustering execution error: ${error}`);
                            reject(error);
                            return;
                        }

                        try {
                            if (!stdout.trim()) {
                                throw new Error('No output from clustering script');
                            }

                            const result = JSON.parse(stdout.trim());

                            if (result.error) {
                                reject(new Error(result.error));
                                return;
                            }

                            // Process enhanced clustering results
                            const processedResult = {
                                labels: result.labels,
                                clusterStats: result.clustering_stats,
                                folderContext: null
                            };

                            // Add folder context if available
                            if (result.folder_context && folderData) {
                                processedResult.folderContext = {
                                    affinities: result.folder_context.affinities,
                                    statistics: result.folder_context.statistics,
                                    folderInfo: {
                                        names: folderData.names,
                                        hierarchy: folderData.hierarchy
                                    }
                                };
                            }

                            resolve(processedResult);
                        } catch (parseError) {
                            console.error('Raw clustering output:', stdout);
                            console.error('Parse error:', parseError);
                            reject(parseError);
                        }
                    }
                );

                pythonProcess.on('error', (error) => {
                    console.error('Process error:', error);
                    reject(error);
                });

            } catch (error) {
                reject(error);
            }
        };

        processingFunction().catch(reject);
    });
}

module.exports = { runClustering };