Por que o seu app de anotações não encontra a sua nota

Product

Existe um momento específico em que um app de anotações deixa de valer o que custa. Não é quando o armazenamento acaba. É a primeira vez em que você tem certeza de que anotou algo, procura, e o app diz que não há resultados.

Você anotou, sim. A nota está lá. Só que você buscou "aquela coisa sobre faixas de preço", e a nota diz "oferta em três patamares, ancorada no SKU do meio". Nenhuma palavra em comum.

É uma falha que piora à medida que o seu arquivo fica mais valioso. Com cem notas, você lembra as palavras. Com dez mil, lembra a ideia e mais nada. A busca por palavra-chave está no seu ponto mais fraco justamente quando você mais precisa dela.

O Notez faz duas buscas para cada consulta e junta os resultados. Este texto mostra como isso funciona de fato — inclusive as partes mais rudimentares do que você imaginaria.

Ramo um: palavra-chave

O ramo de palavra-chave é uma simples busca de substring em SQL. Sua consulta vira %query% e é comparada, sem diferenciar maiúsculas de minúsculas, com:

  • o título da nota

  • o texto puro extraído da nota (o documento de texto rico achatado em texto ao salvar)

  • os valores das propriedades personalizadas — os rótulos das opções de seleção e de seleção múltipla e o texto das propriedades do tipo texto

Este último item pesa mais do que parece. Se você tem uma propriedade Client com a opção Acme, buscar acme encontra todas as notas marcadas com ela, mesmo que a palavra nunca apareça no corpo.

Voltam até 50 resultados por palavra-chave, ordenados pela atualização mais recente.

No que este ramo é bom: encontrar exatamente o que você digitou. Identificadores, códigos de erro, nomes, uma frase que você lembra palavra por palavra. Se você digita NOTE_MODIFIED, quer a nota que contém essa string literal, em primeiro lugar, na hora.

No que é ruim: em todo o resto. Uma busca de substring não é aproximada — um erro de digitação na consulta não encontra nada. Sinônimos não encontram nada. Conceitos não encontram nada.

Ramo dois: semântico

O ramo semântico converte sua consulta num vetor e procura conteúdo de notas que fique perto dele nesse espaço.

Na prática: a consulta vira um embedding de 1.536 dimensões com o modelo text-embedding-3-small da OpenAI, e o Postgres (com pgvector) ordena os trechos de notas armazenados pela distância de cosseno até ele. Voltam até 20 notas.

No que este ramo é bom: no caso que quebrou a busca por palavra-chave. "Faixas de preço" e "oferta em três patamares" ficam perto uma da outra no espaço de embeddings, porque o modelo foi treinado com texto suficiente para saber que tratam do mesmo assunto.

No que é ruim: em precisão. Vetores não têm noção de "exatamente esta string". Busque um UUID e o ramo semântico vai entregar, cheio de confiança, algo de tema parecido e inútil.

Nenhum dos dois ramos basta sozinho. Esse é todo o argumento para rodar os dois.

O que de fato vira embedding

Não dá para transformar uma nota inteira num único vetor e esperar bons resultados. Uma nota de reunião de 4.000 palavras reduzida à média num único ponto do espaço não fica perto de nada em particular — é um borrão sobre todos os temas que ela toca.

Por isso as notas são divididas em trechos (chunks) antes do embedding, e a divisão segue a estrutura do próprio documento:

  1. O título vai para o início do conteúdo antes de qualquer outro passo, para que as palavras do título façam parte do que é indexado, e não fiquem num campo separado que ninguém pesquisa.

  2. Dividir nos títulos de seção. Cada título de seção começa uma nova seção. Um título de seção sem texto embaixo é indexado mesmo assim, sozinho, porque ele tem significado mesmo quando a seção está vazia.

  3. Dividir seções longas em parágrafos. Tudo o que passa de 2.000 caracteres é cortado no limite entre parágrafos, e não no meio de uma frase.

  4. Juntar trechos minúsculos aos vizinhos. Tudo o que tem menos de 100 caracteres é incorporado ao trecho adjacente em vez de ser descartado.

Essa última regra existe por causa de um bug. Uma versão anterior descartava os trechos pequenos demais, e uma nota feita de várias seções curtas — exatamente o formato de uma nota de reunião em tópicos — ficava com quase nada. A nota estava indexada, tecnicamente. E também era impossível de encontrar.

Cada trecho armazenado guarda o seu título de seção e a sua posição no documento, então um resultado semântico mostra qual parte de uma nota longa correspondeu à busca, e não só que a nota correspondeu em algum lugar.

Como uma nota longa gera muitos trechos, a consulta busca o triplo dos resultados de que precisa e depois mantém só o trecho com melhor pontuação de cada nota. Sem isso, uma única nota extensa ocuparia todas as posições dos resultados.

Os dois limites, e por que são diferentes

A distância de cosseno vai de 0 (mesma direção) para cima. Um resultado só conta como correspondência abaixo de um teto de distância — e o Notez usa dois tetos diferentes para duas tarefas diferentes.

A busca usa 0,62. Um teto rígido.

O painel Notas semelhantes usa 0,8. Um teto folgado.

