Second Brain documentation

ADR 0013 – Recherche plein texte : champs d'aspects indexés par le moteur, français sans accents, classement pondéré

Accepté (2026-10-15, mode autonome)

← Toutes les décisions

ADR 0013 – Recherche plein texte : champs d’aspects indexés par le moteur, français sans accents, classement pondéré

Statut : Accepté (2026-10-15, mode autonome)

Décisions prises en mode autonome (Aurélien, 2026-10-15 : options les plus simples, sûres et réversibles) : les valeurs des champs d’aspects entrent dans node.search par une colonne texte node.aspect_text, calculée en Java à chaque écriture d’aspect, dans la même transaction (ni trigger ni table dénormalisée) ; seuls les champs de texte libre sont indexés (type: string sans enum ni const, sans format ou avec format: email | uri), selon une règle générique tirée du JSON Schema, sans clé YAML au jalon 1 (pas de x-searchable) ; pondération titre A, champs B, corps C (D réservé au contenu des fichiers, jalon 3) ; classement par ts_rank après les rangs existants (clé exacte, début de titre) ; insensibilité aux accents par une configuration public.french_unaccent (extension unaccent, racinisation french_stem conservée) appliquée au titre, au corps, aux champs et au début de titre ; rattrapage par une réconciliation Java au démarrage, avant l’ouverture du port HTTP, qui sert aussi après une modification de config/ ; tags hors recherche plein texte ; texte des champs plafonné à 20 000 caractères ; pas d’extrait ni de surlignage ; API inchangée.

Contexte

Le cahier (« Recherche et retrouvabilité », MVP, jalon 1) demande une recherche plein texte sur les « titres, corps, valeurs de champs, avec support du français (accents, racinisation) ». Le backlog en fait la tranche « Recherche plein texte étendue aux valeurs de champs d’aspects ».

