Pourquoi votre application de prise de notes ne trouve pas votre note

Product

Il y a un moment précis où une application de prise de notes cesse de mériter sa place. Ce n’est pas quand vous manquez d’espace de stockage. C’est la première fois que vous savez avec certitude avoir noté quelque chose, que vous le cherchez, et que l’application vous répond qu’il n’y a aucun résultat.

Vous l’avez bien écrit. La note est là. Simplement, vous avez cherché « l’histoire des grilles tarifaires » et la note dit « offre en trois paliers, ancrée sur le SKU du milieu ». Pas un mot en commun.

Cette défaillance s’aggrave à mesure que vos archives prennent de la valeur. Avec cent notes, vous vous souvenez de la formulation. Avec dix mille, vous vous souvenez de l’idée, et de rien d’autre. La recherche par mots-clés est au plus faible exactement quand vous en avez le plus besoin.

Notez lance deux recherches pour chaque requête et fusionne les résultats. Cet article explique comment cela fonctionne vraiment, y compris les parties plus rudimentaires que vous ne le supposez peut-être.

Première branche : les mots-clés

La branche mots-clés est une simple recherche de sous-chaîne en SQL. Votre requête devient %query% et est comparée, sans tenir compte de la casse, à :

  • le titre de la note

  • le texte brut extrait de la note (le document enrichi aplati en texte au moment de l’enregistrement)

  • les valeurs des propriétés personnalisées : les libellés des options de sélection et de sélection multiple, et le texte des propriétés de type texte

Ce dernier point compte plus qu’il n’y paraît. Si vous avez une propriété Client avec l’option Acme, chercher acme trouve toutes les notes qui portent cette valeur, même si le mot n’apparaît jamais dans le corps du texte.

Cette branche renvoie jusqu’à 50 résultats, des plus récemment modifiés aux plus anciens.

Ce que cette branche fait bien : retrouver un texte exact. Identifiants, codes d’erreur, noms, une phrase dont vous vous souvenez mot pour mot. Si vous tapez NOTE_MODIFIED, vous voulez la note qui contient cette chaîne littérale, en premier, tout de suite.

Ce qu’elle fait mal : tout le reste. Une recherche de sous-chaîne n’est pas approximative : une faute de frappe dans la requête ne trouve rien. Les synonymes ne trouvent rien. Les concepts ne trouvent rien.

Deuxième branche : la recherche sémantique

La branche sémantique convertit votre requête en vecteur et cherche le contenu de notes qui se trouve près de lui dans cet espace.

Concrètement : la requête est transformée en embedding par le modèle text-embedding-3-small d’OpenAI, sur 1 536 dimensions, et Postgres (avec pgvector) classe les fragments de notes stockés selon leur distance cosinus à ce vecteur. Jusqu’à 20 notes sont renvoyées.

Ce que cette branche fait bien : le cas qui mettait la recherche par mots-clés en échec. « Grilles tarifaires » et « offre en trois paliers » tombent près l’une de l’autre dans l’espace des embeddings, parce que le modèle a été entraîné sur assez de texte pour savoir qu’il s’agit du même sujet.

Ce qu’elle fait mal : la précision. Les vecteurs ne connaissent pas la notion de « cette chaîne exacte ». Cherchez un UUID, et la branche sémantique vous tendra avec assurance quelque chose de thématiquement voisin et inutile.

Aucune branche ne suffit seule. C’est toute la raison de faire tourner les deux.

Ce qui est réellement transformé en embedding

On ne peut pas transformer une note entière en un seul vecteur et espérer de bons résultats. Une note de réunion de 4 000 mots, moyennée en un seul point de l’espace, n’est proche de rien en particulier : c’est une traînée floue sur tous les sujets qu’elle aborde.

Les notes sont donc découpées en fragments avant l’embedding, et le découpage suit la structure du document lui-même :

  1. Le titre est placé en tête du contenu avant toute autre étape, si bien que les mots du titre font partie de ce qui est indexé au lieu de rester un champ séparé que personne ne cherche.

  2. Découpage aux titres de section. Chaque titre ouvre une nouvelle section. Un titre sans texte en dessous est quand même indexé seul, parce qu’un titre porte un sens même quand la section est vide.

  3. Découpage des longues sections aux paragraphes. Tout ce qui dépasse 2 000 caractères est coupé entre deux paragraphes plutôt qu’au milieu d’une phrase.

  4. Fusion des tout petits fragments avec leurs voisins. Tout ce qui fait moins de 100 caractères est fusionné avec le fragment adjacent au lieu d’être abandonné.

Cette dernière règle existe à cause d’un bug. Une version précédente jetait les fragments trop petits : une note faite de plusieurs sections courtes, exactement ce à quoi ressemble une note de réunion à puces, ne gardait presque rien. Techniquement, la note était indexée. Elle était aussi introuvable.

Chaque fragment stocké garde son titre de section et sa position dans le document : un résultat sémantique peut ainsi indiquer quelle partie d’une longue note correspond, et pas seulement que la note correspond quelque part.

Comme une longue note produit de nombreux fragments, la requête récupère trois fois plus de résultats que nécessaire, puis ne garde que le fragment le mieux classé de chaque note. Sans cela, une seule note tentaculaire occuperait toutes les places de vos résultats.

Les deux seuils, et pourquoi ils diffèrent

La distance cosinus part de 0 (même direction) et augmente. Un résultat ne compte comme correspondance que sous un plafond de distance, et Notez utilise deux plafonds différents pour deux usages différents.

La recherche utilise 0,62. Strict.

Notes similaires utilise 0,8. Permissif.

