Embeddings Vetoriais e GraphRAG

O que a busca semântica realmente acrescenta à recuperação de conhecimento — medido, não estimado

Em resumo, antes de qualquer número

O quê. Uma segunda forma de procurar conceitos no seu grafo: por significado, não por palavra exata.

Por quê. Procurar só por palavra-chave falha quando você não usa o termo exato que está na sua ontologia — e você quase nunca usa, porque ninguém decora o próprio vocabulário controlado.

O que muda. Com o recurso ligado, o conceito certo chega até o assistente em cerca de 9 de cada 10 perguntas. Sem ele, em pouco menos de 6 de cada 10.

Quanto custa. Cerca de cinquenta milésimos de segundo por pergunta — imperceptível numa conversa. Roda no seu computador, sem internet e sem API paga.

Quando ligar. Se você conversa com um assistente (Claude Desktop via MCP) sobre a sua pesquisa, usando suas próprias palavras. Se você só faz consultas estruturadas em que já sabe o nome exato do conceito, não precisa.

Este documento explica o recurso e mostra a medição que sustenta esses números. Você pode parar no resumo acima e ligar o recurso; o resto é a prestação de contas.


1 O que é o synesis-graph, e o que muda agora

O synesis-graph é o pipeline que pega um projeto Synesis já codificado (as anotações qualitativas de um pesquisador, com seus conceitos, fontes e relações) e o transforma num banco de grafos consultável — hoje, Neo4j ou ArcadeDB. Não é um passo de exportação e pronto: é o que permite que Claude Desktop, conectado via MCP (o protocolo que dá a um LLM acesso a ferramentas e dados externos), leia o grafo e responda perguntas sobre a pesquisa em linguagem natural, com citação da fonte. Esse uso — pesquisador perguntando, agente respondendo com base no grafo — é o que chamamos de GraphRAG: Retrieval-Augmented Generation apoiada em uma estrutura de grafo em vez de um monte de trechos de texto soltos (ver Knowledge Graphs e GraphRAG para essa base).

Instalação:

pip install synesis-graph

A busca vetorial que este documento mede precisa do extra opcional embeddings (traz o sentence-transformers, que gera os vetores no seu próprio computador, sem chamada a API externa e sem custo por uso):

pip install "synesis-graph[embeddings]"

Até aqui, quando o Claude Desktop precisava achar um conceito no grafo, ele só tinha uma ferramenta: buscar por palavra-chave. O synesis-graph agora oferece uma segunda: buscar por significado.

Backend usado neste experimento

A busca vetorial hoje só está implementada no backend ArcadeDB do synesis-graph — não no Neo4j. Todos os números deste documento vêm de synesis-graph arcadedb --vector-embeddings ... contra um banco ArcadeDB real, não de um ambiente simulado.

1.1 Três termos, e o que cada um significa no seu trabalho

A literatura técnica chama esses métodos por sigla. Vale traduzir cada um para o raciocínio de quem codifica material qualitativo, porque o resto do documento depende dessa intuição.

Busca por palavra-chave (BM25). Tecnicamente: o algoritmo por trás do Elasticsearch, que pontua um conceito pela sobreposição literal de termos entre a pergunta e a descrição. Na sua prática: é o Ctrl+F numa transcrição. Só encontra se a palavra exata estiver lá. Se você escreveu “fedor” e a ontologia diz “odor”, ele não acha — não porque seja burro, mas porque não é o trabalho dele.

Busca por significado (embeddings vetoriais). Tecnicamente: a descrição de cada conceito é convertida num vetor de números; descrições com sentido parecido produzem vetores próximos, mesmo sem compartilhar nenhuma palavra. Na sua prática: é o colega que leu todo o seu material e lembra — “isso aí é parecido com aquilo que você categorizou como X”. Ele não repete suas palavras; ele reconhece a ideia.

Fusão dos dois (RRF, Reciprocal Rank Fusion). Tecnicamente: os dois métodos produzem cada um a sua lista ordenada, e a fórmula padrão da literatura de IR (Information Retrieval, a área que estuda sistemas de busca) — 1/(60+posição) — combina as duas numa lista só, sem precisar normalizar escalas à mão. Na sua prática: é consultar os dois colegas e dar mais peso ao que cada um colocou no topo da própria lista. Como você verá, é o que funciona melhor — e a razão é o ponto central deste estudo.

