Você digita “sapato para maratona”. A loja tem tênis de corrida na prateleira, mas responde: nenhum resultado encontrado. O problema não é falta de produto. É que a busca só sabe comparar palavras.
Nesta matéria, vamos construir um buscador semântico mínimo. Ele transforma as descrições de um catálogo e a consulta do cliente em embeddings, compara os vetores por similaridade de cosseno e mostra os resultados no terminal. Ao lado dele, teremos uma busca por palavra exata. O resultado final será um programa executável, com os dois métodos competindo pelas mesmas consultas.
A busca exata enxerga letras, não intenção
Uma busca lexical procura termos iguais ou relacionados por regras explícitas. Se o catálogo diz “tênis de corrida para longas distâncias” e a consulta diz “sapato para maratona”, talvez nenhuma palavra coincida. O sistema não conclui que os objetos pertencem ao mesmo contexto.
Embeddings oferecem outra representação. Um texto vira uma lista de números que posiciona seu significado em um espaço vetorial. Frases semanticamente próximas ficam próximas nesse espaço, mesmo quando não compartilham palavras.
Isso não é compreensão humana. É uma geometria aprendida a partir de muitos exemplos de linguagem. O mecanismo captura associações úteis, mas não aplica automaticamente regras de negócio, como preço máximo, disponibilidade ou “não quero produto usado”.
Primeiro, faça o catálogo virar vetor

