Busca semântica
Do texto bruto ao resultado ordenado: dividir em trechos, indexar, consultar e saber quando o resultado não presta.
Atualizado em 10 de ago. de 2026
Busca por palavra-chave encontra o que foi escrito. Busca semântica encontra o que foi dito — ainda que com outras palavras. Esta página monta uma, do texto bruto ao resultado ordenado, com os números que a API devolveu de verdade.
Uma base de respostas de suporte. O usuário escreve "quero meu dinheiro de volta" — uma frase que não tem nenhuma palavra em comum com a resposta certa. Um LIKE no banco devolve zero linhas. A busca por significado devolve:
| Score | Trecho da base |
|---|---|
| **0,5486** | Cancelamentos podem ser feitos em até 7 dias após a compra, com reembolso integral. |
| 0,5054 | Emitimos nota fiscal eletrônica automaticamente após a confirmação do pagamento. |
| 0,4854 | Aceitamos cartão de crédito, Pix e boleto bancário. |
| 0,4199 | O prazo de entrega padrão é de 3 a 5 dias úteis para todo o Brasil. |
"Dinheiro de volta" e "reembolso integral" não se parecem como texto; se parecem como assunto. É todo o ponto.
- 1
Divida em trechos
Cada trecho precisa fazer sentido sozinho — é ele que vai ser devolvido, não o documento inteiro.
- 2
Gere os embeddings uma vez
Em lote, e guarde os vetores junto do texto e da origem. Só o que mudar precisa ser refeito.
- 3
Na consulta, gere o embedding da pergunta
Mesmo modelo, mesma dimensão. Uma chamada, alguns milésimos de centavo.
- 4
Ordene por similaridade
Produto escalar contra cada vetor guardado, do maior para o menor.
- 5
Corte no que não presta
Devolver os três primeiros sempre é errado: às vezes a resposta não está na base.
A base tem oito respostas de suporte. O código gera os vetores de todas em uma chamada, gera o da pergunta em outra, e ordena:
import os, requests
CHAVE = os.environ["HINOW_API_KEY"]
MODELO = "hinow/hiembed"
BASE = [
"Para trocar a senha, abra Configurações › Segurança e clique em Redefinir senha.",
"O prazo de entrega padrão é de 3 a 5 dias úteis para todo o Brasil.",
"Aceitamos cartão de crédito, Pix e boleto bancário.",
"Cancelamentos podem ser feitos em até 7 dias após a compra, com reembolso integral.",
"O aplicativo está disponível para Android e iOS.",
"Emitimos nota fiscal eletrônica automaticamente após a confirmação do pagamento.",
"Nosso suporte atende de segunda a sexta, das 9h às 18h.",
"Para acompanhar o pedido, use o código de rastreio enviado por e-mail.",
]
def embeddings(textos):
resposta = requests.post(
"https://api.hinow.ai/v1/embeddings",
headers={"Authorization": f"Bearer {CHAVE}"},
json={"model": MODELO, "input": textos},
)
resposta.raise_for_status()
itens = sorted(resposta.json()["data"], key=lambda i: i["index"])
return [item["embedding"] for item in itens]
# Os vetores já vêm normalizados: o produto escalar é a similaridade de cosseno.
def similaridade(a, b):
return sum(x * y for x, y in zip(a, b))
indice = list(zip(BASE, embeddings(BASE))) # gere uma vez, guarde
pergunta = embeddings(["quero meu dinheiro de volta"])[0]
ranking = sorted(
((similaridade(pergunta, vetor), texto) for texto, vetor in indice),
reverse=True,
)
for score, texto in ranking[:3]:
print(f"{score:.4f} {texto}")const CHAVE = process.env.HINOW_API_KEY!;
const MODELO = "hinow/hiembed";
const BASE = [
"Para trocar a senha, abra Configurações › Segurança e clique em Redefinir senha.",
"O prazo de entrega padrão é de 3 a 5 dias úteis para todo o Brasil.",
"Aceitamos cartão de crédito, Pix e boleto bancário.",
"Cancelamentos podem ser feitos em até 7 dias após a compra, com reembolso integral.",
"O aplicativo está disponível para Android e iOS.",
"Emitimos nota fiscal eletrônica automaticamente após a confirmação do pagamento.",
"Nosso suporte atende de segunda a sexta, das 9h às 18h.",
"Para acompanhar o pedido, use o código de rastreio enviado por e-mail.",
];
async function embeddings(textos: string[]): Promise<number[][]> {
const resposta = await fetch("https://api.hinow.ai/v1/embeddings", {
method: "POST",
headers: {
Authorization: `Bearer ${CHAVE}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: MODELO, input: textos }),
});
if (!resposta.ok) throw new Error(await resposta.text());
const { data } = await resposta.json();
return [...data].sort((a, b) => a.index - b.index).map((item) => item.embedding);
}
// Os vetores já vêm normalizados: o produto escalar é a similaridade de cosseno.
const similaridade = (a: number[], b: number[]) =>
a.reduce((soma, valor, i) => soma + valor * b[i], 0);
const vetores = await embeddings(BASE); // gere uma vez, guarde
const [pergunta] = await embeddings(["quero meu dinheiro de volta"]);
const ranking = BASE.map((texto, i) => ({ texto, score: similaridade(pergunta, vetores[i]) }))
.sort((a, b) => b.score - a.score);
for (const { score, texto } of ranking.slice(0, 3)) {
console.log(score.toFixed(4), texto);
}package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"sort"
)
const modelo = "hinow/hiembed"
var base = []string{
"Para trocar a senha, abra Configurações › Segurança e clique em Redefinir senha.",
"O prazo de entrega padrão é de 3 a 5 dias úteis para todo o Brasil.",
"Aceitamos cartão de crédito, Pix e boleto bancário.",
"Cancelamentos podem ser feitos em até 7 dias após a compra, com reembolso integral.",
"O aplicativo está disponível para Android e iOS.",
"Emitimos nota fiscal eletrônica automaticamente após a confirmação do pagamento.",
"Nosso suporte atende de segunda a sexta, das 9h às 18h.",
"Para acompanhar o pedido, use o código de rastreio enviado por e-mail.",
}
func embeddings(textos []string) ([][]float64, error) {
corpo, _ := json.Marshal(map[string]any{"model": modelo, "input": textos})
req, _ := http.NewRequest("POST", "https://api.hinow.ai/v1/embeddings", bytes.NewReader(corpo))
req.Header.Set("Authorization", "Bearer "+os.Getenv("HINOW_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var r struct {
Data []struct {
Embedding []float64 `json:"embedding"`
Index int `json:"index"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
return nil, err
}
vetores := make([][]float64, len(r.Data))
for _, item := range r.Data {
vetores[item.Index] = item.Embedding
}
return vetores, nil
}
// Os vetores já vêm normalizados: o produto escalar é a similaridade de cosseno.
func similaridade(a, b []float64) float64 {
var soma float64
for i := range a {
soma += a[i] * b[i]
}
return soma
}
func main() {
vetores, err := embeddings(base) // gere uma vez, guarde
if err != nil {
panic(err)
}
pergunta, err := embeddings([]string{"quero meu dinheiro de volta"})
if err != nil {
panic(err)
}
type achado struct {
score float64
texto string
}
ranking := make([]achado, len(base))
for i, texto := range base {
ranking[i] = achado{similaridade(pergunta[0], vetores[i]), texto}
}
sort.Slice(ranking, func(i, j int) bool { return ranking[i].score > ranking[j].score })
for _, r := range ranking[:3] {
fmt.Printf("%.4f %s\n", r.score, r.texto)
}
}A saída, com os números que a API devolveu:
0.5486 Cancelamentos podem ser feitos em até 7 dias após a compra, com reembolso integral.
0.5054 Emitimos nota fiscal eletrônica automaticamente após a confirmação do pagamento.
0.4854 Aceitamos cartão de crédito, Pix e boleto bancário.Duas chamadas à API e vinte linhas de código. Para oito trechos, um laço em memória resolve; a partir de algumas dezenas de milhares, é hora de uma base vetorial — a lógica continua exatamente esta.
| Consulta | Primeiro resultado | Score |
|---|---|---|
| "esqueci como entrar na minha conta" | Para trocar a senha, abra Configurações › Segurança… | 0,6155 |
| "posso pagar com pix?" | Aceitamos cartão de crédito, Pix e boleto bancário. | 0,7096 |
| "quando chega minha encomenda?" | O prazo de entrega padrão é de 3 a 5 dias úteis… | 0,6718 |
| "I forgot my password" | Para trocar a senha, abra Configurações › Segurança… | 0,5752 |
A última linha não é um detalhe: a pergunta está em inglês, a base inteira em português, e o trecho certo veio em primeiro lugar. O mesmo índice atende usuários de idiomas diferentes sem uma tradução no meio do caminho.
O número não é probabilidade nem porcentagem de acerto — é a distância entre dois sentidos, nessa base, com esse modelo. As faixas medidas aqui servem de referência inicial:
| Faixa | O que costuma ser |
|---|---|
| acima de 0,90 | Paráfrase ou duplicata — o mesmo conteúdo escrito de outro jeito |
| 0,55 a 0,75 | Relacionado de verdade: é o resultado que o usuário queria |
| 0,45 a 0,55 | Mesmo domínio, outra pergunta — costuma decepcionar |
| abaixo de 0,45 | Sem relação |
Calibre o corte com a sua base
Esses limites mudam com o tamanho dos trechos, o assunto e o modelo — e não podem ser comparados entre modelos diferentes. Rode trinta perguntas reais, olhe o score do resultado certo e o do primeiro errado, e corte no meio. Sem um corte, a busca sempre devolve alguma coisa: inclusive quando a resposta não existe na base.
É a decisão que mais muda a qualidade da busca, e ela acontece antes de qualquer chamada à API.
- Um trecho, uma ideia. O trecho é o que volta para o usuário — ou o que entra no contexto do modelo. Um capítulo inteiro dilui o assunto e derruba o score; uma frase solta perde o contexto que dava sentido a ela.
- De 200 a 500 tokens costuma ser o ponto de equilíbrio para documentação e base de conhecimento. O teto técnico é bem mais alto — cerca de 8.192 tokens — mas encostar nele piora o resultado.
- Corte por estrutura, não por contagem. Título, seção, parágrafo. Quebrar no meio de uma frase produz um vetor sobre nada.
- Repita um pouco do vizinho (uma ou duas frases de sobreposição) quando o texto é corrido, para não perder a ideia que atravessa a fronteira.
- Leve o contexto junto. Guardar "Política de reembolso › Prazos" na frente do trecho ajuda o vetor e ajuda quem lê o resultado.
Comece simples e suba conforme o volume:
- Até alguns milhares de trechos — uma lista em memória e o laço acima. É o suficiente para a maioria dos produtos no primeiro ano, e é fácil de depurar.
- Dezenas de milhares para cima — uma base vetorial, com índice aproximado. A conta de similaridade continua a mesma; o que muda é não percorrer tudo a cada consulta.
- Sem querer manter nada disso — a HINOW já oferece o caminho pronto: as bases de conhecimento fazem ingestão, divisão em trechos, indexação e busca, e você chama uma API só.
Guarde sempre, junto do vetor: o texto de origem, a referência do documento, o id do modelo e a dimensão. Sem o modelo e a dimensão gravados, o dia da troca de modelo vira uma arqueologia.
- Filtro exato — status, categoria, CPF, número de pedido. Isso é
WHERE, e o banco faz melhor, mais barato e sem erro. - Número, data e faixa — "pedidos acima de R$ 500 em março" não é assunto, é condição.
- Identificador e código — SKU, versão, nome de arquivo. O vetor acha "parecido", e parecido aqui é errado.
- Negação — "planos que não incluem suporte" fica quase idêntico a "planos que incluem suporte".
Na prática os dois convivem: filtro exato para restringir o conjunto, busca semântica para ordenar o que sobrou. E quando o usuário digita um termo que existe literalmente no texto, a busca por palavra ainda é imbatível — juntar as duas listas costuma render mais que escolher uma.
Monte um conjunto de trinta a cinquenta perguntas reais — das que chegam no suporte, não das que você imaginou — e anote qual trecho deveria vir em primeiro lugar. Meça duas coisas: com que frequência o trecho certo aparece no topo, e com que frequência aparece entre os cinco primeiros.
É esse número que diz se mexer no tamanho do trecho, no corte ou no modelo melhorou alguma coisa. Sem ele, toda mudança parece boa.

