Update Tabellen und Suchalgo

This commit is contained in:
2026-02-17 17:29:40 +01:00
parent ce89fccdb5
commit 441ba313aa
12 changed files with 131 additions and 70 deletions

View File

@@ -10,60 +10,55 @@ const { sql, getConnection } = require('../config/database');
* 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
// Spalten für die Volltextsuche in TEILE (WHERE-Klausel)
const TEILE_SEARCH_COLUMNS = [
'Teil',
'Bez',
'Bez2',
'Ben7',
'Ben8',
'Hersteller'
];
// 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
// 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: COMMON_COLUMNS,
primaryKey: 'ISN' // technisch primärer Schlüssel der Tabelle
columns: TEILE_SEARCH_COLUMNS,
primaryKey: 'ISN'
}
];
/**
* 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
* Erstellt eine SQL-Bedingung für ein einzelnes Wort: mindestens eine Spalte muss das Wort enthalten
* @param {Array} columns - Array von Spaltennamen
* @param {string} searchTerm - Suchbegriff
* @returns {string} WHERE-Klausel
* @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 buildSearchCondition = (columns, searchTerm) => {
const buildSingleWordCondition = (columns, paramName, tableAlias = '') => {
const prefix = tableAlias ? `${tableAlias}.` : '';
return columns
.map(col => `[${col}] LIKE @searchPattern COLLATE SQL_Latin1_General_CP1_CI_AS`)
.map(col => `${prefix}[${col}] LIKE @${paramName} 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)
* 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)
@@ -71,7 +66,7 @@ const buildSearchCondition = (columns, searchTerm) => {
* - Case-insensitive Suche via COLLATE statt LOWER()
* - Optimierte Query-Struktur mit TOP für bessere Performance
*
* @param {string} searchTerm - Der zu suchende Begriff
* @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) => {
@@ -79,30 +74,55 @@ const fullTextSearch = async (searchTerm) => {
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()}%`;
const words = searchTerm.trim().split(/\s+/).filter(Boolean);
if (words.length === 0) {
throw new Error('Suchbegriff darf nicht leer sein');
}
// OPTIMIERUNG: Parallele Suche über alle Tabellen statt sequentiell
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 {
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)
// 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 *
FROM [${tableConfig.tableName}] WITH (NOLOCK)
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();
request.input('searchPattern', sql.NVarChar, searchPattern);
words.forEach((word, i) => {
request.input(`searchWord${i}`, sql.NVarChar, `%${word}%`);
});
const result = await request.query(query);
if (result.recordset && result.recordset.length > 0) {
@@ -112,11 +132,9 @@ const fullTextSearch = async (searchTerm) => {
records: result.recordset
};
}
return null; // Keine Ergebnisse gefunden
return null;
} 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,