u
This commit is contained in:
@@ -74,6 +74,45 @@ let articleByInternalId = new Map();
|
|||||||
|
|
||||||
// #region Helpers
|
// #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<string>} 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
|
// #region getOrCreateFlexIndex
|
||||||
// MARK: constructs or retrieves FlexSearch document index
|
// MARK: constructs or retrieves FlexSearch document index
|
||||||
/**
|
/**
|
||||||
@@ -93,8 +132,8 @@ function getOrCreateFlexIndex(articles) {
|
|||||||
index: [
|
index: [
|
||||||
{ field: 'all', tokenize: 'forward', resolution: 9 },
|
{ field: 'all', tokenize: 'forward', resolution: 9 },
|
||||||
{ field: 'sku', tokenize: 'full', resolution: 9 },
|
{ field: 'sku', tokenize: 'full', resolution: 9 },
|
||||||
{ field: 'name', tokenize: 'forward', resolution: 9 },
|
{ field: 'name', tokenize: 'full', resolution: 9 },
|
||||||
{ field: 'brand', tokenize: 'forward', resolution: 5 },
|
{ field: 'brand', tokenize: 'full', resolution: 5 },
|
||||||
{ field: 'eans', tokenize: 'forward', resolution: 9 },
|
{ field: 'eans', tokenize: 'forward', resolution: 9 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -108,8 +147,10 @@ function getOrCreateFlexIndex(articles) {
|
|||||||
const docId = i;
|
const docId = i;
|
||||||
byId.set(docId, article);
|
byId.set(docId, article);
|
||||||
const sku = String(article.sku ?? '');
|
const sku = String(article.sku ?? '');
|
||||||
const name = String(article.name ?? '');
|
const rawName = String(article.name ?? '');
|
||||||
const brand = String(article.brand?.name || article.brandName || '');
|
const rawBrand = String(article.brand?.name || article.brandName || '');
|
||||||
|
const name = expandSearchTerms(rawName);
|
||||||
|
const brand = expandSearchTerms(rawBrand);
|
||||||
const eans = (article.eans ?? []).join(' ');
|
const eans = (article.eans ?? []).join(' ');
|
||||||
const all = `${sku} ${name} ${brand} ${eans}`.trim();
|
const all = `${sku} ${name} ${brand} ${eans}`.trim();
|
||||||
|
|
||||||
@@ -184,8 +225,10 @@ export function getArticleRelevanceScore(article, query) {
|
|||||||
if (!query) return 0;
|
if (!query) return 0;
|
||||||
const q = String(query).trim().toLowerCase();
|
const q = String(query).trim().toLowerCase();
|
||||||
const sku = String(article.sku ?? '').toLowerCase();
|
const sku = String(article.sku ?? '').toLowerCase();
|
||||||
const name = String(article.name ?? '').toLowerCase();
|
const rawName = String(article.name ?? '');
|
||||||
const brand = String(article.brand?.name || article.brandName || '').toLowerCase();
|
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());
|
const eans = (article.eans ?? []).map((e) => String(e).toLowerCase());
|
||||||
|
|
||||||
// 1. Exact SKU match
|
// 1. Exact SKU match
|
||||||
@@ -198,8 +241,9 @@ export function getArticleRelevanceScore(article, query) {
|
|||||||
if (name.startsWith(q)) return 3000;
|
if (name.startsWith(q)) return 3000;
|
||||||
// 5. SKU contains query
|
// 5. SKU contains query
|
||||||
if (sku.includes(q)) return 2000;
|
if (sku.includes(q)) return 2000;
|
||||||
|
|
||||||
|
const words = extractWords(rawName);
|
||||||
// 6. Word in name starts with query
|
// 6. Word in name starts with query
|
||||||
const words = name.split(/\s+/);
|
|
||||||
if (words.some((w) => w.startsWith(q))) return 1500;
|
if (words.some((w) => w.startsWith(q))) return 1500;
|
||||||
// 7. Name contains query
|
// 7. Name contains query
|
||||||
if (name.includes(q)) return 1000;
|
if (name.includes(q)) return 1000;
|
||||||
@@ -207,11 +251,35 @@ export function getArticleRelevanceScore(article, query) {
|
|||||||
if (eans.includes(q)) return 800;
|
if (eans.includes(q)) return 800;
|
||||||
// 9. EAN starts with query
|
// 9. EAN starts with query
|
||||||
if (eans.some((e) => e.startsWith(q))) return 600;
|
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;
|
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;
|
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;
|
return 10;
|
||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|||||||
Reference in New Issue
Block a user