1.2 Como uma pergunta vira resposta

flowchart LR
    classDef pergunta fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#1B5E20,font-weight:bold;
    classDef metodo fill:#F8FAFC,stroke:#4A5568,stroke-width:1px,color:#084C84;
    classDef fusao fill:#E0F7FA,stroke:#084C54,stroke-width:2px,color:#084C54,font-weight:bold;
    classDef saida fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#E65100,font-weight:bold;

    P["Pergunta do pesquisador<br/>em linguagem natural"]:::pergunta
    BM["Busca por<br/>palavra exata"]:::metodo
    VE["Busca por<br/>significado"]:::metodo
    RRF["Fusão das<br/>duas listas"]:::fusao
    L["Lista de conceitos<br/>candidatos"]:::saida
    R["Resposta do assistente<br/>citando a fonte"]:::saida

    P --> BM
    P --> VE
    BM --> RRF
    VE --> RRF
    RRF --> L
    L --> R

O pesquisador só vê as duas pontas: a pergunta que digitou e a resposta que recebeu. Tudo entre elas é o que este documento mede.

1.3 O caso que mostra a diferença

Um exemplo real do gabarito deste experimento — com o resultado que as duas buscas de fato devolveram. A pergunta foi:

“what nature provides for free that a new installation might destroy” (o que a natureza oferece de graça e que uma nova instalação pode destruir?)

O conceito certo na ontologia chama-se Ecosystem_Service. Repare que a pergunta não contém “ecossistema” nem “serviço” — nenhuma palavra em comum com o nome do alvo.

Só busca por palavra exata

Devolveu Dominant_Social_Paradigm e Environmental_Condition. O alvo não aparece na lista — o assistente responde com o que tem à mão, que pode ser tangencial.

Com busca por significado

Reconhece que “o que a natureza oferece de graça” é a ideia de serviço ecossistêmico, e traz Ecosystem_Service em 1º lugar, mesmo sem nenhuma palavra em comum.

Este é um dos casos medidos em que só a busca vetorial acertou em 1º. O padrão inverso também existe — há perguntas em que só a palavra exata acerta — e é justamente por isso que a fusão dos dois vence. A seção Onde cada método vence sozinho trata disso.

A pergunta deste estudo é simples de formular e difícil de responder honestamente: quanto, exatamente, esse recurso melhora a chance de o Claude Desktop achar o conceito certo?

A resposta não vem de opinião nem de benchmark de terceiros. Vem de um experimento controlado contra dados reais — o corpus Social_Acceptance (1.388 conceitos sobre aceitação social de tecnologias de transição energética), com um gabarito de 40 perguntas fixado antes de qualquer busca ser executada, medido contra um banco ArcadeDB real em 17 de agosto de 2026.

Quer conferir em vez de acreditar?

O gabarito completo das 40 perguntas, os scripts que reproduzem tudo do zero, as consultas exatas enviadas ao banco e o resultado pergunta por pergunta estão em Como o benchmark foi feito — incluindo o que deu errado e as limitações do desenho.


2 O experimento

2.1 Por que um gabarito fixo importa

A armadilha mais comum em qualquer comparação de busca é formular a pergunta depois de ver o resultado — o que torna qualquer método “vencedor” por construção. Para evitar isso:

  1. Uma amostra aleatória de 40 conceitos foi extraída do corpus (semente fixa, reprodutível).
  2. Para cada conceito, foi escrita uma pergunta em linguagem natural cujo vocabulário é deliberadamente diferente da descrição do conceito — a pergunta usa sinônimos e paráfrases, nunca os termos exatos do ontology_description.
  3. O par (pergunta, conceito esperado) foi congelado em arquivo antes de qualquer consulta rodar.
  4. Só então os três métodos de busca foram executados contra o mesmo índice, no mesmo banco ArcadeDB.

Exemplo do gabarito (as quatro primeiras das 40):

