Skip to content

FEATURE_SIMILARITY_FINDER

Frédéric Clavert edited this page Jul 26, 2026 · 13 revisions

Feature: Similarity Finder - Recommandations bibliographiques contextuelles

Version: 1.0.0-rc.4 (re-verified) Status: Implémenté Dernière mise à jour: 2026-07-20


Vue d'ensemble

La fonctionnalité Similarity Finder permet à l'historien·ne de comparer son texte en cours de rédaction avec les PDFs indexés dans sa base vectorielle. Le système analyse chaque segment du texte (section, paragraphe ou phrase selon le choix) et recommande des sources pertinentes pour approfondir chaque partie.

Philosophie : Analyse asynchrone à la demande, adaptée aux contraintes d'un environnement local avec Ollama.


Cas d'usage principal

  1. L'utilisateur·rice rédige dans ClioDeck (l'analyse porte sur le contenu actif de l'éditeur — document.md pour un article, mais tout aussi bien un chapitre de livre)
  2. À un moment opportun (pause, fin de session), il/elle clique sur "Trouver des similarités" dans la barre d'outils de l'éditeur
  3. Une barre de progression indique l'avancement de l'analyse
  4. Une fois terminé, l'utilisateur·rice clique sur un segment dans la liste de résultats du panneau latéral (pas dans le texte affiché de l'éditeur lui-même)
  5. Un panneau latéral affiche les PDFs recommandés pour ce segment
  6. L'utilisateur·rice peut ouvrir le PDF ou insérer une citation

Spécifications fonctionnelles

1. Découpage du texte

L'utilisateur·rice choisit la granularité d'analyse :

