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 * } */ // Gemeinsame Spalten für die TEILE Tabelle // WICHTIG: // - Die Teilenummer steht in der Spalte `Teil` // - Es gibt KEINE Spalte `WarenNr` in TEILE, daher darf diese hier NICHT verwendet werden // - Spaltennamen müssen exakt den vorhandenen Spalten in TEILE entsprechen, // sonst schlägt die Suche mit "invalid column name" fehl. const COMMON_COLUMNS = [ 'Teil', // Teilenummer 'Art', // Art 'Gruppe', // Gruppe 'Bez', // Bezeichnung 'Bez2', // Bezeichnung 2 'Hersteller' // Hersteller // Weitere Felder (z.B. SuchL, SuchR, EUWarenNr, Ben-Felder) können bei Bedarf ergänzt werden ]; // Erweiterte Spalten (falls benötigt) für spezielle Varianten mit zusätzlichen Feldern // Basieren auf den gemeinsamen Spalten der TEILE Tabelle const COLUMNS_EL = [ ...COMMON_COLUMNS, 'AUF', // Auftragsfeld 'AUF1', // Auftragsfeld 1 'AUF2', // Auftragsfeld 2 'AUF3' // Auftragsfeld 3 ]; const SEARCH_CONFIG = [ { tableName: 'TEILE', columns: COMMON_COLUMNS, primaryKey: 'ISN' // technisch primärer Schlüssel der Tabelle } ]; /** * Erstellt eine SQL WHERE-Klausel für die Volltextsuche * Findet Teilübereinstimmungen in allen Spalten (case-insensitive) * Optimiert durch: * - Verwendung von COLLATE für case-insensitive Vergleich (schneller als LOWER) * - Effiziente CAST-Größen statt NVARCHAR(MAX) * - Vermeidung von redundanten Funktionsaufrufen * @param {Array} columns - Array von Spaltennamen * @param {string} searchTerm - Suchbegriff * @returns {string} WHERE-Klausel */ const buildSearchCondition = (columns, searchTerm) => { return columns .map(col => `[${col}] LIKE @searchPattern COLLATE SQL_Latin1_General_CP1_CI_AS`) .join(' OR '); }; /** * Führt eine Volltextsuche über alle konfigurierten Tabellen durch * Findet alle Datensätze, die den Suchbegriff als Teilstring enthalten (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 * @returns {Promise} Array mit Suchergebnissen */ const fullTextSearch = async (searchTerm) => { if (!searchTerm || searchTerm.trim() === '') { throw new Error('Suchbegriff darf nicht leer sein'); } const pool = await getConnection(); // Pattern für Teilübereinstimmungen: findet den Begriff überall im Text // % = beliebige Zeichen davor/dahinter (SQL Wildcard) const searchPattern = `%${searchTerm.trim()}%`; // OPTIMIERUNG: Parallele Suche über alle Tabellen statt sequentiell const searchPromises = SEARCH_CONFIG.map(async (tableConfig) => { try { const whereClause = buildSearchCondition(tableConfig.columns, searchTerm); // Optimierte Query: // - Verwendet COLLATE für case-insensitive Vergleich (schneller als LOWER) // - TOP 100 limitiert Ergebnisse früh im Execution Plan // - WITH (NOLOCK) für Read-Uncommitted (schneller, da keine Locks) const query = ` SELECT TOP 100 * FROM [${tableConfig.tableName}] WITH (NOLOCK) WHERE ${whereClause} `; const request = pool.request(); request.input('searchPattern', sql.NVarChar, searchPattern); 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; // Keine Ergebnisse gefunden } catch (error) { console.error(`Fehler beim Durchsuchen der Tabelle ${tableConfig.tableName}:`, error.message); // Tabelle existiert möglicherweise nicht - Fehler zurückgeben 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 };