Mostrar código do gráfico
linhas = ["| Pergunta (vocabulário do pesquisador) | Conceito esperado |", "|---|---|"]
for r in RES["por_pergunta"][:4]:
    linhas.append(f"| \"{r['question']}\" | `{r['gold']}` |")
print("\n".join(linhas))
Pergunta (vocabulário do pesquisador) Conceito esperado
“how ready are people to actually put panels on their roof, not just say they like the idea” Adoption_Willingness
“the gains people think they will get that offset the downsides of a project” Benefit
“the big money you have to put down at the start before anything is built” Capital_Cost
“residents taking civic action and participating out of a sense of collective duty” Citizenship_Behavior

Nenhuma dessas perguntas contém uma palavra que apareça no nome do conceito-alvo — regra verificada por código, não no olho. É o cenário realista de um pesquisador conversando com o Claude Desktop via MCP: ele não conhece o vocabulário controlado da ontologia.

O gabarito completo, com as 40 perguntas e o resultado de cada método, está em Como o benchmark foi feito.

2.2 O que foi comparado

Três estratégias de recuperação, todas rodando dentro do ArcadeDB, sem intermediário:

  • BM25 — o índice full-text já existente (SEARCH_INDEX), com o mesmo mecanismo que já documentamos em Knowledge Graphs e GraphRAG.
  • Vetor — vector.neighbors() sobre o índice LSM_VECTOR (a estrutura que o ArcadeDB usa para achar vetores próximos sem varrer todos), usando o modelo all-mpnet-base-v2 (768 dimensões) já embedado para este corpus.
  • RRF — fusão por Reciprocal Rank Fusion dos dois rankings anteriores.

2.3 As métricas, com exemplo

Nenhuma inventada para a ocasião — todas de uso corrente em avaliação de sistemas de busca. O motivo de usar três em vez de uma é que cada uma responde uma pergunta diferente sobre o mesmo resultado.

Suponha que, para uma dada pergunta, a busca devolveu esta lista ordenada: Payment_System, Tenure, Capacity — e o conceito certo era Tenure, que caiu na 2ª posição, não na 1ª.

Métrica O que responde Neste exemplo
Recall@k (o alvo está entre os k primeiros?) Vale 1 se sim, 0 se não, para um k escolhido Recall@1 = 0 (não está em 1º); Recall@3 = 1 (está entre os 3 primeiros)
MRR (posição média da resposta certa) Usa o inverso da posição (1/posição), então 1º lugar vale 1,0 e posições distantes valem cada vez menos 1/2 = 0,5
nDCG@k (o mesmo, mas mais tolerante) Desconta a posição de forma mais suave (log em vez de razão simples) — é o padrão da indústria em ranking de busca Próximo do MRR, ligeiramente mais tolerante com a 2ª posição

Repare a diferença de propósito: a taxa de acerto entre os k primeiros é a métrica que mais importa para GraphRAG, porque o LLM recebe uma lista de conceitos como contexto (não só o primeiro) — o que importa é se o conceito certo está nessa lista, não em que posição exata. As outras duas complementam mostrando quão perto do topo a resposta certa costuma cair, o que interessa quando a ordem também é usada (por exemplo, para decidir qual conceito citar primeiro na resposta).


3 Resultado agregado (40 perguntas, corpus completo)

Mostrar código do gráfico
import plotly.graph_objects as go

metricas = ["Recall@1", "Recall@3", "Recall@5", "Recall@10", "MRR", "nDCG@5", "nDCG@10"]
bm25  = [M["bm25"][m] for m in metricas]
vetor = [M["vector"][m] for m in metricas]
rrf   = [M["rrf"][m] for m in metricas]

CINZA, AZUL, LARANJA = "#94A3B8", "#0E7490", "#E65100"

def rotulo(v, metrica):
    if metrica.startswith("Recall"):
        return f"{round(v * 10)} em cada 10"
    return f"{v:.2f}".replace(".", ",")

