Source: backend/models/modelFolderSuggestion.js

  1. /**
  2. * @fileoverview Diese Datei enthält die Implementierung einer Engine zur Ordner-Vorschlagserstellung.
  3. * Sie ermöglicht die Berechnung von Ähnlichkeiten zwischen Dokument- und Ordner-Embeddings
  4. * und gibt passende Ordner-Vorschläge zurück.
  5. *
  6. * @author Lennart, Luca
  7. * Die Funktionen wurden mit Unterstützung von KI-Tools angepasst und optimiert.
  8. * @module modelFolderSuggestion
  9. */
  10. const { performance } = require('perf_hooks');
  11. const db = require('../../ConnectPostgres');
  12. const vectorOps = require('./modelVectorOperations');
  13. /**
  14. * Eine Engine zur Berechnung von Ordner-Vorschlägen basierend auf Dokument-Embeddings.
  15. *
  16. * @class FolderSuggestionEngine
  17. * @property {number} similarityThreshold - Der Ähnlichkeitsschwellenwert für Vorschläge.
  18. * @property {number} maxSuggestions - Maximale Anzahl an Ordner-Vorschlägen.
  19. * @property {Map} similarityCache - Cache zur Speicherung berechneter Ähnlichkeiten.
  20. * @property {Object} metrics - Statistik-Daten zur Performance-Messung.
  21. */
  22. class FolderSuggestionEngine {
  23. constructor(options = {}) {
  24. this.similarityThreshold = options.similarityThreshold || 0.75;
  25. this.maxSuggestions = options.maxSuggestions || 3;
  26. this.similarityCache = new Map();
  27. this.metrics = {
  28. totalRequests: 0,
  29. successfulRequests: 0,
  30. failedRequests: 0,
  31. averageResponseTime: 0,
  32. lastError: null
  33. };
  34. }
  35. /**
  36. * Gibt eine Liste empfohlener Ordner für ein gegebenes Dokument-Embedding zurück.
  37. *
  38. * @async
  39. * @method getSuggestedFolders
  40. * @memberof FolderSuggestionEngine
  41. * @param {Object} params - Parameter-Objekt.
  42. * @param {number[]} params.docEmbedding - Das Embedding des Dokuments.
  43. * @param {number} params.userId - Die Benutzer-ID.
  44. * @returns {Promise<Object>} Ein Objekt mit den empfohlenen Ordnern und der Verarbeitungszeit.
  45. * @throws {Error} Falls die Vorschlagsberechnung fehlschlägt.
  46. * @example
  47. * const suggestions = await folderEngine.getSuggestedFolders({ docEmbedding, userId: 123 });
  48. * console.log(suggestions);
  49. */
  50. async getSuggestedFolders({ docEmbedding, userId }) {
  51. const startTime = performance.now();
  52. this.metrics.totalRequests++;
  53. const cacheKey = `folder_suggestions:${userId}:${this._hashEmbedding(docEmbedding)}`;
  54. try {
  55. if (this.similarityCache.has(cacheKey)) {
  56. return this.similarityCache.get(cacheKey);
  57. }
  58. const foldersData = await this._getFolderData(userId);
  59. if (foldersData.rows.length === 0) {
  60. const result = {
  61. suggestedFolders: [],
  62. processingTime: performance.now() - startTime
  63. };
  64. this._cacheResult(cacheKey, result);
  65. return result;
  66. }
  67. const suggestions = await this._processSuggestions(
  68. docEmbedding,
  69. foldersData.rows
  70. );
  71. const result = {
  72. suggestedFolders: suggestions.slice(0, this.maxSuggestions),
  73. processingTime: performance.now() - startTime
  74. };
  75. this._cacheResult(cacheKey, result);
  76. this.metrics.successfulRequests++;
  77. this.metrics.averageResponseTime =
  78. (this.metrics.averageResponseTime * (this.metrics.successfulRequests - 1) +
  79. (performance.now() - startTime)) / this.metrics.successfulRequests;
  80. return result;
  81. } catch (error) {
  82. this.metrics.failedRequests++;
  83. this.metrics.lastError = error.message;
  84. throw error;
  85. }
  86. }
  87. /**
  88. * Berechnet die Ähnlichkeit zwischen zwei Embeddings unter Verwendung vordefinierter Methoden.
  89. *
  90. * @async
  91. * @method _calculateSimilarity
  92. * @memberof FolderSuggestionEngine
  93. * @param {number[]} embedding1 - Erstes Embedding.
  94. * @param {number[]} embedding2 - Zweites Embedding.
  95. * @param {string} [cacheKey=null] - Optionaler Cache-Schlüssel für schnelleren Zugriff.
  96. * @returns {Promise<number>} Der berechnete Ähnlichkeitswert zwischen 0 und 1.
  97. */
  98. async _calculateSimilarity(embedding1, embedding2, cacheKey = null) {
  99. if (cacheKey && this.similarityCache.has(cacheKey)) {
  100. return this.similarityCache.get(cacheKey);
  101. }
  102. try {
  103. const similarity = vectorOps.calculateSimilarity(embedding1, embedding2);
  104. if (cacheKey && similarity > this.similarityThreshold * 0.8) {
  105. this.similarityCache.set(cacheKey, similarity);
  106. }
  107. return similarity;
  108. } catch (error) {
  109. console.error('Error calculating similarity:', error);
  110. return 0;
  111. }
  112. }
  113. /**
  114. * Verarbeitet die Ordner-Daten und berechnet passende Vorschläge basierend auf Ähnlichkeiten.
  115. *
  116. * @async
  117. * @method _processSuggestions
  118. * @memberof FolderSuggestionEngine
  119. * @param {number[]} docEmbedding - Das Embedding des Dokuments.
  120. * @param {Array<Object>} folders - Liste der verfügbaren Ordner mit ihren Embeddings.
  121. * @returns {Promise<Array<Object>>} Eine sortierte Liste der besten Ordner-Vorschläge.
  122. */
  123. async _processSuggestions(docEmbedding, folders) {
  124. const suggestions = [];
  125. try {
  126. for (const folder of folders) {
  127. const similarity = await this._calculateSimilarity(
  128. docEmbedding,
  129. folder.embedding,
  130. folder.folder_id
  131. );
  132. // Remove the similarity threshold check to get all suggestions
  133. suggestions.push({
  134. folderId: folder.folder_id,
  135. folderName: folder.folder_name,
  136. similarity: parseFloat(similarity.toFixed(4)),
  137. fileCount: folder.file_count,
  138. parentId: folder.parent_folder_id,
  139. recentFiles: folder.recent_files,
  140. confidence: this._calculateConfidence(similarity, folder)
  141. });
  142. }
  143. // Nach Ähnlichkeit sortieren und die besten Vorschläge auswählen
  144. return suggestions
  145. .sort((a, b) => b.similarity - a.similarity)
  146. .slice(0, this.maxSuggestions); // immer maxSuggestions number of folders zurückgeben
  147. } catch (error) {
  148. console.error('Error processing suggestions:', error);
  149. return [];
  150. }
  151. }
  152. /**
  153. * Ruft die verfügbaren Ordnerdaten für einen bestimmten Benutzer aus der Datenbank ab.
  154. *
  155. * @async
  156. * @method _getFolderData
  157. * @memberof FolderSuggestionEngine
  158. * @param {number} userId - Die Benutzer-ID.
  159. * @returns {Promise<Object>} Ordnerdaten inklusive Embeddings, Datei-Anzahl und letzte Dateien.
  160. */
  161. async _getFolderData(userId) {
  162. const query = `
  163. WITH RankedFiles AS (
  164. SELECT
  165. f.folder_id,
  166. f.folder_name,
  167. f.embedding,
  168. f.parent_folder_id,
  169. fl.file_name,
  170. ROW_NUMBER() OVER (PARTITION BY f.folder_id ORDER BY fl.created_at DESC) as rn
  171. FROM main.folders f
  172. LEFT JOIN main.files fl ON f.folder_id = fl.folder_id
  173. WHERE f.user_id = $1
  174. AND f.embedding IS NOT NULL
  175. )
  176. SELECT
  177. folder_id,
  178. folder_name,
  179. embedding,
  180. parent_folder_id,
  181. COUNT(file_name) as file_count,
  182. array_agg(
  183. CASE WHEN rn <= 5 THEN file_name ELSE NULL END
  184. ) FILTER (WHERE rn <= 5) as recent_files
  185. FROM RankedFiles
  186. GROUP BY folder_id, folder_name, embedding, parent_folder_id
  187. `;
  188. return await db.query(query, [userId]);
  189. }
  190. /**
  191. * Berechnet einen Konfidenzwert für einen Ordner-Vorschlag basierend auf mehreren Faktoren.
  192. *
  193. * @method _calculateConfidence
  194. * @memberof FolderSuggestionEngine
  195. * @param {number} similarity - Der berechnete Ähnlichkeitswert.
  196. * @param {Object} folder - Das Ordner-Objekt mit Metadaten.
  197. * @returns {number} Der berechnete Konfidenzwert (zwischen 0 und 1).
  198. */
  199. _calculateConfidence(similarity, folder) {
  200. const baseFactor = 0.7;
  201. const fileCountFactor = Math.min(folder.file_count / 10, 0.2);
  202. const recentFilesFactor = folder.recent_files?.length ? 0.1 : 0;
  203. return similarity;
  204. }
  205. /**
  206. * Erstellt einen eindeutigen Hash-Wert für ein Embedding, um es im Cache zu speichern.
  207. *
  208. * @method _hashEmbedding
  209. * @memberof FolderSuggestionEngine
  210. * @param {number[]} embedding - Das Embedding als Array von Zahlen.
  211. * @returns {string} Ein Base64-gekürzter Hash-String.
  212. */
  213. _hashEmbedding(embedding) {
  214. return Buffer.from(embedding.join(',')).toString('base64').slice(0, 10);
  215. }
  216. /**
  217. * Speichert ein Ergebnis im Cache und entfernt alte Einträge, falls der Cache zu groß wird.
  218. *
  219. * @method _cacheResult
  220. * @memberof FolderSuggestionEngine
  221. * @param {string} key - Der Cache-Schlüssel.
  222. * @param {Object} value - Das zu speichernde Ergebnis.
  223. */
  224. _cacheResult(key, value) {
  225. this.similarityCache.set(key, value);
  226. if (this.similarityCache.size > 1000) {
  227. const firstKey = this.similarityCache.keys().next().value;
  228. this.similarityCache.delete(firstKey);
  229. }
  230. }
  231. }
  232. module.exports = new FolderSuggestionEngine({
  233. similarityThreshold: 0.75,
  234. maxSuggestions: 3
  235. });