201 lines
5.8 KiB
JavaScript
201 lines
5.8 KiB
JavaScript
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 (nur diese 8 Felder, siehe Spalten.txt)
|
|
// Text aus TEXT (TEILE.MemoID = TEXT.TextId), PrsVK aus TSSAEF (Teil = Teil)
|
|
const TEILE_OUTPUT_COLUMNS = [
|
|
'Teil',
|
|
'Bez',
|
|
'Bez2',
|
|
'Ben7',
|
|
'Ben8',
|
|
'Hersteller',
|
|
'Text', // aus Tabelle TEXT
|
|
'PrsVK' // aus Tabelle TSSAEF
|
|
];
|
|
|
|
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)
|
|
* @returns {Promise<Array>} Array mit Suchergebnissen
|
|
*/
|
|
const fullTextSearch = async (searchTerm) => {
|
|
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: Feste 8 Spalten mit JOINs auf TEXT (MemoID=TextId) und TSSAEF (Teil)
|
|
const searchPromises = SEARCH_CONFIG.map(async (tableConfig) => {
|
|
try {
|
|
// Pro Wort: (Spalte1 LIKE @searchWord0 OR ... OR SpalteN LIKE @searchWord0), alle mit AND verknüpft
|
|
const wordConditions = words.map((_, i) => {
|
|
const paramName = `searchWord${i}`;
|
|
const cond = buildSingleWordCondition(tableConfig.columns, paramName, 't');
|
|
return `(${cond})`;
|
|
});
|
|
const whereClause = wordConditions.join(' AND ');
|
|
|
|
const query = `
|
|
SELECT TOP 100
|
|
t.Teil,
|
|
t.Bez,
|
|
t.Bez2,
|
|
t.Ben7,
|
|
t.Ben8,
|
|
t.Hersteller,
|
|
txt.Text AS [Text],
|
|
ts.PrsVk AS PrsVK
|
|
FROM [${tableConfig.tableName}] t WITH (NOLOCK)
|
|
OUTER APPLY (
|
|
SELECT TOP 1 [Text]
|
|
FROM [TEXT] WITH (NOLOCK)
|
|
WHERE TextId = COALESCE(NULLIF(RTRIM(LTRIM(t.MemoID)), N''), N'MEM-' + t.Teil)
|
|
ORDER BY LfdNr
|
|
) txt
|
|
OUTER APPLY (
|
|
SELECT TOP 1 PrsVk
|
|
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}%`);
|
|
});
|
|
|
|
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>} 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>} 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
|
|
};
|
|
|