fig = go.Figure()
for nome, dados, cor in [
    ("Só palavra exata", bm25, CINZA),
    ("Só significado", vetor, AZUL),
    ("Os dois juntos", rrf, LARANJA),
]:
    fig.add_trace(go.Bar(
        y=metricas, x=dados, name=nome, orientation="h",
        marker=dict(color=cor),
        text=[rotulo(v, m) for v, m in zip(dados, metricas)],
        textposition="outside",
        textfont=dict(size=11),
        hovertemplate="%{y} — " + nome + ": %{x:.3f}<extra></extra>",
    ))

fig.update_layout(
    template="plotly_white",
    barmode="group",
    height=620,
    xaxis=dict(title="Desempenho (0 = nunca acerta, 1 = sempre acerta)", range=[0, 1.15]),
    yaxis=dict(title="", autorange="reversed"),
    legend=dict(orientation="h", yanchor="bottom", y=1.02, x=0),
    font=dict(size=12),
    margin=dict(l=10, r=10, t=60, b=40),
    bargap=0.25,
)
fig.show()
Figura 1: Desempenho dos três métodos nas 40 perguntas do gabarito. Barras mais longas são melhores. A leitura em linguagem comum está anotada ao lado de cada barra.
Mostrar código do gráfico
linhas = ["::: {.callout-note collapse=\"true\"}", "## Os números exatos, em tabela", "",
          "| Métrica | Só palavra | Só significado | Os dois juntos |", "|---|---:|---:|---:|"]
for m in metricas:
    vals = {k: M[k][m] for k in ("bm25", "vector", "rrf")}
    melhor = max(vals, key=vals.get)
    cells = [(f"**{vals[k]:.3f}**" if k == melhor else f"{vals[k]:.3f}").replace(".", ",")
             for k in ("bm25", "vector", "rrf")]
    linhas.append(f"| {m} | " + " | ".join(cells) + " |")
linhas += ["", "Medição de 17/08/2026 — ver [metodologia completa](benchmark_rrf_metodologia.qmd).", ":::"]
print("\n".join(linhas))
Métrica Só palavra Só significado Os dois juntos
Recall@1 0,350 0,325 0,375
Recall@3 0,475 0,525 0,675
Recall@5 0,500 0,725 0,800
Recall@10 0,575 0,825 0,875
MRR 0,421 0,471 0,552
nDCG@5 0,433 0,522 0,607
nDCG@10 0,458 0,556 0,631

Medição de 17/08/2026 — ver metodologia completa.

Três leituras, na ordem em que valem a pena ser feitas:

Mostrar código do gráfico
o = RES["sobreposicao_top1"]
ganho_mrr = (M["rrf"]["MRR"] / M["bm25"]["MRR"] - 1) * 100
ganho_r10 = (M["vector"]["Recall@10"] / M["bm25"]["Recall@10"] - 1) * 100
falha_bm25 = (1 - M["bm25"]["Recall@10"]) * 100
n = lambda v: f"{v:.3f}".replace(".", ",")
pct = lambda v: f"{v * 100:.1f}%".replace(".", ",")

print(f"""
**A fusão vence em toda métrica, não só na média.** Isso não era garantido — poderia ter havido um
método claramente superior sozinho, tornando a fusão um custo sem benefício. Não foi o caso: a fusão
entrega a melhor posição média ({n(M['rrf']['MRR'])} contra {n(M['bm25']['MRR'])} da palavra exata,
um ganho de **+{ganho_mrr:.0f}%**) e a melhor taxa de acerto no top-10
([**{pct(M['rrf']['Recall@10'])}**]{{style="color:#c0392b"}} das perguntas).

**O significado sozinho não é sempre melhor que a palavra exata.** No acerto em 1º lugar, o vetor
({n(M['vector']['Recall@1'])}) perde para a palavra exata ({n(M['bm25']['Recall@1'])}). É
contraintuitivo se a expectativa for "busca semântica sempre vence" — não vence. O vetor aproxima
*significados*, e um corpus com 1.388 conceitos tem muitos significados vizinhos (`Attitude_Change`
e `Opinion_Change`; `Discussion` e `Public_Discussion`). Errar o primeiro lugar entre dois
quase-sinônimos é diferente de não encontrar nada.

**Onde o vetor realmente compensa é na lista dos 10 primeiros** ({n(M['vector']['Recall@10'])} contra
{n(M['bm25']['Recall@10'])} da palavra exata — um salto de {ganho_r10:.0f}%). É o resultado que mais
importa para GraphRAG: o vetor quase nunca deixa o conceito certo *fora* da lista de contexto entregue
ao LLM, mesmo quando não o coloca em primeiro. A palavra exata, sozinha, deixa o alvo de fora em
[**{falha_bm25:.0f}% das perguntas**]{{style="color:#c0392b"}} — o LLM simplesmente não recebe a
informação para responder.
""")

