From ea52753ef96cf33fbaa32349ad5aa3b8303787bf Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 18 Oct 2026 04:01:40 +0200 Subject: [PATCH] u --- src/automats/articleSearchAutomat.js | 86 +++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/src/automats/articleSearchAutomat.js b/src/automats/articleSearchAutomat.js index f78ea25..bc74741 100644 --- a/src/automats/articleSearchAutomat.js +++ b/src/automats/articleSearchAutomat.js @@ -74,6 +74,45 @@ let articleByInternalId = new Map(); // #region Helpers +// #region expandSearchTerms +// MARK: expands search strings by splitting camelCase and punctuation +/** + * Expands a string by splitting camelCase words and punctuation delimiters + * into separate words so that sub-words (e.g. "BioBizz" -> "Bio Bizz", "Light-Mix" -> "Light Mix") + * can be indexed and searched by prefix or infix. + * + * @param {string} str Raw text string. + * @returns {string} Text with added spaced variants. + */ +function expandSearchTerms(str) { + if (!str) return ''; + const camel = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); + const normalized = camel.replace(/[-_./·+]/g, ' '); + if (normalized === str) return str; + return `${str} ${normalized}`; +} +// #endregion + +// #region extractWords +// MARK: extracts normalized word tokens from a string +/** + * Splits text into individual lowercase word tokens, splitting across + * camelCase transitions and punctuation delimiters. + * + * @param {string} str Raw text. + * @returns {Array} Normalized word list. + */ +function extractWords(str) { + if (!str) return []; + return str + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[-_./·+]/g, ' ') + .toLowerCase() + .split(/\s+/) + .filter(Boolean); +} +// #endregion + // #region getOrCreateFlexIndex // MARK: constructs or retrieves FlexSearch document index /** @@ -93,8 +132,8 @@ function getOrCreateFlexIndex(articles) { index: [ { field: 'all', tokenize: 'forward', resolution: 9 }, { field: 'sku', tokenize: 'full', resolution: 9 }, - { field: 'name', tokenize: 'forward', resolution: 9 }, - { field: 'brand', tokenize: 'forward', resolution: 5 }, + { field: 'name', tokenize: 'full', resolution: 9 }, + { field: 'brand', tokenize: 'full', resolution: 5 }, { field: 'eans', tokenize: 'forward', resolution: 9 }, ], }, @@ -108,8 +147,10 @@ function getOrCreateFlexIndex(articles) { const docId = i; byId.set(docId, article); const sku = String(article.sku ?? ''); - const name = String(article.name ?? ''); - const brand = String(article.brand?.name || article.brandName || ''); + const rawName = String(article.name ?? ''); + const rawBrand = String(article.brand?.name || article.brandName || ''); + const name = expandSearchTerms(rawName); + const brand = expandSearchTerms(rawBrand); const eans = (article.eans ?? []).join(' '); const all = `${sku} ${name} ${brand} ${eans}`.trim(); @@ -184,8 +225,10 @@ export function getArticleRelevanceScore(article, query) { if (!query) return 0; const q = String(query).trim().toLowerCase(); const sku = String(article.sku ?? '').toLowerCase(); - const name = String(article.name ?? '').toLowerCase(); - const brand = String(article.brand?.name || article.brandName || '').toLowerCase(); + const rawName = String(article.name ?? ''); + const name = rawName.toLowerCase(); + const rawBrand = String(article.brand?.name || article.brandName || ''); + const brand = rawBrand.toLowerCase(); const eans = (article.eans ?? []).map((e) => String(e).toLowerCase()); // 1. Exact SKU match @@ -198,8 +241,9 @@ export function getArticleRelevanceScore(article, query) { if (name.startsWith(q)) return 3000; // 5. SKU contains query if (sku.includes(q)) return 2000; + + const words = extractWords(rawName); // 6. Word in name starts with query - const words = name.split(/\s+/); if (words.some((w) => w.startsWith(q))) return 1500; // 7. Name contains query if (name.includes(q)) return 1000; @@ -207,11 +251,35 @@ export function getArticleRelevanceScore(article, query) { if (eans.includes(q)) return 800; // 9. EAN starts with query if (eans.some((e) => e.startsWith(q))) return 600; - // 10. Brand starts with query + + const brandWords = extractWords(rawBrand); + // 10. Brand exact match + if (brand === q) return 500; + // 11. Brand starts with query if (brand.startsWith(q)) return 400; - // 11. Brand contains query + // 12. Word in brand starts with query + if (brandWords.some((w) => w.startsWith(q))) return 350; + // 13. Brand contains query if (brand.includes(q)) return 200; + // 14. Multi-word query where all terms match across article fields + const qWords = q.split(/\s+/).filter(Boolean); + if (qWords.length > 1) { + const allMatch = qWords.every((term) => + words.some((w) => w.startsWith(term)) || + name.includes(term) || + brandWords.some((w) => w.startsWith(term)) || + brand.includes(term) || + sku.includes(term) + ); + if (allMatch) { + const prefixCount = qWords.filter((term) => + words.some((w) => w.startsWith(term)) || brandWords.some((w) => w.startsWith(term)) + ).length; + return 1200 + prefixCount * 300; + } + } + return 10; } // #endregion