RAG for Implementers: Recherche hybride et indexation automatique
Introduction
📖 Partie de la série RAG Il s'agit de la partie 5 - modèles d'intégration de production:
- Partie 1: Origines et fondements du RAG - Quels sont les emplacements, pourquoi ils sont importants
- Partie 2: Architecture et éléments internes du RAG Bases de données vectorielles
- Partie 3: RAG dans la pratique - Établissement de systèmes RAG complets
- Partie 4a: ONNX & Mise en œuvre de la garantie - CPU- fondation de recherche sémantique conviviale
- Partie 4b: Recherche sémantique en action - Typeahead , recherche hybride, et composants de l'interface
- Pièce 5: Recherche hybride & AutoM SK2Indexage ( cet articleM SK1 - Modèles d'intégration de la production
- Partie 6: GraphRAG - Graphiques de connaissances pour le corpus
À l'intérieur Partie 4a, nous avons construit la base : embeddings ONNX et stockage Qdrant Partie 4b, nous avons couvert l’interface de recherche et la mise en œuvre hybride de la recherche . Maintenant, nous indexation automatique (ZéroM SK1Mises à jour des contenus touchés) via FileSystemWatcherMSC3
Recherche hybride: Le meilleur des deux mondes
La recherche sémantique est puissante, mais la recherche traditionnelle complète-la recherche textuelle excelle encore en termes exacts et techniques. La solution? Utiliser les deux .
Pourquoi Hybride? Différentes approches ont des forces différentes:
- PostgreSQL Full-Texte (qui sont couverts ici): Correspondances exactesM SK1 termes techniques, opérateurs bouliens
- Recherche par vecteur sémantique: MeaningM SK1 context , synonymes, contenu conceptuellement lié
Fusion de rang réciproque (RRFM SK1
Nous utilisons Fusion de rang réciproque pour combiner les résultats de plusieurs sources de recherche:
flowchart TB
A[User Query: 'docker containers'] --> B[PostgreSQL Full-Text Search]
A --> C[Semantic Vector Search]
B --> D["Results:<br/>1. 'Docker Basics' (rank 1)<br/>2. 'Containerizing Apps' (rank 2)<br/>3. 'Docker Compose' (rank 3)"]
C --> E["Results:<br/>1. 'Containerizing Apps' (rank 1)<br/>2. 'Kubernetes Guide' (rank 2)<br/>3. 'Docker Basics' (rank 3)"]
D --> F[RRF Algorithm]
E --> F
F --> G["Combined Results:<br/>1. 'Containerizing Apps'<br/> (1/61 + 1/62 = 0.0328)<br/>2. 'Docker Basics'<br/> (1/61 + 1/63 = 0.0322)<br/>3. 'Docker Compose'<br/> (1/63 = 0.0159)"]
style A stroke:#10b981,stroke-width:2px
style B stroke:#3b82f6,stroke-width:2px
style C stroke:#6366f1,stroke-width:2px
style D stroke:#3b82f6,stroke-width:2px
style E stroke:#6366f1,stroke-width:2px
style F stroke:#ec4899,stroke-width:4px
style G stroke:#8b5cf6,stroke-width:2px
La formule RRF: score = Σ(1 / (k + rank))
k= 60 ≥(constant pour empêcher les premiers rangs de dominerrank= position dans cette méthode de recherche- Résultats obtenus les deux les sources reçoivent des notes de chacune ajoutées ensemble
Pourquoi le RRF fonctionne:
- Déduplication: Résultat identique dans les deux sources avec un score plus élevé
- Équité: Aucune méthode de recherche ne domine injustement
- Simplicitité: Aucun ajustement complexe n'est requis
Mise en œuvre
public class HybridSearchService : IHybridSearchService
{
private readonly ISemanticSearchService _semanticSearchService;
private const int RrfConstant = 60;
public async Task<List<SearchResult>> SearchAsync(
string query,
string language = "en",
int limit = 10,
CancellationToken cancellationToken = default)
{
// Execute both searches in parallel
var semanticResults = await _semanticSearchService.SearchAsync(
query, limit * 2, cancellationToken);
// Filter by language and apply RRF
var filteredResults = semanticResults
.Where(r => r.Language == language)
.ToList();
return ApplyReciprocalRankFusion(filteredResults)
.Take(limit)
.ToList();
}
private List<SearchResult> ApplyReciprocalRankFusion(List<SearchResult> results)
{
var rrfScores = new Dictionary<string, RrfScore>();
for (int i = 0; i < results.Count; i++)
{
var result = results[i];
var key = $"{result.Slug}_{result.Language}";
if (!rrfScores.ContainsKey(key))
rrfScores[key] = new RrfScore { Result = result };
// RRF formula: 1 / (k + rank)
rrfScores[key].Score += 1.0 / (RrfConstant + i + 1);
}
return rrfScores.Values
.OrderByDescending(x => x.Score)
.Select(x => x.Result)
.ToList();
}
}
Note: Cela montre seulement la recherche sémantique. Dans la production, exécuter une recherche PostgreSQL complèteM SK2texte en parallèle et inclure ces résultats dans le calcul RRF .
Intégration
Si vous ' avez déjà mis en œuvre PostgreSQL complet - recherche de texte (tel qu'il est décrit ici), l'ajout de la recherche sémantique est straightforward
// Program.cs
services.AddSemanticSearch(configuration);
services.AddSingleton<IHybridSearchService, HybridSearchService>();
[HttpGet("search/hybrid")]
public async Task<IActionResult> HybridSearch(string query, string language = "en")
{
var results = await _hybridSearchService.SearchAsync(query, language);
return PartialView("_SearchResults", results);
}
Indexage automatique avec un observateur du système de fichiers
La fonctionnalité la plus puissante: indexation automatique. Save a blog post, itM SK2s immediately searchable - no manual interventionMSC4
Comment cela fonctionne
flowchart TB
A[Save Markdown File] --> B[FileSystemWatcher Detects Change]
B --> C{File in Main Directory?}
C -->|Yes| D[Save to Database]
C -->|No| E[Save to Database Only]
D --> F[Create BlogPostDocument]
F --> G[Generate Embedding via ONNX]
G --> H[Store in Qdrant]
H --> I[Post Searchable Immediately]
style A stroke:#10b981,stroke-width:2px
style B stroke:#f59e0b,stroke-width:2px
style C stroke:#ec4899,stroke-width:3px
style D stroke:#3b82f6,stroke-width:2px
style E stroke:#6b7280,stroke-width:2px
style F stroke:#8b5cf6,stroke-width:2px
style G stroke:#6366f1,stroke-width:3px
style H stroke:#ef4444,stroke-width:2px
style I stroke:#10b981,stroke-width:2px
Décision de conception clé: Seuls les fichiers index dans le le répertoire principal Markdown, non sous-directoires (translated/, drafts/, comments/). Cela permet de garder l'index de recherche propre
Intégration du moniteur de fichiers
Le blog a déjà un MarkdownDirectoryWatcherService. Nous l’extendons pour déclencher une indexation sémantique
// In MarkdownDirectoryWatcherService.cs
private async Task OnChangedAsync(WaitForChangedResult e)
{
if (e.Name == null) return;
await retryPolicy.ExecuteAsync(async () =>
{
var savedModel = await blogService.SavePost(slug, language, markdown);
// Index ONLY if file is in main directory (no path separators in name)
if (!e.Name.Contains(Path.DirectorySeparatorChar) &&
!e.Name.Contains(Path.AltDirectorySeparatorChar))
{
await IndexPostForSemanticSearchAsync(scope, savedModel, language);
}
});
}
private async Task IndexPostForSemanticSearchAsync(
IServiceScope scope,
BlogPostDto post,
string language)
{
var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();
if (semanticSearchService == null) return; // Not configured
var document = new BlogPostDocument
{
Id = $"{post.Slug}_{language}",
Slug = post.Slug,
Title = post.Title,
Content = post.PlainTextContent,
Language = language,
Categories = post.Categories?.ToList() ?? new List<string>(),
PublishedDate = post.PublishedDate
};
await semanticSearchService.IndexPostAsync(document);
_logger.LogInformation("Indexed {Slug} ({Language}) in semantic search", post.Slug, language);
}
Traitement des suppressions
Lorsque un poste est supprimé, le retirer de l’index sémantique:
private async Task OnDeletedAsync(WaitForChangedResult e)
{
await blogService.Delete(slug, language);
// Delete from semantic search ONLY if file was in main directory
if (!e.Name.Contains(Path.DirectorySeparatorChar) &&
!e.Name.Contains(Path.AltDirectorySeparatorChar))
{
var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();
await semanticSearchService?.DeletePostAsync(slug, language);
}
}
Service d'information pour l'indexation initiale
Au démarrage, un service de base indexe les postes existants qui ne sont pas encore dans Qdrant.
flowchart TB
A[Application Starts] --> B[Wait 10 seconds]
B --> C[Initialize Semantic Search]
C --> D{Model Exists?}
D -->|No| E[Download from Hugging Face]
D -->|Yes| F[Load ONNX Model]
E --> F
F --> G[Scan Main Markdown Directory]
G --> H{For Each .md File}
H --> I[Compute Content Hash]
I --> J{Hash Changed?}
J -->|Yes| K[Generate Embedding]
J -->|No| L[Skip - Already Indexed]
K --> M[Store in Qdrant]
M --> H
L --> H
H -->|Done| N[Indexing Complete]
style A stroke:#10b981,stroke-width:2px
style C stroke:#6366f1,stroke-width:2px
style E stroke:#f59e0b,stroke-width:2px
style F stroke:#6366f1,stroke-width:3px
style G stroke:#8b5cf6,stroke-width:2px
style J stroke:#ec4899,stroke-width:3px
style K stroke:#6366f1,stroke-width:2px
style M stroke:#ef4444,stroke-width:2px
style N stroke:#10b981,stroke-width:2px
public class SemanticIndexingBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Wait for app to be ready
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
// Initialize (downloads model if needed)
await _semanticSearchService.InitializeAsync(stoppingToken);
// Get all posts from main directory only
var markdownFiles = Directory.GetFiles(
_markdownConfig.MarkdownPath,
"*.md",
SearchOption.TopDirectoryOnly); // NOT subdirectories
foreach (var file in markdownFiles)
{
var needsIndexing = await _semanticSearchService.NeedsReindexingAsync(
slug, language, contentHash, stoppingToken);
if (needsIndexing)
await _semanticSearchService.IndexPostAsync(document, stoppingToken);
}
}
}
Cela garantit
- Chargement de modèles lazi - Les téléchargements lors de la première utilisation, ne bloquent pas le démarrage
- Indexage progressif - Seuls les nouveauxM SK1postes modifiés re-indexés par le biais du contenu hashMSC4
- Répertoire principal seulement - Les projets et les fichiers traduits ne polluent pas l’indice
Ce que nous avons construit
Parmi les parties 4aM SK1 4b, et 5, nous avons maintenantMSC5
- ✅ CPU-recherche sémantique conviviale - Aucun GPU n'est requis
- ✅ Postes connexes découverte - Contenu sémantiquement similaire
- ✅ Recherche linguistique naturelle - Trouver par signification, pas seulement mots clés
- ✅ Recherche hybride - Le meilleur de la sémantique
- ✅ Indexage automatique - Zero-touch mises à jour du contenu
- ✅ Sous-traitance - Vos données restent sur votre serveur
Améliorations futures:
- Catégorie-Suivre attentivement - Résultats de renforcement provenant de catégories spécifiques
- Embeddings multilingues - Langue - Modèles d'embedding spécifiques
- Intégration d'OpenSearch - Ajouter OpenSearch au mélange hybridevoir mon article OpenSearch)
Conclusion
Cela complète la mise en oeuvre pratique de RAG-style semantic search. Combiné avec Partie 4a (fondation) et Partie 4b (interface de recherche), vous disposez de tout ce dont vous avez besoin pour ajouter une recherche intelligente à votre application .NET
Poursuite de l'apprentissage
- Partie RAG 1: Origines et fondements - La théorie derrière les emplacements
- Partie RAG 2: Architecture et intérieurs - plongée profonde dans les systèmes RAG
- Partie RAG 3: Applications pratiques - RAG complet avec l'intégration de LLM
- Partie 4a: ONNX & Mise en œuvre de la garantie - Fondation: Emplacements et stockage de vecteurs
- Partie 4b: Recherche sémantique en action - Typeahead , recherche hybride, et UI
- Recherche complète-Texte avec PostgreSQL - Le côté texte complet de la recherche hybride
Ressources
Bases de données des vecteurs Qdrant &
- Self-Databases de vecteurs hébergées avec Qdrant - plongée profonde dans les concepts de Qdrant , indexation de l’HNSW M SK2 filtration , et client C
- Recherche hybride de Qdrant - Qdrant's support hybride natif
Recherche hybride
- Papier de fusion réciproque du rang - L'algorithme RRF
Surveillance du système des fichiers
- Classe de FileSystemWatcher Documentation NET
- Classe de services généraux - Services hébergés dans ASP.NET Core
Code complet
Tous les codes sont disponibles à: github.comM SK1scottgal/mostlylucidweb
Mostlylucid.SemanticSearch/- Bibliothèque de recherche sémantique centraleMostlylucid/Blog/WatcherService/- Observateur de fichiers avec index sémantique