A fusão vence em toda métrica, não só na média. Isso não era garantido — poderia ter havido um método claramente superior sozinho, tornando a fusão um custo sem benefício. Não foi o caso: a fusão entrega a melhor posição média (0,552 contra 0,421 da palavra exata, um ganho de +31%) e a melhor taxa de acerto no top-10 (87,5% das perguntas).

O significado sozinho não é sempre melhor que a palavra exata. No acerto em 1º lugar, o vetor (0,325) perde para a palavra exata (0,350). É contraintuitivo se a expectativa for “busca semântica sempre vence” — não vence. O vetor aproxima significados, e um corpus com 1.388 conceitos tem muitos significados vizinhos (Attitude_Change e Opinion_Change; Discussion e Public_Discussion). Errar o primeiro lugar entre dois quase-sinônimos é diferente de não encontrar nada.

Onde o vetor realmente compensa é na lista dos 10 primeiros (0,825 contra 0,575 da palavra exata — um salto de 43%). É o resultado que mais importa para GraphRAG: o vetor quase nunca deixa o conceito certo fora da lista de contexto entregue ao LLM, mesmo quando não o coloca em primeiro. A palavra exata, sozinha, deixa o alvo de fora em 43% das perguntas — o LLM simplesmente não recebe a informação para responder.


4 Onde cada método vence sozinho

Uma média agregada esconde uma pergunta mais interessante: os dois métodos erram nas mesmas perguntas, ou em perguntas diferentes? Se erram nas mesmas, a fusão não ajuda. Se erram em perguntas diferentes, a fusão é o ganho real.

Mostrar código do gráfico
o = RES["sobreposicao_top1"]
print(f"""| | Contagem (de 40) | Exemplo real da medição |
|---|---:|---|
| Ambos acertam o 1º lugar | {o['ambos']} | — |
| Só a palavra exata acerta | {o['so_bm25']} | "the incumbent coal oil and gas regime..." → `Fossil_Fuel` |
| Só o significado acerta | {o['so_vetor']} | "what nature provides for free that a new installation might destroy" → `Ecosystem_Service` |
| Nenhum acerta o 1º lugar | {o['nenhum']} | (a maioria tem o alvo no top-3/top-5 — ver abaixo) |""")
Contagem (de 40) Exemplo real da medição
Ambos acertam o 1º lugar 5 —
Só a palavra exata acerta 9 “the incumbent coal oil and gas regime…” → Fossil_Fuel
Só o significado acerta 8 “what nature provides for free that a new installation might destroy” → Ecosystem_Service
Nenhum acerta o 1º lugar 18 (a maioria tem o alvo no top-3/top-5 — ver abaixo)

Os conjuntos de erro quase não se sobrepõem. Vale ver isso como três situações que você reconhece do seu próprio trabalho.

Quando você lembra do jargão, a palavra exata basta. Você quer conferir onde codificou o peso do regime de carvão, petróleo e gás. Você escreve “the incumbent coal oil and gas regime that locks in existing infrastructure” — e esses termos estão quase literais na descrição do conceito. A palavra exata traz Fossil_Fuel em 1º; a busca por significado se dispersa entre Concentrated_Power e Infrastructure_Lock-In, ideias vizinhas e plausíveis, mas não a que você queria.

Quando você lembra do fenômeno mas não do rótulo, só o significado salva. Você está codificando uma entrevista sobre o que a natureza oferece de graça e que uma obra pode destruir. Você não lembra que categorizou isso como Ecosystem_Service meses atrás, e sua pergunta não contém “ecossistema” nem “serviço”. A palavra exata devolve Dominant_Social_Paradigm e Environmental_Condition — nada útil. A busca por significado reconhece a ideia e traz o conceito em 1º lugar.

