Skip to content

Semantic search

From raw text to a ranked result: split into passages, index, query, and know when the result is no good.

Updated on Aug 10, 2026

Keyword search finds what was written. Semantic search finds what was meant — even in other words. This page builds one, from raw text to a ranked result, with the numbers the API actually returned.

The problem, in one query

A base of support answers, written in Portuguese. The user types "quero meu dinheiro de volta"I want my money back — a sentence with not a single word in common with the right answer. A LIKE in the database returns zero rows. Search by meaning returns:

ScorePassage from the base
**0.5486**Cancelamentos podem ser feitos em até 7 dias após a compra, com reembolso integral.
0.5054Emitimos nota fiscal eletrônica automaticamente após a confirmação do pagamento.
0.4854Aceitamos cartão de crédito, Pix e boleto bancário.
0.4199O prazo de entrega padrão é de 3 a 5 dias úteis para todo o Brasil.

"Dinheiro de volta" and "reembolso integral" — money back and full refund — do not look alike as text; they look alike as subject matter. That is the whole point.

The flow

  1. 1

    Split into passages

    Each passage has to make sense on its own — it is the passage that gets returned, not the whole document.

  2. 2

    Generate the embeddings once

    In a batch, and store the vectors alongside the text and its source. Only what changes has to be redone.

  3. 3

    At query time, embed the question

    Same model, same dimension. One call, a few thousandths of a cent.

  4. 4

    Rank by similarity

    Dot product against every stored vector, from highest to lowest.

  5. 5

    Cut off what is no good

    Always returning the top three is wrong: sometimes the answer is not in the base.

A runnable example

The base holds eight support answers. The code embeds all of them in one call, embeds the question in another, and ranks:

busca.pypython
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}")

The output, with the numbers the API returned:

saída
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.

Two API calls and twenty lines of code. For eight passages, an in-memory loop does the job; from a few tens of thousands upward, it is time for a vector database — the logic stays exactly this one.

Other queries, same base

QueryFirst resultScore
"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

The last row is not a detail: the question is in English, the whole base is in Portuguese, and the right passage came first. The same index serves users in different languages with no translation step in between.

Reading the score

The number is neither a probability nor a percentage of correctness — it is the distance between two meanings, in this base, with this model. The bands measured here work as a starting reference:

BandWhat it usually is
above 0.90Paraphrase or duplicate — the same content written another way
0.55 to 0.75Genuinely related: the result the user wanted
0.45 to 0.55Same domain, different question — usually disappointing
below 0.45Unrelated

Calibrate the cut-off with your own base

These thresholds move with the size of the passages, the subject, and the model — and they cannot be compared across different models. Run thirty real questions, look at the score of the right result and of the first wrong one, and cut in between. Without a cut-off, the search always returns something: including when the answer does not exist in the base.

Splitting the text into passages

This is the decision that changes search quality the most, and it happens before any API call.

  • One passage, one idea. The passage is what goes back to the user — or what enters the model's context. A whole chapter dilutes the subject and drags the score down; a lone sentence loses the context that gave it meaning.
  • From 200 to 500 tokens is usually the balance point for documentation and knowledge bases. The technical ceiling is much higher — around 8,192 tokens — but getting close to it makes the result worse.
  • Cut by structure, not by count. Title, section, paragraph. Breaking mid-sentence produces a vector about nothing.
  • Repeat a little of the neighbour (one or two sentences of overlap) when the text runs continuously, so you do not lose the idea that crosses the boundary.
  • Carry the context along. Storing "Refund policy › Deadlines" in front of the passage helps the vector and helps whoever reads the result.

Where to store the vectors

Start simple and scale with the volume:

  • Up to a few thousand passages — a list in memory and the loop above. That is enough for most products in their first year, and it is easy to debug.
  • Tens of thousands and up — a vector database, with an approximate index. The similarity math stays the same; what changes is not scanning everything on every query.
  • Not wanting to maintain any of it — HINOW already offers the ready-made path: knowledge bases handle ingestion, splitting, indexing, and search, and you call a single API.

Always store, alongside the vector: the source text, the document reference, the model id, and the dimension. Without the model and dimension recorded, the day you switch models turns into archaeology.

When semantic search is the wrong tool

  • Exact filter — status, category, tax id, order number. That is WHERE, and the database does it better, cheaper, and without error.
  • Number, date, and range — "orders above R$ 500 in March" is not a subject, it is a condition.
  • Identifier and code — SKU, version, file name. The vector finds "similar", and similar is wrong here.
  • Negation — "plans that do not include support" comes out almost identical to "plans that include support".

In practice the two coexist: an exact filter to narrow the set, semantic search to rank what is left. And when the user types a term that exists literally in the text, word search is still unbeatable — merging the two lists usually pays off more than choosing one.

Evaluate before you trust it

Put together a set of thirty to fifty real questions — the ones that arrive at support, not the ones you imagined — and note which passage should come first. Measure two things: how often the right passage lands at the top, and how often it lands in the top five.

That number is what tells you whether changing the passage size, the cut-off, or the model improved anything. Without it, every change looks good.

Next steps