189 lines
5.4 KiB
Go
189 lines
5.4 KiB
Go
package feeds
|
|||
|
|
|
||
|
|
import "strings"
|
||
|
|
|
||
|
|
// MappingSuggestion is a suggested source→target pair from schema extraction.
|
||
|
|
type MappingSuggestion struct {
|
||
|
|
Source string `json:"source"`
|
||
|
|
Target string `json:"target"`
|
||
|
|
Confidence string `json:"confidence"`
|
||
|
|
Score float64 `json:"score"`
|
||
|
|
}
|
||
|
|
|
||
|
|
var sourceAliases = map[string][]string{
|
||
|
|
"gtin": {"gtin"},
|
||
|
|
"ean": {"gtin"},
|
||
|
|
"upc": {"gtin"},
|
||
|
|
"barcode": {"gtin"},
|
||
|
|
"title": {"title"},
|
||
|
|
"name": {"title"},
|
||
|
|
"productname": {"title"},
|
||
|
|
"brand": {"brand"},
|
||
|
|
"manufacturer": {"brand"},
|
||
|
|
"description": {"description"},
|
||
|
|
"desc": {"description"},
|
||
|
|
"price": {"price"},
|
||
|
|
"regularprice": {"price"},
|
||
|
|
"saleprice": {"sale_price", "price"},
|
||
|
|
"currency": {"currency"},
|
||
|
|
"image": {"image_url", "main_image", "image"},
|
||
|
|
"imageurl": {"image_url", "main_image", "image"},
|
||
|
|
"imagelink": {"image_url", "main_image", "image"},
|
||
|
|
"mainimage": {"image_url", "main_image", "image"},
|
||
|
|
"mainimageurl": {"image_url", "main_image", "image"},
|
||
|
|
"link": {"product_url"},
|
||
|
|
"url": {"product_url"},
|
||
|
|
"producturl": {"product_url"},
|
||
|
|
"sku": {"sku"},
|
||
|
|
"mpn": {"mpn"},
|
||
|
|
"category": {"category"},
|
||
|
|
"availability": {"availability"},
|
||
|
|
"stockstatus": {"availability", "stock_status"},
|
||
|
|
"stock": {"stock"},
|
||
|
|
"quantity": {"stock"},
|
||
|
|
"qty": {"stock"},
|
||
|
|
"color": {"color"},
|
||
|
|
"size": {"size"},
|
||
|
|
"material": {"material"},
|
||
|
|
"weight": {"weight"},
|
||
|
|
"netmass": {"weight"},
|
||
|
|
"purchaseprice": {"purchase_price", "price"},
|
||
|
|
"buyprice": {"purchase_price"},
|
||
|
|
"cost": {"purchase_price"},
|
||
|
|
"costprice": {"purchase_price"},
|
||
|
|
"officiallink": {"official_link"},
|
||
|
|
"warranty": {"warranty"},
|
||
|
|
"garancija": {"warranty"},
|
||
|
|
"service": {"service"},
|
||
|
|
"servis": {"service"},
|
||
|
|
"productmodel": {"product_model"},
|
||
|
|
"model": {"product_model"},
|
||
|
|
"modelnumber": {"product_model"},
|
||
|
|
"eprelid": {"eprel_id"},
|
||
|
|
"eprel": {"eprel_id"},
|
||
|
|
"specifications": {"specs", "specifications"},
|
||
|
|
"specs": {"specs", "specifications"},
|
||
|
|
"specification": {"specs", "specifications"},
|
||
|
|
"techspecs": {"specs", "specifications"},
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeSuggestKey(raw string) string {
|
||
|
|
s := strings.ToLower(strings.TrimSpace(raw))
|
||
|
|
var b strings.Builder
|
||
|
|
for _, r := range s {
|
||
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||
|
|
b.WriteRune(r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return b.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
func leafSuggestKey(path string) string {
|
||
|
|
leaf := path
|
||
|
|
if i := strings.LastIndex(path, "/"); i >= 0 {
|
||
|
|
leaf = path[i+1:]
|
||
|
|
}
|
||
|
|
if i := strings.LastIndex(leaf, ":"); i >= 0 {
|
||
|
|
leaf = leaf[i+1:]
|
||
|
|
}
|
||
|
|
return normalizeSuggestKey(leaf)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SuggestTarget returns the best canonical target key for a single source name/path,
|
||
|
|
// or "" when no alias matches. Used during schema extract to annotate fields.
|
||
|
|
func SuggestTarget(source string) string {
|
||
|
|
keys := []string{normalizeSuggestKey(source), leafSuggestKey(source)}
|
||
|
|
for _, key := range keys {
|
||
|
|
if key == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if aliases, ok := sourceAliases[key]; ok && len(aliases) > 0 {
|
||
|
|
return aliases[0]
|
||
|
|
}
|
||
|
|
// Exact identity for known-looking keys already in alias map as targets.
|
||
|
|
for _, aliases := range sourceAliases {
|
||
|
|
for _, a := range aliases {
|
||
|
|
if a == key {
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
// SuggestMappings fuzzy-matches schema fields onto enabled target keys (1:1).
|
||
|
|
func SuggestMappings(schema []SchemaField, enabledTargets []string) []MappingSuggestion {
|
||
|
|
enabled := make(map[string]struct{}, len(enabledTargets))
|
||
|
|
for _, t := range enabledTargets {
|
||
|
|
t = strings.TrimSpace(t)
|
||
|
|
if t != "" {
|
||
|
|
enabled[t] = struct{}{}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(schema) == 0 || len(enabled) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
used := map[string]struct{}{}
|
||
|
|
type scored struct {
|
||
|
|
MappingSuggestion
|
||
|
|
order int
|
||
|
|
}
|
||
|
|
var candidates []scored
|
||
|
|
for i, f := range schema {
|
||
|
|
keys := []string{
|
||
|
|
normalizeSuggestKey(f.FieldName),
|
||
|
|
leafSuggestKey(f.Path),
|
||
|
|
normalizeSuggestKey(f.Path),
|
||
|
|
}
|
||
|
|
var hit *MappingSuggestion
|
||
|
|
for _, key := range keys {
|
||
|
|
if key == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, ok := enabled[key]; ok {
|
||
|
|
if _, taken := used[key]; !taken {
|
||
|
|
hit = &MappingSuggestion{Source: f.Path, Target: key, Confidence: "exact", Score: 1}
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if aliases, ok := sourceAliases[key]; ok {
|
||
|
|
for _, a := range aliases {
|
||
|
|
if _, ok := enabled[a]; !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, taken := used[a]; taken {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
hit = &MappingSuggestion{Source: f.Path, Target: a, Confidence: "alias", Score: 0.95}
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if hit != nil {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if hit != nil {
|
||
|
|
candidates = append(candidates, scored{MappingSuggestion: *hit, order: i})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Prefer higher score, then earlier schema order.
|
||
|
|
for i := 0; i < len(candidates); i++ {
|
||
|
|
for j := i + 1; j < len(candidates); j++ {
|
||
|
|
if candidates[j].Score > candidates[i].Score ||
|
||
|
|
(candidates[j].Score == candidates[i].Score && candidates[j].order < candidates[i].order) {
|
||
|
|
candidates[i], candidates[j] = candidates[j], candidates[i]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out := make([]MappingSuggestion, 0, len(candidates))
|
||
|
|
for _, c := range candidates {
|
||
|
|
if _, taken := used[c.Target]; taken {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
used[c.Target] = struct{}{}
|
||
|
|
out = append(out, c.MappingSuggestion)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|