A diferença não é descuido. Notas semelhantes é um apoio para navegar, no fim de uma nota que você já está lendo; uma correspondência folgada ali custa só uma olhada. Resultados de busca são uma afirmação de que aquelas notas respondem à sua consulta.

Com 0,8, a busca ficou inútil de um jeito que demorou a ser percebido. Uma consulta qualquer correspondia a quase qualquer nota curta — texto curto gera embeddings que ficam perto do meio de tudo —, e por isso um espaço de trabalho com notas nunca conseguia mostrar "Nenhuma nota encontrada". Toda busca retornava alguma coisa. Parte era lixo, e nada separava um caso do outro.

Uma busca que nunca consegue dizer "não tem nada aqui" não é uma busca. É um gerador de notas aleatórias com passos extras.

Como os dois ramos são combinados

Esta é a parte mais simples do que você imaginaria: os resultados por palavra-chave vêm primeiro, em ordem. Depois, os resultados semânticos que ainda não estão na lista são acrescentados ao final.

Só isso. Não há reranking com cross-encoder, nem fusão de pontuações, nem combinação por rank recíproco. Os dois ramos rodam em paralelo, as duplicatas são removidas pelo id da nota, e o ramo de palavra-chave ganha os empates por vir primeiro.

Digo isso com todas as letras porque tem uma consequência que você vai notar: uma correspondência semântica perfeita pode aparecer abaixo de uma correspondência medíocre por palavra-chave. Se a sua consulta for uma substring comum, o topo dos resultados vai ser das correspondências de substring.

Um ranking de fusão de verdade seria uma melhoria real, e não está implementado. O que está implementado é honesto sobre o que é: uma união, não um ranking.

Os filtros valem para os dois ramos

Qualquer filtro que você aplicou — tags, valores de propriedades personalizadas, a lixeira — restringe as duas buscas, não só a de palavra-chave. O ramo semântico leva essas condições para a mesma consulta que calcula a distância vetorial, então filtrar nunca descarta em silêncio a metade semântica dos seus resultados.

O que acontece quando o ramo semântico falha

Gerar o embedding de uma consulta exige uma chamada de rede a um modelo externo. Chamadas de rede falham.

Quando o ramo semântico lança uma exceção, o erro vai para o log e esse ramo devolve uma lista vazia. A busca não dá erro. Você recebe os resultados por palavra-chave, exatamente o que receberia de qualquer app de anotações convencional, e a página é renderizada.

É de propósito: a metade sofisticada de um recurso nunca deve conseguir derrubar a metade sem graça. Cair para a busca simples por palavra-chave é um dia ruim. Mostrar uma página de erro porque um banco de dados vetorial ficou lento é um dia pior.

Notas semelhantes, em resumo

Toda nota tem um painel Notas semelhantes, que funciona um pouco diferente da busca: em vez de gerar o embedding de uma consulta, ele pega o embedding do primeiro trecho da própria nota e encontra os vizinhos mais próximos no mesmo espaço de trabalho, excluindo a própria nota. Até cinco resultados, com o teto mais folgado, de 0,8.

É o recurso que traz à tona coisas que você nunca teria buscado, porque, para buscar, você já precisa suspeitar de que a conexão existe.

Aqui, o limite do espaço de trabalho é garantido por um join com a tabela de notas, em vez de confiar nas linhas de embeddings, que não têm coluna de espaço de trabalho própria. Erre nisso e o espaço de trabalho do seu emprego começa a recomendar suas notas pessoais.

Como buscar bem, na prática

Considerando como os dois ramos dividem o trabalho:

  • Para algo que você lembra exatamente — um nome, uma mensagem de erro, um identificador —, digite ao pé da letra. O ramo de palavra-chave vai acertar em cheio.

  • Para algo de que você só se lembra por alto, escreva uma frase curta descrevendo a ideia, não as palavras. "Por que tiramos o retry da fila" funciona melhor do que tentar adivinhar o vocabulário original.

  • Não escreva consultas longas. Consultas muito longas viram um embedding que é a média de vários temas e não correspondem bem a nada.

  • Combine busca e filtros. Restringir a uma tag antes de buscar transforma uma consulta ampla numa precisa, e a restrição vale para os dois ramos.

  • Se você encontrou a nota, mas não o trecho, abra-a e confira Notas semelhantes — muitas vezes é por esse painel que você encontra as outras três notas sobre o assunto.

O que não existe

Para você não descobrir do jeito difícil:

  • Nada de correspondência aproximada ou tolerante a erros de digitação. Erre a grafia e o ramo de palavra-chave fica em silêncio.

  • Nada de operadores de busca — sem frases entre aspas, sem AND/OR, sem sintaxe por campo.

  • Nada de ranking de relevância entre os dois ramos, como descrito acima.

  • Nada de buscas salvas. Os filtros ficam na URL e podem ir para os favoritos do navegador, mas não existe um recurso de visualizações com nome.

Nada disso é segredo, e nada disso é o motivo de a sua nota ser difícil de encontrar. O motivo quase sempre é o primeiro deste texto: você buscou as palavras, e o que anotou foi a ideia.

Duas buscas unidas resolvem esse problema específico. Não é mágica. É só reconhecer que a memória humana do que você mesmo escreveu é semântica, e uma consulta LIKE não é.

Continue lendo