Fix size-aware product suggestions in resolver
Previously a different-size variant was boosted to 0.95 confidence
('same product, different size'), so a Small order would be confidently
suggested the Large product. Now suggestions require the same size; a
known different size is suppressed entirely (you create the right one
instead). Also handle 'Size: X for ...' phrasing, medium sizes, and
American 'Color:' spelling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3c3adcdea9
commit
46d1ca3375
1 changed files with 57 additions and 55 deletions
|
|
@ -260,6 +260,31 @@ const calculateTitleSimilarity = (title1: string, title2: string): number => {
|
||||||
return matchingWords / totalUniqueWords;
|
return matchingWords / totalUniqueWords;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a size token (large/med/small) from a title or variant string.
|
||||||
|
* Prefers an explicit "Size: X" so trailing words like "Large for med plants"
|
||||||
|
* don't get misread.
|
||||||
|
*/
|
||||||
|
export const extractSize = (text: string): string => {
|
||||||
|
const lower = text.toLowerCase();
|
||||||
|
const explicit = lower.match(/size:\s*(large|medium|med|small)/);
|
||||||
|
let token = explicit ? explicit[1] : '';
|
||||||
|
if (!token) {
|
||||||
|
if (lower.includes('large') && !lower.includes('small')) token = 'large';
|
||||||
|
else if (lower.includes('small') && !lower.includes('large')) token = 'small';
|
||||||
|
}
|
||||||
|
return token === 'medium' ? 'med' : token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanForCompare = (title: string): string =>
|
||||||
|
title
|
||||||
|
.replace(/Colou?r:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') // strip Color/Colour
|
||||||
|
.replace(/Size:\s*[^,\s-]+(?:\s+(?:for|plants?|succulents?)\b[^,]*)?/i, '') // strip "Size: X for ..."
|
||||||
|
.replace(/3D-Printed/i, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find potential matches for a missing product
|
* Find potential matches for a missing product
|
||||||
* This helps suggest existing products that might be similar
|
* This helps suggest existing products that might be similar
|
||||||
|
|
@ -270,77 +295,54 @@ export const findPotentialMatches = (
|
||||||
maxSuggestions: number = 5
|
maxSuggestions: number = 5
|
||||||
): Array<{product: any; confidence: number; reason: string}> => {
|
): Array<{product: any; confidence: number; reason: string}> => {
|
||||||
const suggestions: Array<{product: any; confidence: number; reason: string}> = [];
|
const suggestions: Array<{product: any; confidence: number; reason: string}> = [];
|
||||||
|
|
||||||
const itemTitle = missingProductTitle.toLowerCase();
|
const itemSize = extractSize(missingProductTitle);
|
||||||
let itemSize = '';
|
const cleanItemTitle = cleanForCompare(missingProductTitle);
|
||||||
|
|
||||||
// Extract size from missing product
|
|
||||||
if (itemTitle.includes('large') && !itemTitle.includes('small')) {
|
|
||||||
itemSize = 'large';
|
|
||||||
} else if (itemTitle.includes('small') && !itemTitle.includes('large')) {
|
|
||||||
itemSize = 'small';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanItemTitle = missingProductTitle
|
|
||||||
.replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '')
|
|
||||||
.replace(/Size:\s*[^,\s-]+/i, '')
|
|
||||||
.replace(/3D-Printed/i, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
for (const product of existingProducts) {
|
for (const product of existingProducts) {
|
||||||
const productTitle = product.title.toLowerCase();
|
const productSize = extractSize(`${product.title} ${(product.aliases || []).join(' ')}`);
|
||||||
let productSize = '';
|
const cleanProductTitle = cleanForCompare(product.title);
|
||||||
|
|
||||||
if (productTitle.includes('large') && !productTitle.includes('small')) {
|
|
||||||
productSize = 'large';
|
|
||||||
} else if (productTitle.includes('small') && !productTitle.includes('large')) {
|
|
||||||
productSize = 'small';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanProductTitle = product.title
|
|
||||||
.replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '')
|
|
||||||
.replace(/Size:\s*[^,\s-]+/i, '')
|
|
||||||
.replace(/3D-Printed/i, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle);
|
const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle);
|
||||||
|
|
||||||
let reason = '';
|
let reason = '';
|
||||||
let confidence = titleSimilarity;
|
let confidence = 0;
|
||||||
|
|
||||||
// Check if it's the same product family but different size
|
|
||||||
if (titleSimilarity > 0.8) {
|
if (titleSimilarity > 0.8) {
|
||||||
if (itemSize && productSize && itemSize !== productSize) {
|
if (itemSize && productSize) {
|
||||||
reason = `Same product, different size (${productSize} vs ${itemSize})`;
|
if (itemSize === productSize) {
|
||||||
confidence = 0.95; // Very high confidence - likely meant to be this size variant
|
reason = `Same product, same size (${itemSize})`;
|
||||||
|
confidence = Math.max(titleSimilarity, 0.95);
|
||||||
|
} else {
|
||||||
|
// Both sizes known and they differ — this is NOT the right product.
|
||||||
|
// Suppress it so a wrong-size variant is never suggested.
|
||||||
|
confidence = 0;
|
||||||
|
}
|
||||||
} else if (itemSize && !productSize) {
|
} else if (itemSize && !productSize) {
|
||||||
reason = `Same product, missing size specification`;
|
reason = `Same product (existing has no size set)`;
|
||||||
confidence = 0.85;
|
confidence = 0.75;
|
||||||
} else if (!itemSize && productSize) {
|
} else if (!itemSize && productSize) {
|
||||||
reason = `Same product, has size: ${productSize}`;
|
reason = `Same product, has size: ${productSize}`;
|
||||||
confidence = 0.8;
|
confidence = 0.7;
|
||||||
} else {
|
} else {
|
||||||
reason = `Very similar product`;
|
reason = `Very similar product`;
|
||||||
confidence = titleSimilarity;
|
confidence = titleSimilarity;
|
||||||
}
|
}
|
||||||
} else if (titleSimilarity > 0.6) {
|
} else if (titleSimilarity > 0.6) {
|
||||||
reason = `Similar product`;
|
// Don't suggest a different-size variant even on a looser title match
|
||||||
confidence = titleSimilarity;
|
if (itemSize && productSize && itemSize !== productSize) {
|
||||||
|
confidence = 0;
|
||||||
|
} else {
|
||||||
|
reason = `Similar product`;
|
||||||
|
confidence = titleSimilarity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confidence > 0.6) {
|
if (confidence > 0.6) {
|
||||||
suggestions.push({
|
suggestions.push({ product, confidence, reason });
|
||||||
product,
|
|
||||||
confidence,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by confidence and return top suggestions
|
// Sort by confidence and return top suggestions
|
||||||
return suggestions
|
return suggestions
|
||||||
.sort((a, b) => b.confidence - a.confidence)
|
.sort((a, b) => b.confidence - a.confidence)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue