Fix product matching for Modern Minimalist Shelf Decor variants
Enhanced product matching algorithm to properly handle: - Large vs Small size variants for shelf decor products - Improved size extraction from product titles - Higher confidence thresholds for shelf decor products to prevent wrong matches - Better handling of 3D-Printed product variations - Enhanced logging for debugging matching issues This fixes the issue where packing slips would create duplicate products instead of matching to existing Large/Small variants of the same product.
This commit is contained in:
parent
ec1d204c36
commit
461e424e5e
1 changed files with 193 additions and 23 deletions
|
|
@ -29,6 +29,7 @@ export interface MatchingResult {
|
|||
/**
|
||||
* Match order items against existing products in the database
|
||||
* Considers size as a factor but ignores color variations
|
||||
* Enhanced for Modern Minimalist Shelf Decor products with Large/Small variants
|
||||
*/
|
||||
export const matchOrderItemsToProducts = (
|
||||
orderItems: any[],
|
||||
|
|
@ -41,44 +42,103 @@ export const matchOrderItemsToProducts = (
|
|||
let bestMatch: any = null;
|
||||
let bestConfidence = 0;
|
||||
|
||||
// Extract size from item title if present
|
||||
const itemSizeMatch = item.title.match(/Size:\s*([^,\s]+(?:\s+[^,\s]+)*)/i);
|
||||
const itemSize = itemSizeMatch ? itemSizeMatch[1].trim().toLowerCase() : '';
|
||||
// Enhanced size extraction - look for "Large" or "Small" specifically
|
||||
const itemTitle = item.title.toLowerCase();
|
||||
let itemSize = '';
|
||||
|
||||
// Clean item title for comparison (remove color but keep size info)
|
||||
// Check for Large/Small in various patterns
|
||||
if (itemTitle.includes('large') && !itemTitle.includes('small')) {
|
||||
itemSize = 'large';
|
||||
} else if (itemTitle.includes('small') && !itemTitle.includes('large')) {
|
||||
itemSize = 'small';
|
||||
}
|
||||
|
||||
// Also check for "Size: Large" pattern
|
||||
const sizeMatch = item.title.match(/Size:\s*(Large|Small|Med|Medium)/i);
|
||||
if (sizeMatch) {
|
||||
itemSize = sizeMatch[1].toLowerCase();
|
||||
if (itemSize === 'med' || itemSize === 'medium') itemSize = 'med';
|
||||
}
|
||||
|
||||
// Clean item title for comparison (remove color and size info for base matching)
|
||||
const cleanItemTitle = item.title
|
||||
.replace(/Colour:\s*[^,\s]+(?:\s+[^,\s]+)*/i, '') // Remove color
|
||||
.replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') // Remove color
|
||||
.replace(/Size:\s*[^,\s-]+/i, '') // Remove explicit size
|
||||
.replace(/3D-Printed/i, '') // Remove 3D-Printed as it's common
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
console.log('Matching item:', cleanItemTitle, 'Size:', itemSize);
|
||||
console.log('🔍 Matching item:', item.title);
|
||||
console.log(' Clean title:', cleanItemTitle);
|
||||
console.log(' Extracted size:', itemSize);
|
||||
|
||||
for (const product of products) {
|
||||
const productTitle = product.title.toLowerCase();
|
||||
let productSize = '';
|
||||
|
||||
// Extract size from product title
|
||||
if (productTitle.includes('large') && !productTitle.includes('small')) {
|
||||
productSize = 'large';
|
||||
} else if (productTitle.includes('small') && !productTitle.includes('large')) {
|
||||
productSize = 'small';
|
||||
} else if (productTitle.includes('med')) {
|
||||
productSize = 'med';
|
||||
}
|
||||
|
||||
// Clean product title for comparison
|
||||
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 productSize = product.size ? product.size.toLowerCase() : '';
|
||||
|
||||
// Calculate title similarity (basic word matching)
|
||||
const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle);
|
||||
|
||||
// Size matching bonus
|
||||
// Size matching - this is crucial for your shelf decor products
|
||||
let sizeSimilarity = 0;
|
||||
if (itemSize && productSize) {
|
||||
sizeSimilarity = itemSize === productSize ? 1 : 0;
|
||||
console.log(` Size match: ${itemSize} vs ${productSize} = ${sizeSimilarity}`);
|
||||
} else if (!itemSize && !productSize) {
|
||||
sizeSimilarity = 0.5; // Neutral if neither has size
|
||||
sizeSimilarity = 0.7; // Both have no size specified
|
||||
} else if (itemSize && !productSize) {
|
||||
// Item has size but product doesn't - penalize slightly
|
||||
sizeSimilarity = 0.3;
|
||||
} else {
|
||||
// Product has size but item doesn't - penalize more
|
||||
sizeSimilarity = 0.1;
|
||||
}
|
||||
|
||||
// Combined confidence score
|
||||
const confidence = (titleSimilarity * 0.8) + (sizeSimilarity * 0.2);
|
||||
// For shelf decor products, size is very important
|
||||
const isShelfDecor = cleanItemTitle.includes('shelf decor') || cleanProductTitle.includes('shelf decor');
|
||||
let confidence;
|
||||
|
||||
console.log(`Product: ${cleanProductTitle}, Title sim: ${titleSimilarity}, Size sim: ${sizeSimilarity}, Confidence: ${confidence}`);
|
||||
if (isShelfDecor && itemSize && productSize) {
|
||||
// For shelf decor with both sizes specified, size match is critical
|
||||
confidence = (titleSimilarity * 0.6) + (sizeSimilarity * 0.4);
|
||||
} else if (isShelfDecor) {
|
||||
// For shelf decor without clear size info, rely more on title
|
||||
confidence = (titleSimilarity * 0.8) + (sizeSimilarity * 0.2);
|
||||
} else {
|
||||
// For other products, standard weighting
|
||||
confidence = (titleSimilarity * 0.8) + (sizeSimilarity * 0.2);
|
||||
}
|
||||
|
||||
if (confidence > bestConfidence && confidence >= 0.5) { // Fixed: >= instead of > for exact threshold matches
|
||||
console.log(` Product: ${product.title}`);
|
||||
console.log(` Clean: ${cleanProductTitle}`);
|
||||
console.log(` Product size: ${productSize}`);
|
||||
console.log(` Title similarity: ${titleSimilarity.toFixed(3)}`);
|
||||
console.log(` Size similarity: ${sizeSimilarity}`);
|
||||
console.log(` Final confidence: ${confidence.toFixed(3)}`);
|
||||
|
||||
// Require higher confidence for shelf decor products to avoid wrong matches
|
||||
const minConfidence = isShelfDecor ? 0.7 : 0.5;
|
||||
|
||||
if (confidence > bestConfidence && confidence >= minConfidence) {
|
||||
bestMatch = product;
|
||||
bestConfidence = confidence;
|
||||
}
|
||||
|
|
@ -117,29 +177,52 @@ export const matchOrderItemsToProducts = (
|
|||
|
||||
/**
|
||||
* Calculate similarity between two product titles
|
||||
* Uses word matching and common substring analysis
|
||||
* Enhanced for Modern Minimalist Shelf Decor and similar products
|
||||
*/
|
||||
const calculateTitleSimilarity = (title1: string, title2: string): number => {
|
||||
// Split into words and filter out common stop words
|
||||
const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', '-', '–'];
|
||||
const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', '-', '–', '3d', 'printed'];
|
||||
|
||||
const words1 = title1.split(/\s+/).filter(word =>
|
||||
word.length > 2 && !stopWords.includes(word)
|
||||
word.length > 2 && !stopWords.includes(word.toLowerCase())
|
||||
);
|
||||
const words2 = title2.split(/\s+/).filter(word =>
|
||||
word.length > 2 && !stopWords.includes(word)
|
||||
word.length > 2 && !stopWords.includes(word.toLowerCase())
|
||||
);
|
||||
|
||||
if (words1.length === 0 || words2.length === 0) return 0;
|
||||
|
||||
// Count matching words
|
||||
// For shelf decor products, check for key identifying words
|
||||
const isShelfDecor1 = title1.includes('shelf') || title1.includes('decor');
|
||||
const isShelfDecor2 = title2.includes('shelf') || title2.includes('decor');
|
||||
|
||||
if (isShelfDecor1 && isShelfDecor2) {
|
||||
// Special handling for shelf decor products
|
||||
const keyWords = ['modern', 'minimalist', 'shelf', 'decor', 'water', 'reservoir', 'bookend', 'planter'];
|
||||
let keyWordMatches = 0;
|
||||
|
||||
for (const keyWord of keyWords) {
|
||||
const inTitle1 = title1.includes(keyWord);
|
||||
const inTitle2 = title2.includes(keyWord);
|
||||
if (inTitle1 && inTitle2) {
|
||||
keyWordMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
// If most key words match, it's likely the same product family
|
||||
if (keyWordMatches >= 4) {
|
||||
return 0.9; // High similarity for same product family
|
||||
}
|
||||
}
|
||||
|
||||
// Count matching words with fuzzy matching
|
||||
let matchingWords = 0;
|
||||
for (const word1 of words1) {
|
||||
for (const word2 of words2) {
|
||||
if (word1 === word2 ||
|
||||
word1.includes(word2) ||
|
||||
word2.includes(word1) ||
|
||||
levenshteinDistance(word1, word2) <= Math.max(1, Math.min(word1.length, word2.length) * 0.2)) {
|
||||
if (word1.toLowerCase() === word2.toLowerCase() ||
|
||||
word1.toLowerCase().includes(word2.toLowerCase()) ||
|
||||
word2.toLowerCase().includes(word1.toLowerCase()) ||
|
||||
levenshteinDistance(word1.toLowerCase(), word2.toLowerCase()) <= Math.max(1, Math.min(word1.length, word2.length) * 0.2)) {
|
||||
matchingWords++;
|
||||
break;
|
||||
}
|
||||
|
|
@ -151,6 +234,93 @@ const calculateTitleSimilarity = (title1: string, title2: string): number => {
|
|||
return matchingWords / totalUniqueWords;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find potential matches for a missing product
|
||||
* This helps suggest existing products that might be similar
|
||||
*/
|
||||
export const findPotentialMatches = (
|
||||
missingProductTitle: string,
|
||||
existingProducts: any[],
|
||||
maxSuggestions: number = 5
|
||||
): Array<{product: any; confidence: number; reason: string}> => {
|
||||
const suggestions: Array<{product: any; confidence: number; reason: string}> = [];
|
||||
|
||||
const itemTitle = missingProductTitle.toLowerCase();
|
||||
let itemSize = '';
|
||||
|
||||
// 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) {
|
||||
const productTitle = product.title.toLowerCase();
|
||||
let productSize = '';
|
||||
|
||||
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);
|
||||
|
||||
let reason = '';
|
||||
let confidence = titleSimilarity;
|
||||
|
||||
// Check if it's the same product family but different size
|
||||
if (titleSimilarity > 0.8) {
|
||||
if (itemSize && productSize && itemSize !== productSize) {
|
||||
reason = `Same product, different size (${productSize} vs ${itemSize})`;
|
||||
confidence = 0.95; // Very high confidence - likely meant to be this size variant
|
||||
} else if (itemSize && !productSize) {
|
||||
reason = `Same product, missing size specification`;
|
||||
confidence = 0.85;
|
||||
} else if (!itemSize && productSize) {
|
||||
reason = `Same product, has size: ${productSize}`;
|
||||
confidence = 0.8;
|
||||
} else {
|
||||
reason = `Very similar product`;
|
||||
confidence = titleSimilarity;
|
||||
}
|
||||
} else if (titleSimilarity > 0.6) {
|
||||
reason = `Similar product`;
|
||||
confidence = titleSimilarity;
|
||||
}
|
||||
|
||||
if (confidence > 0.6) {
|
||||
suggestions.push({
|
||||
product,
|
||||
confidence,
|
||||
reason
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by confidence and return top suggestions
|
||||
return suggestions
|
||||
.sort((a, b) => b.confidence - a.confidence)
|
||||
.slice(0, maxSuggestions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate Levenshtein distance between two strings
|
||||
* Used for fuzzy string matching
|
||||
|
|
|
|||
Loading…
Reference in a new issue