const { sql, getConnection } = require('../config/database'); /** * Definieren Sie hier die Tabellen und Spalten, die durchsucht werden sollen * * Format: * { * tableName: 'TabellenName', * columns: ['Spalte1', 'Spalte2', 'Spalte3'], * primaryKey: 'ID' // Primärschlüssel der Tabelle * } */ // Spalten für die Volltextsuche in TEILE (WHERE-Klausel) const TEILE_SEARCH_COLUMNS = [ 'Teil', 'Bez', 'Bez2', 'Ben7', 'Ben8', 'Hersteller' ]; // Ausgabe-Spalten für Suchergebnisse (siehe Spalten.txt) // Text aus TEXT: MemoID aus EKATSS (EKATSS.TEIL = Teil), dann TEXT.TextId = MemoID; PrsVK + Ersatz aus TSSAEF (Teil = Teil) const TEILE_OUTPUT_COLUMNS = [ 'Teil', 'Bez', 'Bez2', 'Ben7', 'Ben8', 'Hersteller', 'Text', // aus Tabelle TEXT 'PrsVK', // aus Tabelle TSSAEF 'Ersatz' // aus Tabelle TSSAEF (Ersetzte durch) ]; const SEARCH_CONFIG = [ { tableName: 'TEILE', columns: TEILE_SEARCH_COLUMNS, primaryKey: 'ISN' } ]; /** * Erstellt eine SQL-Bedingung für ein einzelnes Wort: mindestens eine Spalte muss das Wort enthalten * @param {Array} columns - Array von Spaltennamen * @param {string} paramName - Name des SQL-Parameters (z. B. "searchWord0") * @param {string} [tableAlias] - Optionaler Tabellenalias (z. B. "t" für t.[Teil]) * @returns {string} Bedingung (OR über alle Spalten) */ const buildSingleWordCondition = (columns, paramName, tableAlias = '') => { const prefix = tableAlias ? `${tableAlias}.` : ''; return columns .map(col => `${prefix}[${col}] LIKE @${paramName} COLLATE SQL_Latin1_General_CP1_CI_AS`) .join(' OR '); }; /** * Führt eine Volltextsuche über alle konfigurierten Tabellen durch * Jedes Wort der Eingabe wird als eigener Suchbegriff behandelt; ein Treffer muss alle Wörter enthalten (AND). * Findet Datensätze, in denen jedes Wort als Teilstring in mindestens einer Spalte vorkommt (case-insensitive) * * Optimierungen: * - Parallele Suche über alle Tabellen (Promise.all statt sequentiell) * - Effiziente WHERE-Klauseln ohne CAST zu NVARCHAR(MAX) * - Case-insensitive Suche via COLLATE statt LOWER() * - Optimierte Query-Struktur mit TOP für bessere Performance * * @param {string} searchTerm - Der zu suchende Begriff (mehrere Wörter möglich, getrennt durch Leerzeichen) * @param {string} [statusFilter] - Optional: 'aktiv' | 'pruefbar' | 'inaktiv' (leer/undefined = alle) * @returns {Promise} Array mit Suchergebnissen */ const fullTextSearch = async (searchTerm, statusFilter) => { if (!searchTerm || searchTerm.trim() === '') { throw new Error('Suchbegriff darf nicht leer sein'); } const words = searchTerm.trim().split(/\s+/).filter(Boolean); if (words.length === 0) { throw new Error('Suchbegriff darf nicht leer sein'); } const pool = await getConnection(); // TEILE: Ausgabe-Spalten mit JOINs auf EKATSS (MemoID), TEXT (TextId=MemoID), TSSAEF (Teil) const searchPromises = SEARCH_CONFIG.map(async (tableConfig) => { try { // Pro Wort: Treffer in TEILE-Spalten ODER im Zusatztext (TEXT über EKATSS); alle Wörter mit AND const wordConditions = words.map((_, i) => { const paramName = `searchWord${i}`; const teileCond = buildSingleWordCondition(tableConfig.columns, paramName, 't'); const textCond = `EXISTS ( SELECT 1 FROM EKATSS ek WITH (NOLOCK) INNER JOIN [TEXT] tx WITH (NOLOCK) ON tx.TextId = ek.MemoID WHERE ek.[Teil] = t.Teil AND tx.[Text] LIKE @${paramName} COLLATE SQL_Latin1_General_CP1_CI_AS )`; return `(${teileCond} OR ${textCond})`; }); let whereClause = wordConditions.join(' AND '); // Status-Filter: aktiv = leer/NULL, pruefbar = L, inaktiv = i if (statusFilter === 'aktiv') { whereClause += ` AND (t.Stat IS NULL OR LTRIM(RTRIM(ISNULL(t.Stat, N''))) = N'')`; } else if (statusFilter === 'pruefbar') { whereClause += ` AND t.Stat = @statusFilter`; } else if (statusFilter === 'inaktiv') { whereClause += ` AND t.Stat = @statusFilter`; } const query = ` SELECT TOP 100 t.Teil, t.Bez, t.Bez2, t.Ben7, t.Ben8, t.Hersteller, t.Stat, t.VkTeil, txt.Text AS [Text], ts.PrsVk AS PrsVK, ts.Ersatz AS Ersatz FROM [${tableConfig.tableName}] t WITH (NOLOCK) OUTER APPLY ( SELECT TOP 1 MemoID FROM EKATSS WITH (NOLOCK) WHERE [Teil] = t.Teil ORDER BY (SELECT NULL) ) ek OUTER APPLY ( SELECT STRING_AGG(tx.[Text], CHAR(10)) WITHIN GROUP (ORDER BY tx.LfdNr) AS [Text] FROM [TEXT] tx WITH (NOLOCK) WHERE tx.TextId = ek.MemoID ) txt OUTER APPLY ( SELECT TOP 1 PrsVk, Ersatz FROM TSSAEF WITH (NOLOCK) WHERE Teil = t.Teil ORDER BY ISN ) ts WHERE ${whereClause} `; const request = pool.request(); words.forEach((word, i) => { request.input(`searchWord${i}`, sql.NVarChar, `%${word}%`); }); if (statusFilter === 'pruefbar') { request.input('statusFilter', sql.NVarChar(10), 'L'); } else if (statusFilter === 'inaktiv') { request.input('statusFilter', sql.NVarChar(10), 'i'); } const result = await request.query(query); if (result.recordset && result.recordset.length > 0) { return { tableName: tableConfig.tableName, matchCount: result.recordset.length, records: result.recordset }; } return null; } catch (error) { console.error(`Fehler beim Durchsuchen der Tabelle ${tableConfig.tableName}:`, error.message); return { tableName: tableConfig.tableName, matchCount: 0, records: [], error: error.message }; } }); // Warte auf alle Suchanfragen parallel const allResults = await Promise.all(searchPromises); // Filtere null-Werte (Tabellen ohne Ergebnisse) heraus const results = allResults.filter(result => result !== null); return results; }; /** * Holt alle verfügbaren Tabellen aus der Datenbank * @returns {Promise} Array mit Tabellennamen */ const getAvailableTables = async () => { const pool = await getConnection(); const query = ` SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME `; const result = await pool.request().query(query); return result.recordset.map(row => row.TABLE_NAME); }; /** * Holt die Spalten einer spezifischen Tabelle * @param {string} tableName - Name der Tabelle * @returns {Promise} Array mit Spalteninformationen */ const getTableColumns = async (tableName) => { const pool = await getConnection(); const query = ` SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName ORDER BY ORDINAL_POSITION `; const request = pool.request(); request.input('tableName', sql.NVarChar, tableName); const result = await request.query(query); return result.recordset; }; module.exports = { fullTextSearch, getAvailableTables, getTableColumns, SEARCH_CONFIG };