Files
home_stock/docs/adr/0003-recherche-full-text-sqlite-fts5.md
2026-01-28 19:22:30 +01:00

260 lines
9.5 KiB
Markdown

# ADR-0003 — Recherche full-text avec SQLite FTS5
- Statut : accepted
- Date : 2026-01-27
---
## Contexte
HomeStock nécessite une fonctionnalité de recherche rapide et efficace pour retrouver des objets dans l'inventaire. Les utilisateurs doivent pouvoir rechercher par :
- Nom de l'objet (ex: "perceuse")
- Description (ex: "outil électrique sans fil")
- Catégorie (ex: "bricolage")
- Localisation (ex: "garage", "étagère A")
**Contraintes identifiées** :
- **Volume de données** : ~1000-5000 items attendus initialement, maximum 10000 items à long terme
- **Performance** : Résultats de recherche en <200ms souhaités
- **Simplicité** : Pas de serveur externe supplémentaire à gérer
- **Pertinence** : Recherche "fuzzy" pas nécessaire, recherche exacte par mots-clés suffit
- **Langue** : Recherche en français uniquement
Le choix de la solution de recherche impacte directement l'expérience utilisateur (fonctionnalité critique) et l'architecture technique (composant additionnel ou intégré).
---
## Décision
Nous utilisons **SQLite FTS5 (Full-Text Search 5)** pour la recherche full-text avec les caractéristiques suivantes :
### Configuration
- **Table virtuelle FTS5** : `fts_items` contenant les champs indexés
- **Champs indexés** : `Item.name`, `Item.description`, `Category.name`, `Location.path`
- **Tokenizer** : `unicode61` (support Unicode, casse insensible)
- **Synchronisation** : Triggers SQLite pour maintenir FTS5 à jour lors des INSERT/UPDATE/DELETE
### Exemple de structure
```sql
CREATE VIRTUAL TABLE fts_items USING fts5(
item_id UNINDEXED,
name,
description,
category_name,
location_path,
tokenize = 'unicode61'
);
-- Triggers pour synchronisation automatique
CREATE TRIGGER items_after_insert AFTER INSERT ON items BEGIN
INSERT INTO fts_items(item_id, name, description, category_name, location_path)
VALUES (
NEW.id,
NEW.name,
NEW.description,
(SELECT name FROM categories WHERE id = NEW.category_id),
(SELECT path FROM locations WHERE id = NEW.location_id)
);
END;
-- Triggers similaires pour UPDATE et DELETE
```
### Requêtes de recherche
```sql
-- Recherche simple
SELECT items.* FROM items
JOIN fts_items ON items.id = fts_items.item_id
WHERE fts_items MATCH 'perceuse';
-- Recherche avec boost (priorité sur le nom)
SELECT items.* FROM items
JOIN fts_items ON items.id = fts_items.item_id
WHERE fts_items MATCH '{name}: perceuse OR {description}: perceuse'
ORDER BY rank;
```
---
## Alternatives considérées
### 1. Elasticsearch
**Description** : Moteur de recherche distribué dédié, standard de l'industrie
**Avantages** :
- ✅ Recherche extrêmement performante et flexible
- ✅ Recherche fuzzy, phonétique, synonymes
- ✅ Agrégations et analytics avancées
- ✅ Scalable horizontalement
**Inconvénients** :
- ❌ Serveur Java lourd (1-2GB RAM minimum)
- ❌ Complexité opérationnelle importante
- ❌ Nécessite synchronisation BDD → Elasticsearch
- ❌ Overhead massif pour 1000-10000 items
- ❌ Configuration complexe pour français
**Verdict** : ❌ **Rejeté** - Tuer une mouche avec un bazooka. Elasticsearch est conçu pour des millions de documents et des cas d'usage complexes (facettes, agrégations, etc.) dont HomeStock n'a pas besoin.
### 2. MeiliSearch
**Description** : Moteur de recherche moderne, léger, orienté UX
**Avantages** :
- ✅ Très rapide et pertinent out-of-the-box
- ✅ API REST simple
- ✅ Typo-tolerance native
- ✅ Plus léger qu'Elasticsearch (~50MB RAM)
- ✅ Configuration minimale
**Inconvénients** :
- ❌ Service externe supplémentaire à déployer
- ❌ Synchronisation BDD → MeiliSearch nécessaire
- ❌ Overhead pour petit volume de données
- ❌ Dépendance additionnelle à maintenir
**Verdict** : ⚠️ **Rejeté mais intéressant** - Excellente solution technique mais ajoute de la complexité pour un bénéfice limité sur <10k items. À considérer si passage à >50k items.
### 3. PostgreSQL Full-Text Search (pg_trgm + GIN)
**Description** : Recherche full-text native de PostgreSQL
**Avantages** :
- ✅ Très performant avec index GIN
- ✅ Recherche trigram (similitude)
- ✅ Support langues naturelles (stemming français)
- ✅ Pas de service externe
**Inconvénients** :
- ❌ Nécessite PostgreSQL (vs SQLite choisi dans ADR-0001)
- ❌ Serveur BDD à gérer
- ❌ Complexité setup (dictionnaires français, configuration)
**Verdict** : ❌ **Rejeté** - Excellente solution technique mais nécessite PostgreSQL. Si migration vers PostgreSQL (cas multi-utilisateurs), reconsidérer.
### 4. LIKE '%keyword%' en SQL
**Description** : Recherche basique avec opérateur LIKE
**Avantages** :
- ✅ Extrêmement simple
- ✅ Aucune dépendance
**Inconvénients** :
- ❌ Performance catastrophique (full table scan)
- ❌ Pas de pertinence/ranking
- ❌ Impossible de rechercher sur plusieurs champs efficacement
- ❌ Pas d'opérateurs (AND, OR, NOT)
**Verdict** : ❌ **Rejeté** - Inacceptable même pour 1000 items, expérience utilisateur dégradée
### 5. SQLite FTS5 (notre choix)
**Description** : Module de recherche full-text intégré à SQLite
**Avantages** :
- ✅ Intégré à SQLite, pas de service externe
- ✅ Performance excellente pour <100k documents
- ✅ Support opérateurs (AND, OR, NOT, NEAR, phrases)
- ✅ Ranking par pertinence (BM25)
- ✅ Triggers pour synchronisation automatique
- ✅ Tokenizer Unicode (support français)
- ✅ Footprint mémoire minimal
**Inconvénients** :
- ⚠️ Pas de recherche fuzzy native (typos)
- ⚠️ Pas de stemming français parfait
- ⚠️ Limitations si >100k documents (acceptable pour notre cas)
**Verdict** : ✅ **Choisi** - Solution optimale pour notre contexte : performance suffisante, simplicité maximale, pas de dépendance externe
---
## Conséquences
### Positives
1. **Simplicité architecture** : Pas de service externe, tout dans SQLite
2. **Performance suffisante** : FTS5 recherche <50ms sur 10k items
3. **Synchronisation automatique** : Triggers maintiennent l'index à jour
4. **Zéro configuration** : FTS5 inclus dans SQLite depuis version 3.9.0 (2015)
5. **Backup simplifié** : Index FTS5 dans le même fichier .db que les données
6. **Opérateurs avancés** : Support AND, OR, NOT, recherche par phrase
7. **Ranking pertinent** : Algorithme BM25 pour trier par pertinence
### Négatives
1. **Pas de typo-tolerance** : "perceuze" ne trouvera pas "perceuse"
2. **Stemming limité** : "perçant" et "percer" non reliés automatiquement
3. **Performance dégradée >100k items** : Limite théorique (très au-delà de notre besoin)
4. **Pas d'agrégations** : Impossible de faire des facettes de recherche complexes
5. **Recherche française limitée** : Pas de dictionnaire français sophistiqué
### Mitigations
- **Typo-tolerance** : Implémenter suggestions de correction côté application si besoin (Levenshtein distance)
- **Stemming** : Ajouter mots-clés/tags aux items pour améliorer découvrabilité
- **Performance** : Si >100k items, migrer vers PostgreSQL pg_trgm ou MeiliSearch
- **Agrégations** : Implémenter filtres côté application (par catégorie, localisation, etc.)
---
## Impacts techniques
### Développement
- **Migration Alembic** : Créer table virtuelle FTS5 + triggers lors du setup initial
- **Service de recherche** : `search_service.py` encapsule la logique FTS5
- **API endpoint** : `GET /api/v1/search?q=keyword` retourne items matchant
### Performance attendue
- **<1000 items** : <10ms
- **1000-5000 items** : <50ms
- **5000-10000 items** : <150ms
- **>10000 items** : Dégrédation possible, monitoring nécessaire
### Structure de code
```python
# services/search_service.py
class SearchService:
async def search_items(self, query: str, limit: int = 50):
"""Recherche full-text avec FTS5"""
# Sanitize query (échapper caractères spéciaux)
safe_query = self._escape_fts5_query(query)
# Query FTS5 avec ranking
sql = """
SELECT items.*,
rank AS relevance
FROM items
JOIN fts_items ON items.id = fts_items.item_id
WHERE fts_items MATCH :query
ORDER BY rank
LIMIT :limit
"""
return await self.db.execute(sql, {"query": safe_query, "limit": limit})
```
### Évolution future
Si les limitations deviennent bloquantes :
1. **Migration PostgreSQL** : Activer pg_trgm pour recherche similitude
2. **Ajout MeiliSearch** : Service séparé pour recherche avancée (garder SQLite pour données)
3. **Hybrid search** : Combiner FTS5 (rapide) + recherche sémantique externe (typos, synonymes)
---
## Notes
SQLite FTS5 est utilisé par de nombreux projets à succès pour des cas d'usage similaires :
- **Firefox** : Recherche dans l'historique et favoris
- **Apple Mail** : Indexation locale des emails
- **VS Code** : Recherche dans l'historique de commandes
Pour HomeStock avec <10k items attendus, FTS5 est largement suffisant et offre le meilleur ratio performance/simplicité.
La décision de ne pas utiliser Elasticsearch ou MeiliSearch n'est pas dogmatique : si le projet évolue vers >50k items ou nécessite recherche fuzzy sophistiquée, migration possible sans refonte majeure (API de recherche reste identique, seule l'implémentation change).
**Principe appliqué** : "Use the simplest tool that works" - Ne pas sur-architecturer pour des besoins hypothétiques futurs.
---
**Contributeurs** : Gilles (décideur) + Claude Code (architecte)
**Références** :
- SQLite FTS5 documentation : https://www.sqlite.org/fts5.html
- BM25 ranking algorithm : https://en.wikipedia.org/wiki/Okapi_BM25