Granularité Découpage Cas d'usage
Section Titres Markdown (#, ##, ###) Vue d'ensemble, textes longs
Paragraphe Blocs séparés par lignes vides Équilibre précision/performance
Phrase Segmentation par ponctuation Analyse fine, textes courts

Défaut recommandé : Paragraphe

2. Options configurables

Option Type Valeur par défaut Description
granularity enum paragraph Niveau de découpage (section, paragraph, sentence)
maxResults number 5 Nombre max de PDFs par segment (1-20)
similarityThreshold number 0 Seuil minimum de similarité (0-1) — aucun filtrage par défaut
collectionFilter string[] null Restriction à certaines collections Zotero
sourceType enum secondary Type de sources à rechercher (secondary, primary, both) — PDFs uniquement par défaut
useReranking boolean true Reranking par LLM activé par défaut pour une meilleure pertinence
useContextualEmbedding boolean true Embeddings contextuels activés par défaut (contexte du document ajouté)

3. Affichage des résultats

Interaction : Au clic sur un segment analysé, un panneau latéral s'affiche avec :

  • Titre du segment (segment.title, présent seulement pour les segments de type section ; sinon les 60 premiers caractères du contenu — pas de repli par numéro)
  • Score de similarité global
  • Liste des PDFs recommandés :
    • Titre de la source
    • Auteur(s)
    • Score de similarité (%)
    • Bouton "Ouvrir le PDF"
    • Bouton "Insérer citation"

4. Persistance et cache

Stratégie de cache intelligent — le verrou se pose au niveau du document entier, pas segment par segment :

Hash du document = hash(contenu_texte_entier)
Hash de la BDD = hash(dernière_modification_vectorstore)

Si document_hash + bdd_hash + options identiques existent en cache :
    → Lire les résultats par segment depuis cache.segments
Sinon :
    → Recalculer les embeddings
    → Sauvegarder en cache

Le cache expire aussi après 24 heures, et se réinvalide si granularity, maxResults ou similarityThreshold changent — pas seulement si le document ou l'index vectoriel changent.

Stockage : Fichier JSON dans le dossier projet (.cliodeck/similarity_cache.json)

5. Actions sur les recommandations

Action Comportement
Ouvrir le PDF Ouvre le PDF dans le lecteur ClioDeck
Insérer citation Insère [@zoteroKey] au curseur si une clé Zotero existe, sinon [titre tronqué] — format fixe, non configurable (SimilarityCard.tsx)

Architecture technique

Composants

┌─────────────────────────────────────────────────────────────────┐
│                         RENDERER                                │
├─────────────────────────────────────────────────────────────────┤
│  EditorPanel (barre d'outils inline, pas un composant dédié)    │
│    └── [Bouton "Trouver des similarités"]                       │
│                                                                 │
│  SimilarityPanel                                                │
│    ├── SimilarityProgress (barre de progression)                │
│    ├── SimilarityResults (panneau latéral)                      │
│    │     └── SimilarityCard (carte par PDF recommandé)          │
│    └── SimilarityOptions (modal de configuration)               │
│                                                                 │
│  useSimilarityStore (Zustand)                                   │
│    ├── results: Map<string, SimilarityResult>  (un par segment) │
│    ├── isAnalyzing: boolean                                     │
│    ├── progress: { current, total, status, percentage,          │
│    │               currentSegment? }  (AnalysisProgress complet) │
│    └── options: SimilarityOptions                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ IPC
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                           MAIN                                  │
├─────────────────────────────────────────────────────────────────┤
│  similarity-handlers.ts                                         │
│    ├── similarity:analyze                                       │
│    ├── similarity:get-all-results (pas get-results)             │
│    ├── similarity:cancel (annulation, absente de tout schéma     │
│    │                       précédent de cette page)              │
│    └── similarity:clear-cache                                   │
│                                                                 │
│  SimilarityService                                              │
│    ├── analyzeDocument(text, options)                           │
│    ├── segmentText(text, granularity)                           │
│    ├── findSimilarPDFs(segment, options)                        │
│    ├── loadCache() / saveCache()                                │
│    └── computeHashes()                                          │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                          BACKEND                                │
├─────────────────────────────────────────────────────────────────┤
│  pdfService (existant — pas un VectorStoreManager séparé)       │
│    └── search(embedding, options)                               │
│                                                                 │
│  LLMProvider (typed ProviderRegistry — Ollama, embedded,        │
│  Anthropic, OpenAI-compatible, Mistral, Gemini)                 │
│    └── generateEmbedding(text) / rerank via generate()          │
└─────────────────────────────────────────────────────────────────┘

SimilarityService no longer falls back to a legacy OllamaClient — callers (the IPC handler, tests) wire in an LLMProvider via setLLMProvider() before analysis runs. If reranking is requested but no provider is wired (or the LLM call fails for any reason), the error is caught and logged — not surfaced to the user — and the original, non-reranked order is used silently instead. Only the first 10 candidates are ever sent to the LLM for reranking (maxCandidates = Math.min(candidates.length, 10)), regardless of maxResults.

Structures de données

// Types principaux
interface SimilarityOptions {
  granularity: 'section' | 'paragraph' | 'sentence';
  maxResults: number;
  similarityThreshold: number;
  collectionFilter: string[] | null;
  sourceType: 'secondary' | 'primary' | 'both';
  useReranking: boolean;
  useContextualEmbedding: boolean;
}

interface TextSegment {
  id: string;           // Hash unique
  content: string;      // Contenu textuel
  startLine: number;    // Position dans le document
  endLine: number;
  type: 'section' | 'paragraph' | 'sentence';
  title?: string;        // Pour les sections : le texte du titre
}

interface SimilarityResult {
  segmentId: string;
  segment: TextSegment;   // Le segment complet, pas seulement son id
  recommendations: PDFRecommendation[];
  analyzedAt: number;   // Timestamp
}

interface PDFRecommendation {
  pdfId: string;
  title: string;
  authors: string[];
  similarity: number;   // 0-1
  chunkPreview: string; // Extrait pertinent du PDF
  zoteroKey?: string;   // Pour insertion citation
  pageNumber?: number;
  sourceType?: 'secondary' | 'primary';
  // Champs spécifiques aux sources primaires (Tropy) :
  sourceId?: string;
  archive?: string;
  collection?: string;
  date?: string;
  tags?: string[];
}

Côté renderer, le store Zustand (similarityStore.ts:71) garde results: Map<string, SimilarityResult>un seul résultat par segment, pas un tableau (SimilarityResult[]) comme un schéma antérieur de cette page le suggérait.


Dépendances existantes réutilisées

Composant existant Utilisation
pdfService Recherche de similarité et métadonnées des PDFs — pas de VectorStoreManager ni de ZoteroService séparés : le filtrage par collection Zotero passe directement par pdfService.search()'s collectionKeys, jamais par un appel direct à ZoteroService
LLMProvider (via ProviderRegistry) Génération d'embeddings et reranking — n'importe quel provider configuré, pas seulement Ollama
editorStore Gestion du document

Utilisation

Accès

  1. Dans l'éditeur, cliquez sur "Trouver des similarités" dans la barre d'outils

Configuration

Avant l'analyse, vous pouvez configurer (modal SimilarityOptions, 6 contrôles réels) :

  • Granularité : Section, Paragraphe, ou Phrase
  • Type de source : Secondaire (PDFs), Primaire (Tropy), ou les deux
  • Nombre de résultats : Maximum de sources par segment
  • Seuil de similarité : Filtrer les résultats peu pertinents
  • Reranking et Embeddings contextuels : activer/désactiver

Le filtrage par collection Zotero est disponible dans l'interface depuis la RC4. collectionFilter existait côté backend (similarity-service.ts) sans qu'aucun composant du renderer ne l'expose.

Interprétation des résultats

Attention : useReranking est activé par défaut, et quand il réussit, le reranking remplace le score de similarité sémantique par un score de rang synthétique ((nombre_candidats - position) / nombre_candidats — pour 5 candidats : 1.0, 0.8, 0.6, 0.4, 0.2). Dans la configuration par défaut, le nombre affiché reflète donc l'ordre de pertinence jugé par le LLM, pas une mesure de similarité sémantique brute. Si le reranking échoue silencieusement (provider non configuré, erreur d'appel), l'ordre et les scores d'origine sont conservés — les seuils ci-dessous ne s'appliquent que dans ce cas, ou avec useReranking: false :

  • Score élevé (> 0.8) : Source très pertinente pour ce segment
  • Score moyen (0.5-0.8) : Source potentiellement intéressante
  • Score faible (< 0.5) : Source marginalement liée

Bonnes pratiques

  1. Indexez vos PDFs avant d'utiliser le Similarity Finder
  2. Choisissez la bonne granularité selon la longueur de votre texte
  3. Utilisez le filtrage par collection — pas disponible dans l'interface aujourd'hui (voir Configuration ci-dessus)
  4. Relancez l'analyse après avoir modifié significativement votre texte

Limitations

  • Nécessite un provider d'embeddings configuré (Ollama, embarqué, ou un provider cloud) — plus une dépendance à Ollama spécifiquement
  • Performance dépend de la taille du corpus et du texte analysé
  • Cache invalidé si l'index vectoriel change

Clone this wiki locally