Quando você quer evitar duplicar um conceito, os dois juntos importam. Este terceiro caso não vem da medição — é o cenário do seu próprio projeto, e o motivo prático de ligar o recurso. Imagine que você está prestes a criar uma categoria nova para “medo de que o cheiro incomode os vizinhos”, numa pesquisa sobre biodigestores rurais. Já são 400 trechos codificados e você não confia mais na própria memória do que existe na ontologia. Você pergunta em linguagem natural: se um conceito de incômodo por odor já existir, a busca por significado o encontra mesmo que você tenha escrito “fedor” e a ontologia diga “odor”; e se existir também um rótulo próximo cujo termo você acertou por acaso, a palavra exata o traz. Ver os dois na mesma lista é o que evita criar um conceito redundante — o tipo de erro que só aparece muito depois, na hora de analisar as frequências.

Essa é a evidência central deste estudo: os dois métodos não competem pelo mesmo espaço de perguntas — cobrem espaços diferentes. É por isso que a fusão soma, e não apenas repete o melhor dos dois.

4.1 Os casos onde nenhum método chegou perto

Vale nomear as falhas em vez de escondê-las atrás da média:

Mostrar código do gráfico
falhas = [r for r in RES["por_pergunta"]
          if not any(r[m]["rank"] for m in ("bm25", "vector", "rrf"))]
print(f"Em **{len(falhas)} das 40 perguntas** o alvo ficou fora do top-10 de *todos* os métodos:\n")
for r in falhas:
    viz = ", ".join(f"`{t}`" for t in r["vector"]["top3"])
    print(f"- `{r['gold']}` — para *\"{r['question']}\"*.  \n  A busca por significado devolveu {viz}.")

Em 2 das 40 perguntas o alvo ficou fora do top-10 de todos os métodos:

  • Market_Demand — para “whether there are enough buyers and investors to make deployment happen”.
    A busca por significado devolveu Technology_Deployment, Investment, Technology_Availability.
  • Renewable_Energy_Adaptation — para “fitting clean generation into the systems and society we already have”.
    A busca por significado devolveu Decarbonization_Success, Sustainable_Business_Practice, New_Ecological_Paradigm.

Nos dois casos o “erro” é um conceito adjacente plausível — Investment para uma pergunta sobre compradores e investidores é o tipo de resposta que um pesquisador aceitaria numa conversa real, mas que um gabarito de resposta única não perdoa. Isso é uma limitação do desenho do experimento (uma pergunta, um único conceito certo), não necessariamente do sistema — e é discutido mais abaixo.


5 O que isso significa para GraphRAG via MCP

Recuperabilidade, no sentido em que a IR usa a palavra, é a capacidade de um sistema trazer de volta a informação certa quando alguém pergunta por ela — mesmo sem usar os termos exatos do índice. É exatamente o problema que o Claude Desktop enfrenta a cada pergunta feita via MCP: o pesquisador não conhece o vocabulário controlado da ontologia, então cada pergunta é, por definição, um teste de recuperabilidade.

O cenário de uso real não é “encontrar o nó exato” — é dar ao LLM contexto suficiente para responder com precisão, o que corresponde à lista dos 5 ou 10 primeiros, não ao acerto em 1º lugar. Sob essa lente:

Mostrar código do gráfico
pct = lambda v: f"{v * 100:.1f}%".replace(".", ",")
print(f"""
- **Só palavra exata** entrega o conceito certo em {pct(M['bm25']['Recall@5'])} das perguntas no top-5.
  Em metade das conversas, o pesquisador pergunta e o MCP simplesmente não traz o conceito relevante —
  o Claude Desktop responde com o que tem, que pode ser tangencial.
- **Os dois juntos** entregam em {pct(M['rrf']['Recall@5'])} no top-5 e
  [**{pct(M['rrf']['Recall@10'])} no top-10**]{{style="color:#c0392b"}}. A diferença não é sutil: é a
  diferença entre "o agente geralmente encontra o que você quer dizer" e "o agente frequentemente não encontra".
""")
  • Só palavra exata entrega o conceito certo em 50,0% das perguntas no top-5. Em metade das conversas, o pesquisador pergunta e o MCP simplesmente não traz o conceito relevante — o Claude Desktop responde com o que tem, que pode ser tangencial.
  • Os dois juntos entregam em 80,0% no top-5 e 87,5% no top-10. A diferença não é sutil: é a diferença entre “o agente geralmente encontra o que você quer dizer” e “o agente frequentemente não encontra”.

