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)
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 parnode_search_idx(GIN). Une colonne générée ne peut lire que les colonnes de sa propre ligne : elle ne voit pasnode_aspect.NodeRepository.searchcombinesearch @@ websearch_to_tsquery('french', q)avec trois conditions sur le titre :ILIKE 'q%',ILIKE '% q%'ettitle_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, puisupdated_at desc. Aucunts_rank: les poids A et B n’ont aujourd’hui aucun effet. LesILIKE '%…'imposent déjà un parcours séquentiel denode.GET /api/nodes?q=sert la liste des nœuds (200 résultats) et toutes les autocomplétions : sélecteurnode-ref, panneau « Liens », déplacement, suggestion[[(10 résultats).node_aspect.dataest un JSONB par aspect, validé à l’écriture contre le JSON Schema de l’aspect. Les seuls chemins d’écriture sontNodeService.create,putAspectetremoveAspect.- Le corps est borné à 200 000 caractères (ADR 0008), parce qu’un
tsvectorest 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/fieldn’a pasadditionalProperties: false) ; seulx-targetest 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 :
typevaut exactement"string";- ni
enumniconst; formatabsent, ouemail, ouuri.
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);
}
DefinitionsgagneMap<String, List<String>> searchFields(même clé queaspects, liste vide si aucun champ), rempli parDefinitionLoader.loadà côté derefFields, et repris parDefinitionRegistry.keepPreviousOnErrorcommerefFields.DefinitionRegistrygagnepublic 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
Stringsont retenues, aprèsstrip(); 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_LENGTHunité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, letsvectorreste 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).NodeFileServicecrée ses nœuds parNodeService.create: rien à changer.DbgagneASPECT_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 EXPRESSIONfixe le minimum à PostgreSQL 17, la version dedocker-compose.ymlet des tests. L’indexnode_search_idxest 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 ausearch_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();
}
afterSingletonsInstantiateds’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, puisUPDATE node SET aspect_text = ? WHERE id = ?seulement si la valeur diffère.updated_atn’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 (
LoadIssuede typeaspectsavec 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) etvoid 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(?) || '%'etpublic.unaccent(title) ILIKE '% ' || public.unaccent(?) || '%'(saisie échappée comme aujourd’hui avecTags.likeEscape,\comme caractère d’échappement).unaccentlaisse%,_et\intacts.echetrouve doncÉchéancedè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éetEtesont deux titres distincts (ADR 0009). - Tri : rang (clé exacte 0, début de titre 1, sinon 2), puis
created_atetidpour la clé exacte (inchangé), puists_rank(search, requête) desc, puisupdated_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_rankjusqu’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 (
NOTICEPostgreSQL), sans correspondance plein texte : comportement inchangé.
API
GET /api/nodes?q=…: paramètres et réponse inchangés. Seule la description@Operationchange : « 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é (skillapi-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-hintsous le champ « Rechercher » : « Titre, contenu et champs ; accents ignorés ».
Tests
Unitaires :
SearchFieldsTest(paquetdefinitions) :stringretenu ;enum,const,format: date | date-time | node-refexclus ;emaileturiretenus ;number,integer,boolean,array,objectettype: [string, null]exclus ; ordre des propriétés conservé.AspectTextTest: ordre (aspects triés, champs dans l’ordre),strip, valeurs vides ignorées, valeur nonStringignorée, aspect inconnu ignoré, NFC, coupe à 20 000 sans séparer une paire de substitution.ExampleDefinitionsTest:searchFieldsdes cinq aspects d’exemple (liste ci-dessus).DefinitionRegistryTest: un aspect devenu invalide au rechargement garde sessearchFieldsprécédents.
Intégration (Testcontainers, postgres:17-alpine) :
NodeSearchIT(nouveau) :facture-fournisseuraveccourrier.expediteur = "Électricité de Strasbourg": trouvé parelectricite,ÉLECTRICITÉetstrasbourg;courrier.canal = poste(valeur par défaut) : non trouvé parposte; UUID d’untache.projetet date d’echeance: non trouvés ;putAspectqui 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 ;
echeplaceÉchéanceen 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 ;
putAspectidentique : ni événement ni changement d’updated_at(inchangé) ; unputAspectré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èsUPDATE node SET aspect_text = '': nœud de nouveau trouvé,updated_atet historique inchangés ; un second appel renvoie 0.
SearchMigrationIT(sur le modèle deTitleKeyMigrationIT) : migration jusqu’à6, insertion de nœuds au titre et au corps accentués, migration complète, puissearch @@ websearch_to_tsquery('public.french_unaccent', 'echeance')vrai etaspect_text = ''.- Les tests existants de recherche (
NodeServiceIT,TagFilterIT,NodeBodyLimitIT) restent verts sans modification. DanssearchMatchesTitlePrefixAndWordStartPrefixFirst, les deux titres du rang 2 ne correspondent pas en plein texte (ts_ranknul) : leur ordre parupdated_atest conservé.
Découpage
- Backend (
feat(search): …) : V7,SearchFieldsetDefinitions.searchFields,AspectText, écritures deNodeService, requête et classement,AspectTextReconciler, description OpenAPI, tests ci-dessus ; skilldefinitions-format(ligne « Recherche plein texte » de l’état d’implémentation : règle des champs cherchables, absence dex-searchable) ; backlog. - Front (
feat(nodes): …) : client régénéré, indication sous le champ de recherche.
Alternatives écartées
- Trigger sur
node_aspectqui met à journode.aspect_text: toujours à jour, même pour une écriture SQL directe. Mais un trigger ne voit pasconfig/: il indexerait les codes d’énumération (postesur tous les courriers,fair,cour) et ne pourrait ni filtrer les formats ni accueillir un futurx-searchable. Il cache aussi une écriture surnodedans une écriture surnode_aspect, à rebours de jOOQ explicite (ADR 0003) et du précédent detitle_key. - Colonne générée sur
node_aspect(searchpar aspect, index GIN) et requêteOR id IN (…): pas de recalcul sur tous les aspects du nœud, mais deux index et un classement à recomposer entre deuxtsvector. Le choix des champs exige toujours Java, ou une fonction SQL qui ignoreconfig/. - Table dénormalisée
node_search: une table de plus à garder cohérente, sans gain par rapport à une colonne denode. tsvectorcalculé 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-searchabledè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-refdans 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 jetonfile(perso/maison) quemaisonne trouve pas. - Fonction
immutable_unaccent()dans l’expression générée : contourne le caractèreSTABLEd’unaccent()par une déclaration mensongère, fragile à la restauration. La configuration de recherche est la voie documentée et passepg_dump/pg_restore(sonde). - Garder
frenchsansunaccent:echeancene trouverait pasÉchéance, contrairement au cahier. DROP COLUMNpuisADD COLUMN searchau lieu deSET 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 comme89012) : 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
unaccentavant 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ééeetcréerne donnent plus le même lexème (creeetcre). 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
unaccentdanspublic: le rôle applicatif doit être propriétaire de la base (c’est le cas avecdocker-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 lesILIKE '%…') lit désormais untsvectorun peu plus gros par nœud, etunaccent(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.searchest 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-refdans 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 dansNodeSummary. - 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 (
stringsansenumniconst,formatabsent,emailouuri) ; énumérations, dates, nombres, booléens etnode-refexclus. - Clé YAML d’exclusion ou de pondération : aucune au jalon 1 ;
x-searchablereporté. - Mécanisme : colonne
node.aspect_textécrite parNodeServicedans 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_rankaprès les rangs existants. - Accents :
unaccentpar une configurationpublic.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.