État du code :

  • node.search (V1) est une colonne générée : setweight(to_tsvector('french', title), 'A') || setweight(to_tsvector('french', body), 'B'), indexée par node_search_idx (GIN). Une colonne générée ne peut lire que les colonnes de sa propre ligne : elle ne voit pas node_aspect.
  • NodeRepository.search combine search @@ websearch_to_tsquery('french', q) avec trois conditions sur le titre : ILIKE 'q%', ILIKE '% q%' et title_key = key(q). Le tri place d’abord la clé exacte (comme la résolution d’un lien wiki, ADR 0009), puis les titres qui commencent par la saisie, puis updated_at desc. Aucun ts_rank : les poids A et B n’ont aujourd’hui aucun effet. Les ILIKE '%…' imposent déjà un parcours séquentiel de node.
  • GET /api/nodes?q= sert la liste des nœuds (200 résultats) et toutes les autocomplétions : sélecteur node-ref, panneau « Liens », déplacement, suggestion [[ (10 résultats).
  • node_aspect.data est un JSONB par aspect, validé à l’écriture contre le JSON Schema de l’aspect. Les seuls chemins d’écriture sont NodeService.create, putAspect et removeAspect.
  • Le corps est borné à 200 000 caractères (ADR 0008), parce qu’un tsvector est limité à 1 Mo. Les champs d’aspects n’ont aucune borne.
  • title_key (ADR 0009) est déjà une colonne dérivée maintenue par Java, sans événement, avec un remplissage SQL dans sa migration.
  • Le méta-schéma accepte toute clé sur un champ ($defs/field n’a pas additionalProperties: false) ; seul x-target est interprété.

Les champs des aspects d’exemple sont de nature très différente : texte libre (courrier.expediteur, document.reference, facture.communication, tache.contexte, projet.objectif), énumérations en codes kebab-case (tache.statut: a-faire, courrier.canal: poste, valeur par défaut de tous les courriers), dates ISO, nombres, booléens, UUID (tache.projet, format: node-ref).

Sonde (PostgreSQL 17.11, image postgres:17-alpine, propriétaire de base non superutilisateur, 2026-10-15) :

Vérification Résultat
unaccent disponible oui, version 1.1, extension trusted : le propriétaire de la base peut la créer
french_unaccent = copie de french, word, hword, hword_part → unaccent, french_stem accepté dans une colonne générée (to_tsvector(regconfig, text) est IMMUTABLE)
Échéance électricité french : 'échéanc' 'électr' ; french_unaccent : 'echeanc' 'electricit'
requêtes echeance, électricite, noel hopital, oeuvre, ca trouvent Échéance, Électricité, Noël à l'hôpital, œuvre, ça
chauffagiste trouve Chauffagistes (racinisation conservée)
été french : mot vide (forme d’« être ») ; french_unaccent : lexème ete
a-faire en-cours main-propre 'a-fair' 'fair' 'en-cour' 'cour' 'main-propr' 'propr'… : un code d’énumération produit des mots courants
UUID, date 2026-10-15 fragments a1b2c3d4, -0000, 2026, -10, -15 : inutilisables
#perso/maison jeton file → lexème perso/maison (maison seul ne le trouve pas)
+++123/4567/89012+++ puis requête 89012 non trouvé (jetons +123, /4567/89012)
ALTER TABLE … ALTER COLUMN search SET EXPRESSION (nouveau en 17) réécrit la table et reconstruit l’index GIN existant
pg_dump -Fc puis pg_restore par le propriétaire extension, configuration et colonne restaurées, recherche sans accent fonctionnelle
taille du tsvector, titre 500 + corps 200 000 + champs 20 000 caractères de mots distincts courts 410 Ko (25 568 lexèmes) ; mots longs : 271 Ko ; limite 1 Mo

Décision

Ce qui est indexé : texte libre des champs, par une règle générique

Un champ d’aspect est cherchable s’il est une propriété de premier niveau du schéma telle que :

  • type vaut exactement "string" ;
  • ni enum ni const ;
  • format absent, ou email, ou uri.

Sont donc exclus : énumérations (codes techniques, bruit : poste, fair, cour), dates et dates-heures, node-ref (UUID), nombres, entiers, booléens, tableaux (y compris multi-sélections d’énumérations), objets. La règle ne dépend que du JSON Schema : aucun nom d’aspect ni de champ dans le code. Sur les exemples de config/ : courrier → expediteur, destinataire ; document → reference, emetteur ; facture → communication ; tache → contexte ; projet → objectif.

Pas de clé YAML au jalon 1 (ni x-searchable, ni poids par champ). La règle par type couvre les exemples. Une clé x-searchable: true | false sur un champ de premier niveau pourra la surcharger plus tard ; elle s’ajoutera sans migration de données, puisque la réconciliation au démarrage (ci-dessous) recalcule tout. Le skill definitions-format indique qu’une clé x-searchable écrite aujourd’hui est sans effet.

Définitions (paquet definitions) :

/** Champs cherchables d'un schéma d'aspect (ADR 0013), dans l'ordre de déclaration des propriétés. */
final class SearchFields {
    static List<String> of(JsonNode schema);
}
  • Definitions gagne Map<String, List<String>> searchFields (même clé que aspects, liste vide si aucun champ), rempli par DefinitionLoader.load à côté de refFields, et repris par DefinitionRegistry.keepPreviousOnError comme refFields.
  • DefinitionRegistry gagne public List<String> searchFields(String aspect) (liste vide si l’aspect est inconnu).
  • Le méta-schéma ne change pas.

Où vit le texte : colonne node.aspect_text écrite par le service

Le texte des champs cherchables de tous les aspects d’un nœud est stocké dans node.aspect_text (text not null default ''). La colonne générée search le lit, puisqu’il est sur la même ligne. NodeService le recalcule à chaque écriture d’aspect, dans la transaction de cette écriture.

Calcul (paquet node) :

/** Texte indexé des champs d'aspects d'un nœud (ADR 0013). */
final class AspectText {
    static final int MAX_LENGTH = 20_000;

    /**
     * @param aspects      données par aspect, telles que stockées (conformes ou non à leur schéma)
     * @param searchFields champs cherchables d'un aspect (liste vide pour un aspect inconnu)
     */
    static String of(Map<String, Map<String, Object>> aspects, Function<String, List<String>> searchFields);
}
  • Aspects dans l’ordre alphabétique de leur nom, champs dans l’ordre de searchFields.
  • Seules les valeurs String sont retenues, après strip() ; les valeurs vides sont ignorées. Une donnée ancienne non conforme (nombre dans un champ texte, par exemple) est ignorée sans erreur : « tolérer à la lecture ».
  • Valeurs séparées par \n, texte normalisé en NFC.
  • Au plus MAX_LENGTH unités UTF-16 au total. La coupe ne sépare pas une paire de substitution. Seul l’index est tronqué, les données restent entières. Avec le corps à 200 000, le tsvector reste sous 410 Ko d’après la sonde, loin de la limite de 1 Mo.
  • Un aspect sans définition chargée ne contribue pas.

Écritures dans NodeService (aucune autre classe n’écrit node_aspect) :

Méthode Changement
create calcule AspectText.of(aspects résolus, definitions::searchFields) et le passe à NodeRepository.insertNode(UUID id, String title, String type, String body, UUID parentId, String aspectText) ; aucun UPDATE de plus
putAspect après l’écriture de l’aspect, calcule le texte sur tous les aspects du nœud (ceux de nodes.aspects(id) déjà lus, avec la nouvelle valeur) ; nodes.touch(id) devient nodes.touch(UUID id, String aspectText), qui écrit updated_at et aspect_text dans le même UPDATE
removeAspect idem, sur les aspects restants
  • touch(UUID id) reste pour les autres usages (tags, fichiers).
  • NodeFileService crée ses nœuds par NodeService.create : rien à changer.
  • Db gagne ASPECT_TEXT = field(name("aspect_text"), String.class).

Pas d’événement pour aspect_text : c’est un index dérivé des données, comme title_key et search. Les modifications de données qui le font changer (aspect.added, aspect.updated, aspect.removed, node.created) sont déjà journalisées dans la même transaction.

Migration V7__recherche_aspects.sql

-- Recherche plein texte (ADR 0013) : champs d'aspects indexés, insensible aux accents, pondération titre A, champs B, corps C.

-- unaccent est une extension « trusted » (PostgreSQL 13+) : le propriétaire de la base peut la créer.
create extension if not exists unaccent schema public;

-- Copie de french où les mots sont désaccentués avant la racinisation ; la même configuration sert aux requêtes.
create text search configuration public.french_unaccent (copy = pg_catalog.french);
alter text search configuration public.french_unaccent
    alter mapping for hword, hword_part, word with public.unaccent, pg_catalog.french_stem;

-- Texte des champs d'aspects cherchables, calculé par Java (AspectText) à chaque écriture d'aspect.
-- Les nœuds existants sont remplis par la réconciliation au démarrage de l'application (la règle dépend de config/).
alter table node add column aspect_text text not null default '';

-- PostgreSQL 17 : réécrit la table (titre et corps réindexés avec la nouvelle configuration) et reconstruit node_search_idx.
-- D réservé au contenu des fichiers (jalon 3).
alter table node alter column search set expression as (
    setweight(to_tsvector('public.french_unaccent'::regconfig, coalesce(title, '')), 'A') ||
    setweight(to_tsvector('public.french_unaccent'::regconfig, aspect_text), 'B') ||
    setweight(to_tsvector('public.french_unaccent'::regconfig, coalesce(body, '')), 'C'));
  • SET EXPRESSION fixe le minimum à PostgreSQL 17, la version de docker-compose.yml et des tests. L’index node_search_idx est conservé et reconstruit.
  • La migration réindexe le titre et le corps de tous les nœuds (réécriture de la table, sous verrou exclusif) : quelques secondes pour quelques milliers de nœuds.
  • La configuration est qualifiée par public. dans la migration comme dans les requêtes : aucune dépendance au search_path.

Rattrapage et changements de config/ : réconciliation au démarrage

Le remplissage ne se fait pas en SQL : la règle dépend des définitions (énumérations, formats), que la base ne voit pas. Une approximation SQL (« toutes les chaînes de premier niveau ») indexerait les codes d’énumération jusqu’à la prochaine écriture de chaque nœud.

/** Recalcule node.aspect_text de tous les nœuds au démarrage, avant l'ouverture du port HTTP (ADR 0013). */
@Component
class AspectTextReconciler implements SmartInitializingSingleton {
    AspectTextReconciler(NodeRepository nodes, DefinitionRegistry definitions, TransactionTemplate tx);

    @Override
    public void afterSingletonsInstantiated();   // appelle reconcile() et journalise le bilan

    /** @return nombre de nœuds dont le texte a été réécrit */
    int reconcile();
}
  • afterSingletonsInstantiated s’exécute après la création de tous les beans (Flyway a migré, les définitions sont chargées), mais avant le démarrage du serveur web : aucune écriture concurrente, donc aucun verrou à prendre.
  • Parcours par pages de 500 nœuds (pagination par id, corbeille comprise, pour qu’une restauration retrouve son index), une transaction par page. Pour chaque nœud : lecture des aspects, AspectText.of, puis UPDATE node SET aspect_text = ? WHERE id = ? seulement si la valeur diffère. updated_at n’est pas touché, aucun événement n’est écrit.
  • Nœuds ignorés : ceux qui portent un aspect dont le fichier est en erreur au démarrage (LoadIssue de type aspects avec un nom). Leur texte est laissé tel quel : une faute de frappe dans un YAML ne vide pas l’index. Un aspect dont le fichier a été supprimé ne contribue plus.
  • Méthodes ajoutées à NodeRepository : List<AspectTextRow> aspectTextPage(UUID after, int size) (record AspectTextRow(UUID id, String aspectText, Map<String, String> aspectsJson), deux requêtes par page) et void updateAspectText(UUID id, String text).
  • Journal : Index de recherche des aspects : {} nœud(s) recalculé(s) sur {}.
  • Le même mécanisme corrige l’index après toute modification de config/ (champ ajouté, champ devenu énumération, aspect supprimé), puisque les définitions ne changent qu’au redémarrage au jalon 1. Le second démarrage n’écrit rien.
  • Coût : une lecture complète de node_aspect à chaque démarrage, de l’ordre de la seconde pour quelques dizaines de milliers de nœuds. Au-delà, on pourra ne recalculer que si une empreinte des champs cherchables a changé (voir Reporté).

Requête et classement

Dans NodeRepository.search :

  • static final String SEARCH_CONFIG = "public.french_unaccent" ; la requête plein texte devient un champ unique réutilisé : websearch_to_tsquery('public.french_unaccent'::regconfig, ?).
  • Le début de titre devient insensible aux accents, pour l’autocomplétion : public.unaccent(title) ILIKE public.unaccent(?) || '%' et public.unaccent(title) ILIKE '% ' || public.unaccent(?) || '%' (saisie échappée comme aujourd’hui avec Tags.likeEscape, \ comme caractère d’échappement). unaccent laisse %, _ et \ intacts. eche trouve donc Échéance dès la quatrième lettre.
  • La clé exacte (title_key = key(q)) reste sensible aux accents : elle désigne le nœud que viserait [[q]], et Été et Ete sont deux titres distincts (ADR 0009).
  • Tri : rang (clé exacte 0, début de titre 1, sinon 2), puis created_at et id pour la clé exacte (inchangé), puis ts_rank(search, requête) desc, puis updated_at desc. Avec les poids par défaut de PostgreSQL (A 1,0 ; B 0,4 ; C 0,2 ; D 0,1), une occurrence dans le titre l’emporte sur une valeur de champ, et une seule valeur de champ (0,24) sur n’importe quel nombre d’occurrences dans le corps (moins de 0,2). Normalisation 0 : pas de pénalité de longueur.
  • Champs et corps échangent leur poids par rapport à V1 (B → C pour le corps) : sans ts_rank jusqu’ici, ce changement n’a aucun effet visible antérieur. Une valeur de champ (expéditeur, référence, émetteur) est courte et précise, un corps est long. D est réservé au contenu des fichiers (Tika, jalon 3), plus long et plus bruité encore.
  • Une saisie réduite à des mots vides donne une requête vide (NOTICE PostgreSQL), sans correspondance plein texte : comportement inchangé.

API

  • GET /api/nodes?q=… : paramètres et réponse inchangés. Seule la description @Operation change : « plein texte sur le titre, les champs de texte des aspects et le contenu, en français, sans tenir compte des accents ». Le client est régénéré (skill api-contract) pour la seule documentation.
  • Pas de score, d’extrait ni de champ « trouvé dans » dans NodeSummary.

Front

  • Aucun changement de logique : la liste et toutes les autocomplétions profitent de la recherche étendue. Les correspondances par champ arrivent après les titres, grâce au classement.
  • Liste des nœuds : mat-hint sous le champ « Rechercher » : « Titre, contenu et champs ; accents ignorés ».

Tests

Unitaires :

  • SearchFieldsTest (paquet definitions) : string retenu ; enum, const, format: date | date-time | node-ref exclus ; email et uri retenus ; number, integer, boolean, array, object et type: [string, null] exclus ; ordre des propriétés conservé.
  • AspectTextTest : ordre (aspects triés, champs dans l’ordre), strip, valeurs vides ignorées, valeur non String ignorée, aspect inconnu ignoré, NFC, coupe à 20 000 sans séparer une paire de substitution.
  • ExampleDefinitionsTest : searchFields des cinq aspects d’exemple (liste ci-dessus).
  • DefinitionRegistryTest : un aspect devenu invalide au rechargement garde ses searchFields précédents.

Intégration (Testcontainers, postgres:17-alpine) :

  • NodeSearchIT (nouveau) :
    • facture-fournisseur avec courrier.expediteur = "Électricité de Strasbourg" : trouvé par electricite, ÉLECTRICITÉ et strasbourg ;
    • courrier.canal = poste (valeur par défaut) : non trouvé par poste ; UUID d’un tache.projet et date d’echeance : non trouvés ;
    • putAspect qui change la valeur : l’ancienne ne trouve plus le nœud, la nouvelle si ; removeAspect : la valeur ne le trouve plus ;
    • accents dans le titre et le corps ; eche place Échéance en tête (début de titre) ;
    • classement : trois nœuds contenant le même mot, dans le titre (au milieu), dans un champ, dans le corps (plusieurs fois) → titre, champ, corps ;
    • putAspect identique : ni événement ni changement d’updated_at (inchangé) ; un putAspect réel n’ajoute aucun événement en plus d’aspect.updated ;
    • champ texte de 30 000 caractères accepté (201) et trouvé par un mot de son début ;
    • AspectTextReconciler.reconcile() après UPDATE node SET aspect_text = '' : nœud de nouveau trouvé, updated_at et historique inchangés ; un second appel renvoie 0.
  • SearchMigrationIT (sur le modèle de TitleKeyMigrationIT) : migration jusqu’à 6, insertion de nœuds au titre et au corps accentués, migration complète, puis search @@ websearch_to_tsquery('public.french_unaccent', 'echeance') vrai et aspect_text = ''.
  • Les tests existants de recherche (NodeServiceIT, TagFilterIT, NodeBodyLimitIT) restent verts sans modification. Dans searchMatchesTitlePrefixAndWordStartPrefixFirst, les deux titres du rang 2 ne correspondent pas en plein texte (ts_rank nul) : leur ordre par updated_at est conservé.

Découpage

  1. Backend (feat(search): …) : V7, SearchFields et Definitions.searchFields, AspectText, écritures de NodeService, requête et classement, AspectTextReconciler, description OpenAPI, tests ci-dessus ; skill definitions-format (ligne « Recherche plein texte » de l’état d’implémentation : règle des champs cherchables, absence de x-searchable) ; backlog.
  2. Front (feat(nodes): …) : client régénéré, indication sous le champ de recherche.

Alternatives écartées

  • Trigger sur node_aspect qui met à jour node.aspect_text : toujours à jour, même pour une écriture SQL directe. Mais un trigger ne voit pas config/ : il indexerait les codes d’énumération (poste sur tous les courriers, fair, cour) et ne pourrait ni filtrer les formats ni accueillir un futur x-searchable. Il cache aussi une écriture sur node dans une écriture sur node_aspect, à rebours de jOOQ explicite (ADR 0003) et du précédent de title_key.
  • Colonne générée sur node_aspect (search par aspect, index GIN) et requête OR id IN (…) : pas de recalcul sur tous les aspects du nœud, mais deux index et un classement à recomposer entre deux tsvector. Le choix des champs exige toujours Java, ou une fonction SQL qui ignore config/.
  • Table dénormalisée node_search : une table de plus à garder cohérente, sans gain par rapport à une colonne de node.
  • tsvector calculé en Java : PostgreSQL racinise et désaccentue mieux et de façon identique pour les documents et les requêtes.
  • Remplissage SQL approximatif dans la migration : règle divergente de Java jusqu’à la réécriture de chaque nœud ; la réconciliation est de toute façon nécessaire après un changement de config/.
  • Réconciliation en ApplicationRunner : elle s’exécute après l’ouverture du port, en concurrence avec les écritures, et demanderait verrous et gardes.
  • Indexer aussi les énumérations, nombres et dates : bruit (codes kebab-case, valeurs par défaut identiques sur tous les nœuds, fragments de dates et d’UUID) pour un besoin mieux servi par les filtres et les vues (jalon 2) et les facettes (V2).
  • x-searchable dès le jalon 1 : clé, méta-schéma, documentation et tests pour un besoin que les exemples n’ont pas. L’ajout ultérieur est purement additif.
  • Titre de la cible d’un node-ref dans le texte : dénormalisation à propager à chaque renommage de la cible (comme les liens wiki, ADR 0009), alors que les rétroliens « Référencé par » existent déjà.
  • Tags dans node.search : les tags ont leur filtre hiérarchique (tag=, ADR 0006) et leur autocomplétion. Les mêler au plein texte demanderait de recalculer le texte à chaque écriture de tags, et un tag hiérarchique devient un jeton file (perso/maison) que maison ne trouve pas.
  • Fonction immutable_unaccent() dans l’expression générée : contourne le caractère STABLE d’unaccent() par une déclaration mensongère, fragile à la restauration. La configuration de recherche est la voie documentée et passe pg_dump/pg_restore (sonde).
  • Garder french sans unaccent : echeance ne trouverait pas Échéance, contrairement au cahier.
  • DROP COLUMN puis ADD COLUMN search au lieu de SET EXPRESSION : compatible avec PostgreSQL 16, mais il faut redéclarer l’index. Le projet est sur la 17.
  • pg_trgm (recherche floue, sous-chaînes comme 89012) : utile au saut rapide (Ctrl+O, « en flou ») ; à décider avec cette tranche, pas ici.

Conséquences

  • Les valeurs de texte libre des aspects sont trouvées par la liste et par toutes les autocomplétions. Une autocomplétion peut donc proposer un nœud dont seul un champ correspond, après les correspondances de titre.
  • Recherche insensible aux accents partout, titre compris. Effet de bord de unaccent avant la liste de mots vides, qui est accentuée : été, à, où… ne sont plus des mots vides. été devient cherchable (la saison), au prix d’un peu de bruit.
  • La racinisation se fait sur les mots désaccentués : créée et créer ne donnent plus le même lexème (cree et cre). Documents et requêtes passent par la même chaîne : la cohérence est garantie, mais la racinisation est un peu moins fine sur certaines formes accentuées.
  • Ordre des résultats différent après la clé exacte et le début de titre : la pertinence (ts_rank) passe avant la date de modification.
  • PostgreSQL 17 au minimum, et l’extension unaccent dans public : le rôle applicatif doit être propriétaire de la base (c’est le cas avec docker-compose.yml). Une base gérée par un tiers doit autoriser les extensions trusted.
  • Le démarrage lit tous les aspects ; il réécrit seulement ce qui a changé. Après une modification de config/, l’index est juste dès le redémarrage, sans commande.
  • Entre deux démarrages, une écriture sur un nœud qui porte un aspect en erreur recalcule son texte sans cet aspect. Le démarrage suivant, une fois le fichier corrigé, le rétablit.
  • Le parcours séquentiel de node (déjà imposé par les ILIKE '%…') lit désormais un tsvector un peu plus gros par nœud, et unaccent(title) est calculé ligne à ligne. C’est sans conséquence au volume d’un usage personnel. Au-delà de quelques dizaines de milliers de nœuds, il faudra séparer la partie indexée (GIN) de la partie titre (pg_trgm).
  • Le texte des champs au-delà de 20 000 caractères n’est pas cherchable ; les données restent entières.
  • Les sauvegardes (tranche dédiée) restaurent l’extension et la configuration avec pg_dump/pg_restore (vérifié par la sonde). La procédure de restauration testée doit inclure une recherche sans accent.
  • La réindexation des corps (backlog, ADR 0009 et 0011) est indépendante : node.search est recalculé par la base.

Reporté

  • x-searchable (surcharge par champ) et pondération par champ ; libellés d’énumération (x-enumLabels), qui rendraient les énumérations cherchables par leur libellé.
  • Titre des cibles node-ref dans l’index ; tags dans le plein texte, ou en facette (V2).
  • Extrait et surlignage (ts_headline, coûteux sur un corps de 200 000 caractères) ; score dans NodeSummary.
  • Recherche floue et par sous-chaîne (pg_trgm), recherche par préfixe dans le corps et les champs : avec le saut rapide (Ctrl+O) ou la palette (Ctrl+K).
  • Contenu des fichiers (Tika, jalon 3), au poids D ; OCR (V2).
  • Rechargement à chaud des définitions (jalon 3) : il devra déclencher AspectTextReconciler.reconcile() en arrière-plan, avec verrouillage des nœuds, puisque le port sera alors ouvert.
  • Empreinte des champs cherchables, pour sauter la réconciliation au démarrage quand rien n’a changé, si le volume le justifie.

Questions ouvertes tranchées (mode autonome)

  • Champs indexés : texte libre seulement (string sans enum ni const, format absent, email ou uri) ; énumérations, dates, nombres, booléens et node-ref exclus.
  • Clé YAML d’exclusion ou de pondération : aucune au jalon 1 ; x-searchable reporté.
  • Mécanisme : colonne node.aspect_text écrite par NodeService dans la transaction, lue par la colonne générée ; ni trigger ni table.
  • Pondération : titre A, champs B, corps C, D réservé aux fichiers ; classement par ts_rank après les rangs existants.
  • Accents : unaccent par une configuration public.french_unaccent, appliquée au titre, au corps, aux champs et au début de titre ; clé exacte des liens wiki inchangée.
  • Rattrapage : réconciliation Java au démarrage, avant l’ouverture du port, plutôt qu’un remplissage SQL dans la migration.
  • Tags : hors recherche plein texte.
  • Limite : 20 000 caractères de texte de champs par nœud.
  • Extrait et surlignage : reportés.
  • Front : une indication sous le champ de recherche, rien d’autre.