RAG-sarjan osa: Tämä on osa 5 - Tuotannon integraatiomallit:
Sisään Osa 4a, rakensimme säätiön: ONNX upotukset ja Qdrantin varasto. In Osa 4b, teimme hakuumme UI:n ja hybridihaun toteutuksen. Nyt teemme siitä tuotantovalmiin automaattinen indeksointi (nollakosketussisältöpäivitykset) FileSystemWatcherin kautta.
Semanttinen haku on tehokas, mutta perinteinen täystekstihaku on silti erinomainen tarkoissa fraaseissa ja teknisissä termeissä. Mikä on ratkaisu? Käytä molempia.
Miksi Hybrid? Erilaisilla lähestymistavoilla on erilaiset vahvuudet:
Käytämme Vastavuoroinen rank Fuusio Yhdistetään tulokset useista hakulähteistä:
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
RRF:n kaava: score = Σ(1 / (k + rank))
k = 60 (jotta varhaiset rivit eivät hallitsisi)rank = asema kyseisen hakumenetelmän tuloksissaMiksi RRF toimii:
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();
}
}
Huomaa: Tämä näyttää vain semanttisen haun. Tuotannossa suorita PostgreSQL-täystekstihaku rinnakkain ja liitä tulokset RRF:n laskelmaan.
Jos olet jo toteuttanut PostgreSQL-kokotekstihaun (sellaisena kuin se on täällä), semanttisen haun lisääminen on yksinkertaista:
// 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);
}
Voimakkain ominaisuus: automaattinen indeksointi. Tallenna blogikirjoitus, se on välittömästi hakukelpoinen - ei manuaalista väliintuloa.
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
Avainpäätös: Vain hakemistotiedostot Markdownin päähakemisto, ei alihakemistoja (translated/, drafts/, comments/). Tämä pitää hakuindeksin puhtaana.
Blogissa on jo MarkdownDirectoryWatcherServiceLaajennamme sitä laukaisemaan semanttisen indeksoinnin:
// 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);
}
Kun viesti poistetaan, poista se semanttisesta indeksistä:
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);
}
}
Startupissa taustapalvelu indeksoi olemassa olevia virkoja, joita ei vielä ole Qdrantissa:
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);
}
}
}
Näin varmistetaan, että
Osat 4a, 4b ja 5 ovat nyt seuraavat:
Tulevia parannuksia:
Tämä täydentää RAG-tyylisen semanttisen haun käytännön toteutusta. Osa 4a (perustaminen) ja Osa 4b (Etsi UI), sinulla on kaikki tarvittava, jotta voit lisätä älykkään haun .NET-sovellukseesi, joka toimii kokonaan CPU:lla, ilman lisäkustannuksia.
Kaikki koodit ovat saatavilla osoitteessa: github.com/scottgal/mostlylucidweb
Mostlylucid.SemanticSearch/ - Core semanttinen hakukirjastoMostlylucid/Blog/WatcherService/ - Tiedostonvalvoja, jolla on semanttinen indeksointi© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.