La différence n’est pas un oubli. Notes similaires est une aide à la navigation au bas d’une note que vous lisez déjà ; une correspondance approximative ne vous y coûte qu’un coup d’œil. Les résultats de recherche, eux, affirment que ces notes répondent à votre requête.

À 0,8, la recherche devenait inutile, d’une manière qui a mis du temps à se remarquer. Une requête quelconque correspondait à presque n’importe quelle note courte (un texte court produit des embeddings situés près du centre de tout), si bien qu’un espace de travail non vide ne pouvait jamais afficher « Aucune note trouvée ». Chaque recherche renvoyait quelque chose. Une partie n’était que du bruit, et rien ne permettait de distinguer les deux cas.

Une recherche incapable de dire « rien ici » n’est pas une recherche. C’est un générateur de notes aléatoires avec des étapes en plus.

Comment les deux branches sont fusionnées

Cette partie-là est plus simple que vous ne le pensez : les résultats par mots-clés viennent en premier, dans leur ordre. Puis les résultats sémantiques qui ne figurent pas déjà dans cette liste sont ajoutés à la suite.

C’est tout. Pas de reclassement par cross-encoder, pas de fusion des scores, pas de mélange par rang réciproque. Les deux branches tournent en parallèle, les doublons sont éliminés par identifiant de note, et la branche mots-clés l’emporte en cas d’égalité, simplement parce qu’elle passe en premier.

Je le dis franchement parce que cela a une conséquence que vous remarquerez : une correspondance sémantique parfaite peut apparaître sous une correspondance par mots-clés médiocre. Si votre requête se trouve être une sous-chaîne courante, le haut de vos résultats sera occupé par ces correspondances de sous-chaîne.

Un vrai classement fusionné serait une réelle amélioration, et il n’existe pas. Ce qui existe ne prétend pas être autre chose : une union, pas un classement.

Les filtres s’appliquent aux deux branches

Quel que soit votre filtre (étiquettes, valeurs de propriétés personnalisées, corbeille), il contraint les deux recherches, pas seulement celle par mots-clés. La branche sémantique intègre ces conditions dans la même requête que la distance vectorielle : filtrer ne fait donc jamais disparaître en silence la moitié sémantique de vos résultats.

Ce qui se passe quand la branche sémantique tombe en panne

Transformer une requête en embedding implique un appel réseau vers un modèle externe. Les appels réseau échouent.

Quand la branche sémantique lève une erreur, celle-ci est journalisée et la branche renvoie une liste vide. La recherche, elle, n’échoue pas. Vous obtenez les résultats par mots-clés, exactement ce que vous auriez obtenu de n’importe quelle application de notes classique, et la page s’affiche.

C’est voulu : la moitié sophistiquée d’une fonction ne doit jamais pouvoir faire tomber la moitié ordinaire. Se rabattre sur une simple recherche par mots-clés, c’est une mauvaise journée. Afficher une page d’erreur parce qu’une base vectorielle était lente, c’en est une pire.

Notes similaires, en bref

Chaque note a un panneau Notes similaires, qui fonctionne un peu différemment de la recherche : au lieu de transformer une requête en embedding, il prend l’embedding du premier fragment de la note et cherche ses plus proches voisins dans le même espace de travail, en excluant la note elle-même. Jusqu’à cinq résultats, avec le plafond plus permissif de 0,8.

C’est la fonction qui fait remonter ce que vous n’auriez jamais cherché, car pour chercher, il faut déjà soupçonner que le lien existe.

Ici, la limite de l’espace de travail est garantie par une jointure avec la table des notes, au lieu de faire confiance aux lignes d’embeddings, qui n’ont pas de colonne d’espace de travail. Une erreur à cet endroit, et votre espace de travail professionnel se met à recommander vos notes personnelles.

Comment bien chercher

Vu la façon dont les deux branches se partagent le travail :

  • Pour ce dont vous vous souvenez exactement (un nom, un message d’erreur, un identifiant), tapez-le tel quel. La branche mots-clés le trouvera du premier coup.

  • Pour ce dont vous n’avez retenu que les contours, écrivez une courte phrase qui décrit l’idée, pas les mots. « Pourquoi on a renoncé aux nouvelles tentatives dans la file » marche mieux que tenter de deviner le vocabulaire d’origine.

  • N’écrivez pas de longues requêtes. Une requête très longue donne un embedding qui fait la moyenne de plusieurs sujets et ne correspond bien à rien.

  • Combinez recherche et filtres. Se limiter à une étiquette avant de chercher transforme une requête large en requête précise, et cela contraint les deux branches.

  • Si vous trouvez la note mais pas le passage, ouvrez-la et regardez Notes similaires : c’est souvent par ce panneau que vous trouvez les trois autres notes sur le sujet.

Ce qui n’existe pas

Pour vous éviter de le découvrir à vos dépens :

  • Pas de recherche par mots-clés approximative ni tolérante aux fautes de frappe. Faites une faute, et la branche mots-clés reste muette.

  • Pas d’opérateurs de recherche : ni expressions entre guillemets, ni AND/OR, ni syntaxe limitée à un champ.

  • Pas de classement par pertinence entre les deux branches, comme expliqué plus haut.

  • Pas de recherches enregistrées. Les filtres vivent dans l’URL et peuvent être ajoutés aux favoris du navigateur, mais il n’existe pas de vues nommées.

Rien de tout cela n’est secret, et rien de tout cela n’explique pourquoi votre note est difficile à trouver. La raison est presque toujours la première de cet article : vous avez cherché les mots, et vous aviez noté l’idée.

Deux recherches réunies règlent ce problème précis. Ce n’est pas de la magie. C’est seulement reconnaître que la mémoire humaine de ses propres écrits est sémantique, et qu’une requête LIKE ne l’est pas.

À lire aussi