5.1 Latência: o custo de adicionar a busca vetorial

Mostrar código do gráfico
import plotly.graph_objects as go

fig = go.Figure()

LAT = RES["latencia_ms"]
_vec, _bm = LAT["vetor_completo"]["media"], LAT["bm25"]["media"]

fig.add_trace(go.Bar(
    y=["Com busca por significado", "Só palavra exata"],
    x=[_vec, _bm],
    orientation="h",
    marker=dict(color=["#E65100", "#94A3B8"]),
    text=[f"{_vec:.0f} ms", f"{_bm:.0f} ms"],
    textposition="outside",
    textfont=dict(size=13),
    hovertemplate="%{y}: %{x:.1f} ms (média)<extra></extra>",
    showlegend=False,
))

fig.add_vline(
    x=100, line_width=2, line_dash="dash", line_color="#c0392b",
    annotation_text="100 ms — limiar do 'instantâneo'",
    annotation_position="top",
    annotation_font=dict(color="#c0392b", size=12),
)

fig.update_layout(
    template="plotly_white",
    height=280,
    xaxis=dict(title="Tempo por pergunta (milissegundos)", range=[0, 145]),
    yaxis=dict(title=""),
    font=dict(size=12),
    margin=dict(l=10, r=10, t=50, b=40),
    bargap=0.4,
)
fig.show()
Figura 2: Tempo de resposta por pergunta, medido no mesmo corpus (1.388 conceitos, 20 execuções por método). A linha marca os 100 ms que a literatura de usabilidade usa como limiar do que parece instantâneo.
Mostrar código do gráfico
extra = LAT["vetor_completo"]["media"] - LAT["bm25"]["media"]
print(f"""
Os dois métodos ficam bem abaixo do limiar. O custo adicional da busca vetorial é de
**~{extra:.0f} ms** sobre a busca por palavra exata, e o total continua abaixo da metade do que uma
pessoa percebe como demora. Em uma sessão de MCP, isso é imperceptível diante da latência do próprio
LLM gerando a resposta — que é da ordem de segundos, não de milissegundos.
""")

nomes = {"bm25": "Busca por palavra (BM25)", "encode": "Codificar a pergunta (encode)",
         "knn": "Busca vetorial (kNN)", "vetor_completo": "**Vetor completo** (encode + busca)"}
linhas = ["::: {.callout-note collapse=\"true\"}", "## Os números exatos, por etapa", "",
          "| Etapa | Média | Mediana | p95 |", "|---|---:|---:|---:|"]
for k, nome in nomes.items():
    v = LAT[k]
    f = lambda x: f"{x:.1f} ms".replace(".", ",")
    linhas.append(f"| {nome} | {f(v['media'])} | {f(v['mediana'])} | {f(v['p95'])} |")
linhas.append(":::")
print("\n".join(linhas))

Os dois métodos ficam bem abaixo do limiar. O custo adicional da busca vetorial é de ~34 ms sobre a busca por palavra exata, e o total continua abaixo da metade do que uma pessoa percebe como demora. Em uma sessão de MCP, isso é imperceptível diante da latência do próprio LLM gerando a resposta — que é da ordem de segundos, não de milissegundos.

Etapa Média Mediana p95
Busca por palavra (BM25) 12,8 ms 5,6 ms 27,2 ms
Codificar a pergunta (encode) 34,7 ms 35,0 ms 37,2 ms
Busca vetorial (kNN) 12,4 ms 11,7 ms 12,9 ms
Vetor completo (encode + busca) 47,1 ms 46,7 ms 50,0 ms

6 O que o experimento não prova

Honestidade sobre limites, porque um estudo que só mostra vitórias não é confiável. Em ordem do que mais afeta a sua decisão:

1. Estes números são de um corpus, em inglês — meça no seu. O que isso significa para você: se o seu projeto for em português, ou tiver muito menos conceitos, ou tratar de um domínio mais homogêneo, os números podem ser bem diferentes. Ligue o recurso e meça no seu próprio projeto antes de assumir o mesmo ganho. O detalhe técnico: os valores vêm do Social_Acceptance com o modelo all-mpnet-base-v2, e tanto o idioma quanto a densidade conceitual do corpus afetam o resultado.

2. A régua é dura demais com sinônimos, então o ganho real é provavelmente maior. O que isso significa para você: boa parte dos “erros” contabilizados são conceitos vizinhos legítimos que você aceitaria numa conversa — Collaboration no lugar de Cooperation, por exemplo. O detalhe técnico: o gabarito admite uma única resposta certa por pergunta. Um gabarito com múltiplas respostas aceitas produziria números mais altos e provavelmente mais realistas, mas seria mais difícil de construir sem viés do avaliador.

3. Nenhum método é infalível, nem com a fusão ligada. O que isso significa para você: ainda haverá perguntas em que o assistente não acha o que você quer dizer. Ligar o recurso reduz muito a frequência disso; não elimina. O detalhe técnico: a fusão erra o 1º lugar na maioria das 40 perguntas — embora, na maior parte delas, o alvo esteja no top-5. Há inclusive 3 casos em que a fusão perdeu um alvo que um método isolado tinha encontrado; estão documentados na metodologia.

4. São 40 perguntas: um padrão claro, não uma prova estatística. O que isso significa para você: a conclusão qualitativa (os dois métodos erram em perguntas diferentes, e por isso somam) é sólida. Os valores decimais específicos, não trate como constantes. O detalhe técnico: 40 perguntas bastam para revelar o padrão, mas não validam uma distribuição populacional inteira do corpus.


7 Perguntas frequentes

Preciso saber programar para ativar isso? Não. É instalar um pacote e passar uma opção a mais no comando que você já usa para gerar o grafo (--vector-embeddings). Nenhuma linha de código.

Isso muda a minha ontologia ou os meus dados? Não muda nada do que você codificou. Os embeddings são um índice adicional, construído a partir das descrições dos conceitos e guardado ao lado deles. Seus conceitos, fontes e relações permanecem exatamente como estão — o que muda é só a forma de procurá-los.

Preciso de internet ou de API paga? Não. O sentence-transformers gera os vetores no seu próprio computador. Não há chamada a serviço externo, custo por uso, nem envio do seu material para fora. (O assistente que consome o grafo via MCP é outra questão — esse é o Claude Desktop, e vale a política de privacidade dele.)

Funciona em português? O modelo padrão (all-mpnet-base-v2) foi treinado em inglês, e este experimento mediu um corpus em inglês. Para material em português, existem modelos multilíngues que se encaixam no mesmo lugar — mas não medimos, então não prometemos números. Se o seu projeto é em português, ligue e avalie no seu caso antes de contar com o ganho.

Vale a pena se meu projeto é pequeno? Provavelmente menos. O problema que a busca por significado resolve — não lembrar o rótulo exato entre centenas de conceitos — quase não existe quando você tem trinta categorias e conhece todas de cabeça. O ganho cresce com o tamanho da ontologia.


8 Recomendação

Ative --vector-embeddings quando o uso for busca conversacional — MCP, GraphRAG, pesquisador perguntando com suas próprias palavras. É exatamente o caso onde a palavra exata sozinha deixa o conceito certo de fora em mais de 40% das perguntas.

Não é necessário para consultas Cypher/SQL estruturadas que já sabem o nome exato do conceito — ali, a busca por palavra-chave (ou o MATCH direto) já é ótima e o vetor não acrescenta nada.

A implementação, a configuração por projeto e os passos para gerar os embeddings estão documentados no README do synesis-graph.


Reflexão final: a busca por significado não é “melhor” que a busca por palavra exata — é complementar, e o experimento mostra exatamente por quê: os dois métodos erram em perguntas diferentes, não nas mesmas. É esse desencontro de erros, não a superioridade de um método sobre o outro, que faz a fusão valer o investimento de infraestrutura.