Un client modifie le titre d’un ticket : la liste affiche bien le nouveau titre, mais la recherche hors ligne trouve encore le ticket avec les anciens termes. Ce type de défaut passe facilement inaperçu lorsqu’un studio change de Mac cloud pour générer ses builds : le projet compile, la base de données s’ouvre, mais l’index de recherche plein texte n’est plus synchronisé avec la table métier. Pour valider le comportement de recherche avant livraison, commencez par un jeu de données fixe. Les données de production peuvent compléter cette vérification, mais elles ne doivent pas en être le seul critère : les résultats sont difficiles à reproduire de façon stable.
Vérifier la version de SQLite réellement utilisée
Dans le terminal du Mac cloud, exécutez sqlite3 :memory: 'SELECT sqlite_version();' et consignez la version utilisée pour le test. Essayez ensuite de créer une table virtuelle FTS5. Si la création échoue, arrêtez-vous : il ne faut pas confondre une fonctionnalité indisponible avec un problème de segmentation qui ne renverrait aucun résultat.
L’outil en ligne de commande ne valide que l’environnement de test. Le projet iOS peut être lié à la bibliothèque système ou embarquer sa propre version de SQLite. Il faut donc aussi exécuter la même instruction de création de table via la connexion réellement utilisée par l’application. Intégrez cette vérification à la cible de tests et exécutez-la sur le système visé. Les exemples en SQL pur permettent d’itérer rapidement sur Mac, mais ne remplacent pas les résultats obtenus sur l’appareil mobile.
Définir la segmentation et la sémantique des requêtes
Choisissez d’abord entre une « recherche d’expression littérale » et une « syntaxe de recherche avancée ». La première convient à un champ de recherche de tickets classique ; la seconde, à un outil qui prend explicitement en charge les expressions booléennes. Ne mélangez pas ces deux comportements dans un même champ sans indication pour l’utilisateur.
unicode61 remove_diacritics 2 gère les accents courants des caractères latins, mais ce n’est pas un outil généraliste de segmentation du chinois. Le jeu de tests doit couvrir au minimum la casse, les accents, les traits d’union, les guillemets et les suites de caractères chinois sans espaces. La réussite des tests en anglais ne permet pas d’affirmer que n’importe quel mot chinois peut être recherché.
La liaison des paramètres SQL et l’échappement de MATCH sont deux mécanismes distincts. La liaison empêche la saisie de devenir un élément de la structure SQL, mais ne désactive pas la syntaxe de requête propre à MATCH. Pour construire une expression littérale, vous pouvez doubler les guillemets doubles internes, puis entourer le tout d’une paire de guillemets doubles :
func literalFTSQuery(_ text: String) -> String? {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return "\"" + trimmed.replacingOccurrences(of: "\"", with: "\"\"") + "\""
}
Liez la valeur renvoyée au paramètre de WHERE ticket_fts MATCH ?, sans construire le SQL par concaténation. Ici, « littéral » implique toujours une segmentation en termes : il ne s’agit pas d’une recherche de sous-chaîne octet par octet. Pour une saisie vide, renvoyez directement un résultat vide ou la liste par défaut prévue par le produit.
Les règles de correspondance font partie du contrat fonctionnel du produit. Préfixes, expressions et sous-chaînes ne sont pas interchangeables. Masquer une erreur de syntaxe pour la faire passer pour une absence de résultats ne l’est pas davantage.
Synchroniser l’index à contenu externe avec des déclencheurs
Une table FTS5 à contenu externe convient aux projets qui disposent déjà d’une table métier et ne souhaitent pas stocker le texte en double. En revanche, elle ne convient pas à une implémentation qui compterait sur FTS5 pour surveiller automatiquement les modifications de cette table. content ne fait que déclarer la relation : les ajouts, suppressions et modifications doivent toujours être synchronisés.
L’exemple ci-dessous s’exécute directement dans le terminal. Toutes les données restent en mémoire, et l’échec d’une seule assertion suffit à faire terminer la commande avec un code de sortie non nul :
sqlite3 -bail :memory: <<'SQL'
CREATE TABLE ticket(id INTEGER PRIMARY KEY, title TEXT NOT NULL);
CREATE VIRTUAL TABLE ticket_fts USING fts5(
title,
content='ticket',
content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
CREATE TRIGGER ticket_ai AFTER INSERT ON ticket BEGIN
INSERT INTO ticket_fts(rowid, title) VALUES(new.id, new.title);
END;
CREATE TRIGGER ticket_ad AFTER DELETE ON ticket BEGIN
INSERT INTO ticket_fts(ticket_fts, rowid, title)
VALUES('delete', old.id, old.title);
END;
CREATE TRIGGER ticket_au AFTER UPDATE ON ticket BEGIN
INSERT INTO ticket_fts(ticket_fts, rowid, title)
VALUES('delete', old.id, old.title);
INSERT INTO ticket_fts(rowid, title) VALUES(new.id, new.title);
END;
CREATE TEMP TABLE assertion(ok INTEGER NOT NULL CHECK(ok = 1));
INSERT INTO ticket VALUES(1, 'Café delivery');
INSERT INTO assertion
SELECT count(*) = 1 FROM ticket_fts
WHERE ticket_fts MATCH '"cafe"';
UPDATE ticket SET title = 'Invoice approved' WHERE id = 1;
INSERT INTO assertion
SELECT count(*) = 0 FROM ticket_fts
WHERE ticket_fts MATCH '"cafe"';
INSERT INTO assertion
SELECT count(*) = 1 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
BEGIN;
DELETE FROM ticket WHERE id = 1;
INSERT INTO assertion
SELECT count(*) = 0 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
ROLLBACK;
INSERT INTO assertion
SELECT count(*) = 1 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
DELETE FROM ticket WHERE id = 1;
INSERT INTO assertion
SELECT count(*) = 0 FROM ticket_fts
WHERE ticket_fts MATCH '"invoice"';
SQL
La suppression d’une entrée de l’index exige de fournir l’ancien texte. Le déclencheur de mise à jour supprime donc d’abord l’ancienne entrée avant d’insérer la nouvelle. La modification métier et les déclencheurs s’exécutent dans la même transaction : après une annulation, la table métier et l’index doivent tous deux retrouver leur état précédent. Si l’exemple ne produit aucun message d’erreur, le test est réussi ; aucune vérification manuelle de tableaux dans le terminal n’est nécessaire.
Valider aussi les données historiques et le tri
Lorsqu’un index est ajouté à une base dont la table métier existe déjà, la mise en place des déclencheurs ne suffit pas à indexer automatiquement les enregistrements historiques. Pendant la migration, exécutez INSERT INTO ticket_fts(ticket_fts) VALUES('rebuild'); dans un cadre où les écritures sont maîtrisées, puis vérifiez les correspondances à l’aide d’enregistrements fixes présents avant la migration. Cette reconstruction relève de la migration : elle ne doit pas être lancée à chaque ouverture de la page de recherche.
N’utilisez pas SELECT count(*) FROM ticket_fts pour déterminer si l’index est complet : en l’absence de MATCH, une table à contenu externe peut lire directement la table de contenu. Il faut des assertions fondées sur de vraies requêtes, qui vérifient séparément que les anciens termes ne trouvent plus l’enregistrement et que les nouveaux le trouvent.
À score égal, ajoutez un critère de tri stable fondé sur la clé primaire métier, par exemple ORDER BY rank, rowid. Le jeu de tests de pagination doit inclure plusieurs résultats de même score. Sinon, un changement d’ordre entre deux exécutions risque d’être interprété à tort comme une perte de données.
Transformer les échecs en points de contrôle avant livraison
Pour chaque livraison, conservez la version de SQLite, les paramètres de création des tables, la version du jeu de données et le nom des cas de test en échec, sans enregistrer les véritables recherches des clients. Ajoutez des cas couvrant une chaîne vide, une saisie composée uniquement de ponctuation, une saisie trop longue et des guillemets doubles. Toute saisie dépassant la longueur maximale doit être explicitement refusée selon les règles du produit, sans compter sur une erreur de la base de données pour la rejeter.
Lors de la validation, vérifiez point par point que les bases nouvellement créées et les bases migrées donnent les mêmes résultats, que les anciens termes ne correspondent plus après un changement de titre, qu’aucune entrée ne subsiste après une suppression, qu’une annulation de transaction rétablit les résultats et que le comportement en chinois respecte la documentation. Enfin, exécutez le même jeu de données dans la cible de tests iOS. Ainsi, lors d’un changement de Mac cloud ou de la reprise d’un projet client, vous vérifiez le comportement de la recherche lui-même, et non un index resté correct par hasard sur une machine donnée.
Questions fréquentes
FTS5 fonctionne dans sqlite3 sur Mac : est-ce suffisant pour iOS ?
Non. L’application peut utiliser une autre bibliothèque SQLite. Créez une table FTS5 temporaire via sa connexion réelle et exécutez les mêmes jeux de données sur le système cible.
unicode61 découpe-t-il les phrases chinoises en mots ?
Il ne fournit pas de segmentation générale des mots chinois. Testez des textes réels et, si cette recherche est nécessaire, utilisez une segmentation supplémentaire identique pour l’index et les requêtes.
Adaptez la durée de location au calendrier de votre projet
Idéal pour les builds ponctuels, les pics d’activité avant livraison et les projets menés pour plusieurs clients. Si votre équipe compile à plein régime toute l’année et possède déjà son matériel, comparez d’abord l’achat et la location avant de choisir.
VMSSD Mac cloud propose des Mac mini physiques dédiés, et non des machines virtuelles. Choisissez une durée à la journée, à la semaine, au mois ou au trimestre. La location s’arrête à l’échéance sans renouvellement ; les modèles et les nœuds disponibles sont indiqués en temps réel dans la console.