MENU
Entrar
Cadastrar
Área do Representante
Departamentos
BICICLETAS MONTADAS
BICICLETA
BICICLETA ELETRICA
BRINQUEDO
BICICLETAS MONTADAS ELITE
BICICLETA ELITE
E-BIKE
FERRAMENTAS
FERRAMENTAS
LUBRIFICANTES
GRAXA
LUBRIFICANTE
MOTOPECAS
ACESSORIO MOTO
ARO DE RODA
BATERIAS
CAMARA MOTO
CAPA DE CHUVA
CAPACETE MOTO
CAPACETE SAN MARINO
CAPACETE TORK
GERAL
KIT MOTOR
KIT RELACAO
METAL
PATIM DE FREIO
PEÇAS MOTO
PLASTICO
PNEU MOTO
PECAS BIKE SHOP
PECAS BIKE ELITE
VESTUARIO
PECAS DE BICICLETA
KIT QD
PECAS BICICLETA
PECAS BIKE ELETRICA
PNEUS E CAMARAS
TINTA
TINTA
PROMOÇÃO
Baixe já nosso APP
0
Já é cliente? - Entrar
|
Não é cliente? - Cadastrar
|
Área do Representante
0
Fale Conosco
(63) 99201-2404
(63) 99201-2404
BICICLETAS MONTADAS
BICICLETA
BICICLETA ELETRICA
BRINQUEDO
BICICLETAS MONTADAS ELITE
BICICLETA ELITE
E-BIKE
FERRAMENTAS
FERRAMENTAS
LUBRIFICANTES
GRAXA
LUBRIFICANTE
MOTOPECAS
ACESSORIO MOTO
ARO DE RODA
BATERIAS
CAMARA MOTO
CAPA DE CHUVA
CAPACETE MOTO
CAPACETE SAN MARINO
CAPACETE TORK
GERAL
KIT MOTOR
KIT RELACAO
METAL
PATIM DE FREIO
PEÇAS MOTO
PLASTICO
PNEU MOTO
PECAS BIKE SHOP
PECAS BIKE ELITE
VESTUARIO
PECAS DE BICICLETA
KIT QD
PECAS BICICLETA
PECAS BIKE ELETRICA
PNEUS E CAMARAS
TINTA
TINTA
PROMOÇÃO
O nosso site coleta cookies próprios e de terceiros para proporcionar uma experiência personalizada ao usuário, apresentar publicidade relevante de acordo com o seu perfil e melhorar a qualidade do nosso site.
Aceitar
Negar não essenciais
Preferências de Cookies
Necessários
Analytics
Marketing
Personalização
Salvar preferências
Cadastre-se para receber nossas ofertas!
Meus Pedidos
Títulos
Notas Fiscais
/// Botão do Whatsapp
///Nome dos produtos ///grid de produtos /* * Busca Inteligente para catálogo de motopeças * ------------------------------------------------------------ * - Não depende de uma lista manual de produtos. * - Indexa automaticamente qualquer produto atual ou futuro. * - Entende abreviações, sinônimos, modelos, cilindradas, * posições, lados, medidas e intervalos de anos. * - Funciona no navegador e no Node.js. */ (function (root, factory) { const api = factory(); if (typeof module === "object" && module.exports) module.exports = api; else root.BuscaMotopecas = api; })(typeof globalThis !== "undefined" ? globalThis : this, function () { "use strict"; const STOP_WORDS = new Set([ "a", "as", "o", "os", "de", "da", "das", "do", "dos", "para", "por", "com", "sem", "em", "no", "na", "nos", "nas", "um", "uma", "e", "peca", "pecas", "produto", "produtos", "moto", "motocicleta" ]); // Regras ordenadas: expressões maiores devem vir antes das menores. const PHRASE_RULES = [ // Tipos de peça e formas usadas pelo cliente [["motor de partida", "motor partida", "motor part", "motor de arranque", "motor arranque", "arranque eletrico", "partida eletrica"], "motor_partida"], [["kit relacao", "kit de relacao", "kit rel", "relacao completa"], "kit_relacao"], [["cabo acelerador", "cabo de acelerador", "cabo acel"], "cabo_acelerador"], [["cabo embreagem", "cabo de embreagem", "cabo emb"], "cabo_embreagem"], [["cabo velocimetro", "cabo do velocimetro", "cabo vel"], "cabo_velocimetro"], [["interruptor partida", "interruptor de partida", "interr partida", "interr part"], "interruptor_partida"], [["interruptor freio", "interruptor de freio", "interr freio"], "interruptor_freio"], [["cilindro bengala", "cilin beng", "cilindro da bengala"], "cilindro_bengala"], [["retentor bengala", "ret bengala", "ret da bengala"], "retentor_bengala"], [["filtro de ar", "filtro ar"], "filtro_ar"], [["filtro de oleo", "filtro oleo"], "filtro_oleo"], [["bomba combustivel", "bomba de combustivel", "bomba comb"], "bomba_combustivel"], [["corrente comando", "corrente de comando"], "corrente_comando"], [["pastilha freio", "pastilha de freio"], "pastilha_freio"], [["disco freio", "disco de freio"], "disco_freio"], [["pedal partida", "pedal de partida"], "pedal_partida"], [["pedal freio", "pedal de freio"], "pedal_freio"], [["pedal cambio", "pedal de cambio"], "pedal_cambio"], [["bobina ignicao", "bobina de ignicao", "bobina ign"], "bobina_ignicao"], [["chave ignicao", "chave de ignicao", "chave ign"], "chave_ignicao"], [["vela ignicao", "vela de ignicao"], "vela_ignicao"], [["rele partida", "rele de partida"], "rele_partida"], [["rele pisca", "rele de pisca"], "rele_pisca"], [["kit motor", "kit de motor"], "kit_motor"], [["kit reparo", "kit de reparo", "kit rep"], "kit_reparo"], [["jogo juntas", "jogo de juntas", "jg juntas", "jg de juntas"], "jogo_juntas"], [["jogo direcao", "jogo de direcao", "jg direcao"], "jogo_direcao"], [["embreagem completa", "embreagem comp"], "embreagem_completa"], [["bloco optico", "bloco otico"], "bloco_optico"], [["carcaca painel", "carcaca do painel"], "carcaca_painel"], [["capa banco", "capa de banco"], "capa_banco"], [["oleo lubrificante", "lubrificante", "oleo de motor", "oleo motor", "oleo"], "oleo_lubrificante"], [["fluido freio", "fluido de freio"], "fluido_freio"], [["com camara", "c camara", "c cam", "com camera"], "com_camara"], [["sem camara", "s camara", "s cam", "sem camera"], "sem_camara"], [["modelo original", "mod original", "mod orig"], "modelo_original"], // Abreviações gerais usadas nos nomes do estoque [["parafuso", "paraf"], "parafuso"], [["retentor", "ret"], "retentor"], [["interruptor", "interr"], "interruptor"], [["carenagem", "caren"], "carenagem"], [["paralama", "paral"], "paralama"], [["protetor", "prot"], "protetor"], [["retrovisor", "retrov"], "retrovisor"], [["cilindro", "cilin"], "cilindro"], [["balancim", "balancin"], "balancim"], [["velocimetro", "veloc"], "velocimetro"], [["combustivel", "comb"], "combustivel"], [["embreagem", "embreag"], "embreagem"], [["tampa", "tpa"], "tampa"], [["jogo", "jg"], "jogo"], [["dentes", "dente", "d"], "dentes"], // Posições e lados [["dianteiro", "dianteira", "frente", "dt"], "dianteiro"], [["traseiro", "traseira", "tras", "tz"], "traseiro"], [["direito", "direita", "dir"], "direito"], [["esquerdo", "esquerda", "esq"], "esquerdo"], [["superior", "sup"], "superior"], [["inferior", "inf"], "inferior"], [["admissao", "adm"], "admissao"], [["escape", "escap"], "escape"], [["completo", "completa", "compl", "comp"], "completo"], // Cores frequentes [["preto", "preta", "pto"], "preto"], [["vermelho", "vermelha", "verm", "vemelho", "vemelha"], "vermelho"], [["azul", "az"], "azul"], [["prata", "prat"], "prata"], [["amarelo", "amarela", "amar"], "amarelo"], [["cristal"], "cristal"], [["fumê", "fume"], "fume"], // Modelos e abreviações encontradas no catálogo [["cg titan", "titan", "ti"], "titan"], [["cg today", "today", "to"], "today"], [["cg fan", "fan"], "fan"], [["nxr bros", "bros"], "bros"], [["c 100 biz", "c100 biz", "biz"], "biz"], [["pop"], "pop"], [["xre"], "xre"], [["xrl"], "xrl"], [["xlr"], "xlr"], [["xr"], "xr"], [["nx"], "nx"], [["cbx"], "cbx"], [["cb twister", "twister", "twist"], "twister"], [["tornado", "torn"], "tornado"], [["ybr"], "ybr"], [["factor"], "factor"], [["fazer"], "fazer"], [["lander", "land"], "lander"], [["tenere"], "tenere"], [["xtz"], "xtz"], [["crypton"], "crypton"], [["intruder"], "intruder"], [["yes"], "yes"], [["crf"], "crf"], [["falcon"], "falcon"], [["sahara"], "sahara"], [["cb"], "cb"], [["cg"], "cg"] ]; const KNOWN_MODELS = new Set([ "titan", "today", "fan", "bros", "biz", "pop", "xre", "xrl", "xlr", "xr", "nx", "cbx", "twister", "tornado", "ybr", "factor", "fazer", "lander", "tenere", "xtz", "crypton", "intruder", "yes", "crf", "falcon", "sahara", "cb", "cg" ]); const POSITION_TOKENS = new Set([ "dianteiro", "traseiro", "direito", "esquerdo", "superior", "inferior", "admissao", "escape" ]); const KNOWN_CC = new Set([ "50", "80", "90", "100", "105", "110", "115", "125", "135", "150", "160", "180", "190", "200", "230", "250", "300", "350", "400", "450", "500", "600", "650", "660", "700", "750", "800", "900", "1000", "1100" ]); const PRIMARY_TYPE_TOKENS = new Set([ "motor_partida", "kit_relacao", "cabo_acelerador", "cabo_embreagem", "cabo_velocimetro", "interruptor_partida", "interruptor_freio", "cilindro_bengala", "retentor_bengala", "filtro_ar", "filtro_oleo", "bomba_combustivel", "corrente_comando", "pastilha_freio", "disco_freio", "pedal_partida", "pedal_freio", "pedal_cambio", "bobina_ignicao", "chave_ignicao", "vela_ignicao", "rele_partida", "rele_pisca", "kit_motor", "kit_reparo", "jogo_juntas", "jogo_direcao", "embreagem_completa", "bloco_optico", "carcaca_painel", "capa_banco", "oleo_lubrificante", "fluido_freio", "pneu", "capacete", "cabo", "kit", "chave", "retentor", "eixo", "interruptor", "pisca", "pistao", "tampa", "capa", "rolamento", "lente", "estator", "chicote", "valvula", "painel", "mola", "manopla", "pedal", "carenagem", "cubo", "viseira", "biela", "filtro", "anel", "rabeta", "guia", "camara", "bloco", "trava", "carcaca", "lampada", "raio", "pastilha", "junta", "disco", "protetor", "alca", "aro", "bomba", "rele", "embreagem", "bico", "retrovisor", "corrente", "patim", "amortecedor", "bucha", "mesa", "carburador", "borracha", "macaneta", "escova", "porca", "suporte", "sensor", "vareta", "lanterna", "soquete", "farol", "refil", "motor", "arruela", "escapamento", "cavalete", "reparo", "cdi", "boia", "graxa", "desengripante", "descarbonizante", "parafuso", "cilindro", "balancim", "paralama", "carenagem", "combustivel" ]); function escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function stripAccents(text) { return String(text || "") .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); } function cleanBase(text) { return stripAccents(text) .toLowerCase() .replace(/^\s*[a-z]\s*-\s*/i, " ") .replace(/([a-z])([0-9])/g, "$1 $2") .replace(/([0-9])([a-z])/g, "$1 $2") .replace(/[º°]/g, " ") .replace(/\s+/g, " ") .trim(); } function isTwoDigitYear(value) { const n = Number(value); return Number.isInteger(n) && ((n >= 0 && n <= 30) || (n >= 70 && n <= 99)); } function fullYear(value) { const n = Number(value); return n <= 30 ? 2000 + n : 1900 + n; } function extractYearTokens(text) { const years = new Set(); const ranges = []; // Intervalos como 04/15, 92/02 e 16/24. Não confunde 90/90 de pneus. const rangeRegex = /(?:^|[^0-9])(\d{2})\s*[\/-]\s*(\d{2})(?![0-9])/g; let match; while ((match = rangeRegex.exec(text)) !== null) { const a = match[1]; const b = match[2]; if (!isTwoDigitYear(a) || !isTwoDigitYear(b)) continue; const start = fullYear(a); const end = fullYear(b); if (end < start || end - start > 35) continue; ranges.push([start, end]); for (let year = start; year <= end; year += 1) years.add(`ano_${year}`); } const fourDigitRangeRegex = /\b(19\d{2}|20\d{2})\s*[\/-]\s*(19\d{2}|20\d{2})\b/g; while ((match = fourDigitRangeRegex.exec(text)) !== null) { const start = Number(match[1]); const end = Number(match[2]); if (end >= start && end - start <= 35) { ranges.push([start, end]); for (let year = start; year <= end; year += 1) years.add(`ano_${year}`); } } const fourDigitRegex = /\b(19\d{2}|20\d{2})\b/g; while ((match = fourDigitRegex.exec(text)) !== null) years.add(`ano_${match[1]}`); return { years, ranges }; } function protectSpecialTokens(text) { let out = text; const protectedTokens = []; // Medidas de pneus: 100/80-17, 90/90x14, 2.75-18. out = out.replace(/\b(\d{2,3})\s*\/\s*(\d{2,3})\s*(?:[-x]|\s)\s*(\d{2})\b/g, (original, a, b, c) => { const rim = Number(c); // O terceiro número precisa ter formato de aro; evita confundir 125/150 02 com pneu. if (Number(a) < 50 || Number(b) < 50 || rim < 10 || rim > 23) return original; const token = `medida_${a}_${b}_${c}`; protectedTokens.push(token); return ` ${token} `; }); out = out.replace(/\b(\d(?:[.,]\d{1,2}))\s*[-x]\s*(\d{2})\b/g, (_, a, b) => { const token = `medida_${a.replace(",", ".").replace(".", "_")}_${b}`; protectedTokens.push(token); return ` ${token} `; }); // Viscosidade, classificação DOT e códigos técnicos. out = out.replace(/\b(\d{1,2})\s*w\s*(\d{2})\b/g, (_, a, b) => { const token = `visc_${a}w${b}`; protectedTokens.push(token); return ` ${token} `; }); out = out.replace(/\bdot\s*([3451])\b/g, (_, grade) => { const token = `dot_${grade}`; protectedTokens.push(token); return ` ${token} `; }); return { text: out, protectedTokens }; } function applyPhraseRules(text) { let out = ` ${text} `; for (const [aliases, canonical] of PHRASE_RULES) { const ordered = aliases.slice().sort((a, b) => b.length - a.length); for (const alias of ordered) { const pattern = escapeRegex(stripAccents(alias.toLowerCase())).replace(/\s+/g, "\\s+"); out = out.replace(new RegExp(`(^|[^a-z0-9_])${pattern}(?=$|[^a-z0-9_])`, "g"), `$1${canonical}`); } } return out.replace(/\s+/g, " ").trim(); } function singularize(token) { if (token.length > 5 && token.endsWith("oes")) return `${token.slice(0, -3)}ao`; if (token.length > 4 && token.endsWith("ais")) return `${token.slice(0, -3)}al`; if (token.length > 4 && token.endsWith("res")) return token.slice(0, -2); if (token.length > 4 && token.endsWith("s")) return token.slice(0, -1); return token; } function canonicalize(text) { const base = cleanBase(text); const yearData = extractYearTokens(base); const protectedData = protectSpecialTokens(base); const phrased = applyPhraseRules(protectedData.text) .replace(/[^a-z0-9_./+-]+/g, " ") .replace(/[./+\\-]+/g, " ") .replace(/\s+/g, " ") .trim(); const tokens = []; for (const rawToken of phrased.split(" ")) { if (!rawToken || STOP_WORDS.has(rawToken)) continue; const token = singularize(rawToken); if (!STOP_WORDS.has(token)) tokens.push(token); } // Mantém também as palavras internas dos termos compostos. // Ex.: cabo_acelerador também gera "cabo" e "acelerador". for (const token of tokens.slice()) { if (token.includes("_") && !token.startsWith("ano_") && !token.startsWith("medida_") && !token.startsWith("visc_") && !token.startsWith("dot_") && !token.startsWith("cc_")) { for (const part of token.split("_")) if (part) tokens.push(part); } } // Anos isolados abreviados, como 09, 15 ou 24, quando aparecem perto do modelo/cilindrada. for (let i = 0; i < tokens.length; i += 1) { const token = tokens[i]; if (!/^\d{2}$/.test(token) || !isTwoDigitYear(token)) continue; const previous = tokens[i - 1] || ""; const previous2 = tokens[i - 2] || ""; const likelyYear = token.startsWith("0") || KNOWN_MODELS.has(previous) || KNOWN_MODELS.has(previous2) || KNOWN_CC.has(previous); if (likelyYear) yearData.years.add(`ano_${fullYear(token)}`); } for (const year of yearData.years) tokens.push(year); for (const special of protectedData.protectedTokens) tokens.push(special); // Acrescenta fabricantes inferidos pelos modelos, ajudando buscas como "Honda Titan". const tokenSnapshot = new Set(tokens); if (["titan", "today", "fan", "bros", "biz", "pop", "xre", "xlr", "xr", "nx", "cbx", "twister", "tornado", "crf", "falcon", "sahara", "cb", "cg"].some(m => tokenSnapshot.has(m))) tokens.push("honda"); if (["ybr", "factor", "fazer", "lander", "tenere", "xtz", "crypton"].some(m => tokenSnapshot.has(m))) tokens.push("yamaha"); if (["intruder", "yes"].some(m => tokenSnapshot.has(m))) tokens.push("suzuki"); // Acrescenta cilindrada canônica sem remover o número original. for (const token of tokens.slice()) { if (KNOWN_CC.has(token)) tokens.push(`cc_${token}`); } const uniqueTokens = Array.from(new Set(tokens)); return { base, normalized: uniqueTokens.join(" "), tokens: uniqueTokens, tokenSet: new Set(uniqueTokens), years: new Set(uniqueTokens.filter(t => t.startsWith("ano_"))), models: new Set(uniqueTokens.filter(t => KNOWN_MODELS.has(t))), cc: new Set(uniqueTokens.filter(t => t.startsWith("cc_"))), positions: new Set(uniqueTokens.filter(t => POSITION_TOKENS.has(t))), specs: new Set(uniqueTokens.filter(t => t.startsWith("medida_") || t.startsWith("visc_") || t.startsWith("dot_"))) }; } function getPrimaryType(tokens) { for (const token of tokens.slice(0, 4)) { if (PRIMARY_TYPE_TOKENS.has(token)) return token; if (KNOWN_MODELS.has(token) || token.startsWith("ano_") || token.startsWith("cc_")) break; } return tokens[0] || ""; } function levenshtein(a, b) { if (a === b) return 0; if (!a.length) return b.length; if (!b.length) return a.length; const previous = Array.from({ length: b.length + 1 }, (_, i) => i); const current = new Array(b.length + 1); for (let i = 1; i <= a.length; i += 1) { current[0] = i; for (let j = 1; j <= b.length; j += 1) { current[j] = Math.min( current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1) ); } for (let j = 0; j <= b.length; j += 1) previous[j] = current[j]; } return previous[b.length]; } function fuzzyTokenMatch(queryToken, productTokens) { if (queryToken.length < 4 || queryToken.includes("_")) return false; for (const productToken of productTokens) { if (productToken.includes("_") || productToken.length < 4) continue; if (productToken.startsWith(queryToken) || queryToken.startsWith(productToken)) return true; const maxDistance = Math.max(queryToken.length, productToken.length) >= 8 ? 2 : 1; if (Math.abs(queryToken.length - productToken.length) <= maxDistance && levenshtein(queryToken, productToken) <= maxDistance) return true; } return false; } function indexProduct(product, options) { const nameField = options.nameField || "nome"; const extraFields = options.extraFields || ["marca", "categoria", "descricao", "codigo", "sku"]; const name = String(product[nameField] || ""); const searchableText = [name] .concat(extraFields.map(field => product[field]).filter(value => value !== undefined && value !== null)) .join(" "); const parsed = canonicalize(searchableText); const nameParsed = canonicalize(name); return { product, name, parsed, nameParsed, primaryType: getPrimaryType(nameParsed.tokens), normalizedName: nameParsed.normalized }; } function createIndex(products, options = {}) { if (!Array.isArray(products)) throw new TypeError("products deve ser um array."); return products .filter(product => product && product[options.nameField || "nome"]) .map(product => indexProduct(product, options)); } function setMatches(querySet, productSet, requireAll = true) { if (!querySet.size) return true; const values = Array.from(querySet); return requireAll ? values.every(value => productSet.has(value)) : values.some(value => productSet.has(value)); } function scoreProduct(query, indexedProduct) { const product = indexedProduct.parsed; const productName = indexedProduct.nameParsed; const queryPrimary = getPrimaryType(query.tokens); // Quando o cliente informa claramente o tipo da peça, o produto precisa conter esse tipo. if (queryPrimary && PRIMARY_TYPE_TOKENS.has(queryPrimary) && !product.tokenSet.has(queryPrimary)) return null; // Filtros fortes evitam resultados tecnicamente incompatíveis. if (!setMatches(query.years, product.years, true)) return null; if (!setMatches(query.models, product.models, true)) return null; if (!setMatches(query.cc, product.cc, true)) return null; if (!setMatches(query.positions, product.positions, true)) return null; if (!setMatches(query.specs, product.specs, true)) return null; let score = 0; let exactMatches = 0; let fuzzyMatches = 0; if (queryPrimary && queryPrimary === indexedProduct.primaryType) score += 350; else if (queryPrimary && PRIMARY_TYPE_TOKENS.has(queryPrimary)) score -= 120; if (productName.normalized.includes(query.normalized)) score += 180; if (product.normalized.includes(query.normalized)) score += 90; for (const token of query.tokens) { if (product.tokenSet.has(token)) { exactMatches += 1; if (token.startsWith("ano_")) score += 95; else if (token.startsWith("cc_")) score += 75; else if (KNOWN_MODELS.has(token)) score += 90; else if (POSITION_TOKENS.has(token)) score += 55; else if (token.startsWith("medida_") || token.startsWith("visc_") || token.startsWith("dot_")) score += 100; else if (token.includes("_")) score += 70; else score += 22; } else if (fuzzyTokenMatch(token, product.tokens)) { fuzzyMatches += 1; score += 8; } } const coverage = (exactMatches + 0.45 * fuzzyMatches) / Math.max(1, query.tokens.length); score += Math.round(coverage * 140); if (coverage < 0.45) return null; // Produtos cujo nome começa pelo tipo pesquisado recebem preferência. if (queryPrimary && productName.tokens[0] === queryPrimary) score += 120; // Penaliza acessórios quando o cliente procura a peça principal. if (queryPrimary === "motor_partida" && indexedProduct.primaryType !== "motor_partida") score -= 220; if (queryPrimary === "kit_relacao" && indexedProduct.primaryType !== "kit_relacao") score -= 180; return { score, coverage, exactMatches, fuzzyMatches }; } function search(queryText, index, options = {}) { const limit = Number.isFinite(options.limit) ? options.limit : 20; const minScore = Number.isFinite(options.minScore) ? options.minScore : 120; const query = canonicalize(queryText); if (!query.tokens.length) return []; return index .map(item => { const result = scoreProduct(query, item); if (!result || result.score < minScore) return null; return { ...item.product, _busca: { score: result.score, cobertura: Number(result.coverage.toFixed(3)), nomeNormalizado: item.normalizedName, tipoPrincipal: item.primaryType } }; }) .filter(Boolean) .sort((a, b) => b._busca.score - a._busca.score) .slice(0, limit); } function generateSearchKeywords(product, options = {}) { const indexed = indexProduct(product, options); const readable = indexed.parsed.tokens.map(token => { if (token.startsWith("ano_")) return token.slice(4); if (token.startsWith("cc_")) return `${token.slice(3)}cc`; if (token.startsWith("visc_")) return token.slice(5); if (token.startsWith("dot_")) return token.replace("_", ""); if (token.startsWith("medida_")) return token.slice(7).replace(/_/g, " "); return token.replace(/_/g, " "); }); return Array.from(new Set(readable)).join(", "); } function updateIndex(index, product, options = {}) { const idField = options.idField || "codigo"; const id = product[idField]; const position = index.findIndex(item => item.product[idField] === id); const newItem = indexProduct(product, options); if (position >= 0) index[position] = newItem; else index.push(newItem); return index; } return { canonicalize, createIndex, search, generateSearchKeywords, updateIndex }; });