Crie uma pasta vazia e salve o arquivo abaixo como search.mjs. O catálogo é pequeno de propósito. Em vez de começar com banco de dados ou índice especializado, vamos guardar tudo em memória e comparar a consulta contra cada item. Para poucos produtos, essa escolha reduz partes móveis e deixa o mecanismo visível.
const catalog = [
{ id: "run-01", text: "Tênis leve para corrida de longa distância, com amortecimento e boa respirabilidade." },
{ id: "run-02", text: "Tênis para trilha com solado aderente e resistência à água." },
{ id: "gym-01", text: "Camiseta esportiva de secagem rápida para treino intenso." },
{ id: "winter-01", text: "Jaqueta térmica para exercícios ao ar livre em dias frios." },
{ id: "code-01", text: "Teclado mecânico compacto com conexão USB e iluminação." }
];
async function createEmbeddings(input) {
const response = await fetch("https://api.hinow.ai/v1/embeddings", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.HINOW_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "hinow/hiembed",
input
})
});
if (!response.ok) throw new Error(await response.text());
const data = await response.json();
return data.data.map(item => item.embedding);
}
function cosineSimilarity(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i += 1) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function main() {
if (!process.env.HINOW_API_KEY) {
throw new Error("Set HINOW_API_KEY before running the program.");
}
const catalogVectors = await createEmbeddings(catalog.map(item => item.text));
const query = process.argv.slice(2).join(" ") || "sapato para maratona";
const [queryVector] = await createEmbeddings([query]);
const results = catalog.map((item, index) => ({
...item,
score: cosineSimilarity(queryVector, catalogVectors[index])
}));
results.sort((a, b) => b.score - a.score);
console.log(`\nQuery: ${query}`);
console.log("\nSemantic search:");
for (const result of results.slice(0, 3)) {
console.log(`${result.score.toFixed(4)} ${result.id} ${result.text}`);
}
}
main().catch(error => {
console.error(error.message);
process.exit(1);
});
A rota de embeddings aceita uma lista no campo input. Por isso, o programa envia o catálogo inteiro em uma chamada e a consulta em outra. A resposta contém vetores em data[i].embedding, com exatamente 1024 dimensões no formato verificado em agosto de 2026. O código não precisa saber o significado de cada posição. Só precisa comparar vetores do mesmo espaço.
A similaridade de cosseno mede o ângulo entre eles. O produto escalar fica no numerador; as normas dos dois vetores ficam no denominador. Assim, o tamanho absoluto perde importância e a direção semântica vira o sinal principal.
Rode e veja a diferença
No terminal, configure a chave e execute:
export HINOW_API_KEY="sua-chave"
node search.mjs "sapato para maratona"
Esperamos que o primeiro resultado semântico seja o tênis para corrida de longa distância, mesmo sem repetir “sapato” ou “maratona”. Isso é uma hipótese de teste, não uma garantia. Execute o programa e confira o ranking real antes de tirar qualquer conclusão.
Agora falta o adversário lexical. Acrescente estas funções ao arquivo:
function lexicalScore(query, text) {
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
const normalizedText = text.toLowerCase();
return terms.filter(term => normalizedText.includes(term)).length;
}
function lexicalSearch(query) {
return catalog
.map(item => ({ ...item, score: lexicalScore(query, item.text) }))
.sort((a, b) => b.score - a.score);
}
Chame lexicalSearch(query) dentro de main e imprima seus três primeiros itens. Para a consulta sobre maratona, a busca lexical pode pontuar zero para todos. Para uma consulta como “tênis corrida”, ela ganha clareza e velocidade, porque encontra exatamente os termos que o usuário forneceu.
Teste quatro consultas: “sapato para maratona”, “roupa para treino no frio”, “tênis para trilha” e “run-01”. Compare os resultados dos dois métodos. A hipótese é que a busca semântica encontre relações que a correspondência literal pode perder nas três primeiras consultas, enquanto a busca exata leve vantagem na consulta pelo código. A execução real precisa confirmar ou refutar essa hipótese.
Não deixe o vetor decidir tudo
O buscador pronto não deve escolher entre lexical e semântico como se um deles fosse universalmente melhor. Combine as duas pontuações.
A busca lexical protege códigos, números, nomes exatos e filtros. A semântica amplia a descoberta quando o cliente descreve uma necessidade com outras palavras. Um sistema híbrido pode recuperar candidatos pelos dois caminhos, remover itens indisponíveis com filtros estruturados e então ordenar os resultados com uma regra explícita.
Há um limite importante: embeddings não entendem negação nem filtros exatos de forma confiável. “Barato, até 200 reais” precisa virar uma condição de preço, não uma esperança depositada na similaridade. O texto do catálogo também importa. Um produto descrito apenas como “modelo M azul” oferece pouco material para qualquer representação semântica.
Como saber se melhorou
Não avalie o buscador olhando apenas para uma consulta que confirma sua tese. Monte um arquivo pequeno com consultas e resultados esperados. Inclua sinônimos, linguagem informal, códigos, erros de digitação e filtros.
Uma métrica simples é a taxa de acerto no topo: conte quantas consultas colocam um resultado aceitável entre os três primeiros e divida pelo total de consultas. Se 8 de 10 consultas acertarem, a taxa será 80%. Esse número é uma medição do seu catálogo e das suas consultas, não uma propriedade universal de embeddings.
Observe também os erros. Se “jaqueta para treino no frio” retorna camiseta, talvez o catálogo não registre o atributo térmico. Se “não quero trilha” ainda retorna tênis de trilha, o filtro precisa ser tratado fora do vetor.
O custo aparece antes do código ficar grande
O serviço de embeddings trabalha com uma janela limitada por entrada. Quando um texto ultrapassa o limite configurado, divida-o antes da vetorização. Para o catálogo pequeno, comparar todos os vetores no próprio processo é suficiente.
Suponha, apenas como exemplo hipotético, um catálogo com 10 mil descrições, cada uma com 50 tokens. A conta seria 10000 × 50, ou 500 mil tokens para gerar os vetores uma vez. O custo exato depende do preço por token vigente, então use a tarifa da sua conta para calcular: 500000 × preço_por_token. A ordem de grandeza pode ser baixa para a indexação inicial, mas cada alteração de descrição exige uma nova vetorização.
Na consulta, há uma chamada para transformar o texto do cliente em vetor. Depois, o programa calcula a similaridade localmente. Em um catálogo pequeno, essa etapa não exige uma chamada por produto. Como exercício de planejamento, suponha que cada uma das duas chamadas leve 300 ms. Nesse cenário hipotético, a rede consumiria cerca de 600 ms antes da comparação local. Isso não é uma medição do programa: meça no seu ambiente antes de definir uma meta de latência.
Quando o catálogo crescer, guarde os vetores em uma base vetorial e use um índice de vizinhos aproximados. A ideia permanece igual, mas a busca deixa de comparar todos os itens. Recalcule o embedding sempre que o texto usado para representar o produto mudar.
A próxima melhoria útil não é trocar a fórmula. É criar um conjunto de avaliação com consultas reais, combinar filtro exato com significado e enriquecer as descrições que ainda não dizem o que torna cada item relevante. Um buscador semântico começa como um vetor, mas vira produto quando seus erros passam a ser medidos.




