La ricerca è una di quelle caratteristiche che tutti sottovalutano. Sembra banale finché gli utenti reali non iniziano a digitare le domande vere -acroniemiM SK2 metà - termine tecnici ricordatiMska4 punteggiaturaMske5 nomi pesanti come "ASPMsko7NETMSC8 o ricerche che sono conceptualmente. Giusto ma sbagliato dal punto di vista textuale. Quando la ricerca fallisce in quei casi, gli utenti non pensanoM SK2non credono "esempio di erroreMST4 M ST5Pendono che il sito sia rottoMst6
È anche sometigno che il ' abbia plagiato questo sito da quando è nato. OpenSearch, ha lavorato su PostgreSQL per la ricerca del testo completo con questo ' le cose fantastiche dei vettori ma non è mai stato veramente felice con questo Infestata. Io, in particolare ora, sto costruendo uno strumento di ricerca. **lucido.RAG Beh, ho pensato che avrei dovuto ripararlo. Che sia O meno è un'altra questione.
Questo articolo non riguarda la costruzione di un motore di ricerca da zero... Ma ' riguarda la riparazione dei bordi acuti dell'intero PostgreSQL.-. La ricerca del testo in un sistema di produzione reale.M SK4. Io. ' passerò attraverso i specifici fallimenti che ho colpito.
Costruire sulla base dei lavori precedenti: Questo articolo estende la Implementazione di ricerca semantica che ha aggiunto la ricerca ibrida con Reciprocal Rank Fusion (RRF). L'articolo precedente ha coperto il fondamento -combinando PostgreSQL fullM SK3la ricerca del testo con la ricerca dei vectori QdrantMSC4 Questo articolo corregge i casi marginali che l'implementazione ha persoMST5acroniemi che non corrispondonoM ST6non corrispondenoM st7 termine tecnici con caratteri specialiM SST8 e domande naturali che la parsetrice di Postgre SQLM SP9 rompeMSST10
L'infrastruttura di ricerca semantica serve a due scopi: alimenta il sitoM SK1 l'utente- la ricerca orientata. e fornisce la stratagemma di recupero per il Avvocato GPT Il sistema RAG -un assistente di scrittura che disegna nuovi posti usando il sito'il contenuto esistente come base di conoscenzaM SK2 Per approfondire la tecnologia sottostanteMSC3 consultate Costruire una "Lawyer GPT" – Parte 3 sulle inserzioni e sulla ricerca dei vettori.
Quando una ricerca non ha risposto nessun risultato, il sistema mostrava articoli vecchi casuali invece di consigli utili. Questo violava il principio della minore sorpresa M SK2 gli utenti aspettavano risultati rilevanti o chiari "no match foundMSC4 messaggio con post recenti come suggerimentiMST5 Questo faceva sembrare che la ricerca avesse funzionato SST6 solo maleSTS7
La soluzione:Modificato BlogSearchService.HybridSearchWithPagingAsync() per rilevare quando la ricerca riporta risultati zero e tornare a mostrare i post recenti ordinati alla data di declino. Added a NoMatchFound bandiere verso. BasePagingModel Così l'interfaccia può mostrare un messaggio appropriato come "Niente corrispondenza trovata. Voglio dire uno di questi?
// No match found - return recent posts as suggestions
if (noMatchFound)
{
Log.Logger.Information("No search results for '{Query}', returning recent posts as suggestions", query);
return await GetRecentPostsAsSuggestions(targetLanguage, startDate, endDate, page, pageSize);
}
La configurazione predefinita della ricerca di testo inglese PostgreSQL' indica tecnicamente gli acroniemi, ma la normalizzazione e il deterrimento rendeno i termini significativi poco affidabili nella praticaM SK4 Quando si cerca MSSK5DiSEMSC6 viene abbassato a "dise+" e poi il dizionario inglese può rimuoverlo o assegnare un basso peso+ , causando la completa ricerca del testo a perdere articoli che contieneno chiaramente "DiSe+" nel titolo e nel contenuto+M SK13
La soluzione: Added acronym detection (terms ≤6 characters with uppercase lettersMSC3 and caseM SK4insensitive substring fallback using PostgreSQL's ILIKE. Questo completa la ricerca di testo completaM SK1 anziché la sostituire ( consultate i Consideramenti sul Rendimento sotto per capire perché è accettabile):
// Detect if query looks like an acronym or short term
var isAcronymLike = query.Length <= 6 && query.Any(char.IsUpper);
// Add substring search for acronyms
searchQuery = searchQuery.Where(x =>
EF.Functions.ILike(x.Title, $"%{acronym}%")
|| EF.Functions.ILike(x.PlainTextContent, $"%{acronym}%"));
Le ricerche per "ASPM SK1NET" o "C#" fallirebbero perché il parsatore di ricerca del testo PostgreSQLMSC5 tratta i periodi e i simboli di hash come dei delimitatori.
La soluzione: Creato un SearchQueryParser che riconosce termini tecnici comuni e li sostituisce con versioni ricercabili:
private static readonly Dictionary<string, string> TechnicalTerms = new(StringComparer.OrdinalIgnoreCase)
{
["asp.net"] = "aspnet",
["c#"] = "csharp",
[".net"] = "dotnet",
["f#"] = "fsharp",
["node.js"] = "nodejs",
// ... more terms
};
PostgreSQL tratta "e" come una parola di stop e la rimuove dalle ricercheM SK2 In combinazione con il problema del carattere specialeMSC3 una domanda naturale come "ASPMNK5NET e AlpineSSK6 sarebbe essenzialmente diventata una ricerca per "AlpineMS" soloMSSK9
La soluzione: Ha implementato un parsatore di query del tipo Google - che gestisce intelligentmente le parole stop e supporta operatori di ricerca avanzati
Ho messo in atto un SearchQueryParser una classe che analizza le domande con il supporto di:
"exact match" - cerca la frase esatta.-unwanted - esclude i risultati che contengono questo termine.ASP* - corrisponde "ASP", "AspNETM SK4 "ASPNetCoreMSC6 eccMST7Il traduttore usa un modello di regex compilato per etichettare la domanda:
[GeneratedRegex(@"""([^""]+)""|(-)?(\S+)", RegexOptions.Compiled)]
private static partial Regex QueryTokenRegex();
public ParsedQuery Parse(string query)
{
var matches = QueryTokenRegex().Matches(processedQuery);
foreach (Match match in matches)
{
// Quoted phrase
if (match.Groups[1].Success)
{
var phrase = match.Groups[1].Value.Trim();
result.Phrases.Add(phrase);
continue;
}
// Excluded term (starts with -)
if (match.Groups[2].Success)
{
var term = match.Groups[3].Value.Trim();
result.ExcludeTerms.Add(term.ToLowerInvariant());
continue;
}
// Regular term or wildcard
var token = match.Groups[3].Value.Trim();
if (token.Contains('*'))
{
result.WildcardTerms.Add(token.Replace("*", ""));
}
else if (!StopWords.Contains(token))
{
result.IncludeTerms.Add(token.ToLowerInvariant());
}
}
}
Il programmatore produce dati strutturati che poi sono convertiti in PostgreSQL. to_tsquery Perché noi' generiamo una sintaxa strutturata - non. websearch_to_tsquery Perché noi, ', abbiamo già analizzato noi stessi gli operatori,, dandoci più controllo su come si combinano i termini.
public string BuildTsQuery(ParsedQuery parsed)
{
var queryParts = new List<string>();
// Add include terms with AND
foreach (var term in parsed.IncludeTerms)
{
queryParts.Add(term);
}
// Add wildcard terms with :* suffix
foreach (var term in parsed.WildcardTerms)
{
queryParts.Add($"{term}:*");
}
return queryParts.Count > 0 ? string.Join(" & ", queryParts) : string.Empty;
}
Perché non usare solo?
websearch_to_tsquery? Perché continua a fallire in termini tecnici, acroniemi, e dominioM SK2sintax specifica MSC3e non offre alcun imbocco per classificazioni ibride o fallbacks . Analizzando noi stessiMska5 possiamo gestire questi casi marginali prima che raggiungano PostgreSQLMske6
L'informazione figura nella parte dispositiva. BuildSearchQuery il metodo è stato completamente riscritto per usare la struttura di query parsed. Notate che baseQuery già filtri secondo la lingua, gamma data, e visibilità M SK2 noiMSC3 stiamo aggiungendo ricerca -condizioni specifiche in altoMNK5
private IOrderedQueryable<BlogPostEntity> BuildSearchQuery(
string query,
string language,
DateTime? startDate,
DateTime? endDate,
string order)
{
var parsed = _queryParser.Parse(query);
IQueryable<BlogPostEntity> searchQuery = baseQuery;
// Handle phrases (exact substring matching)
foreach (var phrase in parsed.Phrases)
{
searchQuery = searchQuery.Where(x =>
EF.Functions.ILike(x.Title, $"%{phrase}%")
|| EF.Functions.ILike(x.PlainTextContent, $"%{phrase}%")
|| x.Categories.Any(c => EF.Functions.ILike(c.Name, $"%{phrase}%")));
}
// Build tsquery for include terms and wildcards
var tsQuery = _queryParser.BuildTsQuery(parsed);
// Apply full-text search if we have terms
if (!string.IsNullOrWhiteSpace(tsQuery))
{
searchQuery = searchQuery.Where(x =>
x.SearchVector.Matches(EF.Functions.ToTsQuery("english", tsQuery))
|| x.Categories.Any(c =>
EF.Functions.ToTsVector("english", c.Name)
.Matches(EF.Functions.ToTsQuery("english", tsQuery))));
}
// Handle acronyms with case-insensitive substring search
// This supplements full-text search (additive OR), not replaces it
var acronymTerms = parsed.IncludeTerms
.Concat(parsed.WildcardTerms)
.Where(t => _queryParser.IsAcronymLike(t))
.ToList();
foreach (var acronym in acronymTerms)
{
searchQuery = searchQuery.Where(x =>
EF.Functions.ILike(x.Title, $"%{acronym}%")
|| EF.Functions.ILike(x.PlainTextContent, $"%{acronym}%"));
}
// Handle excluded terms (must NOT contain these)
foreach (var excludeTerm in parsed.ExcludeTerms)
{
searchQuery = searchQuery.Where(x =>
!EF.Functions.ILike(x.Title, $"%{excludeTerm}%")
&& !EF.Functions.ILike(x.PlainTextContent, $"%{excludeTerm}%")
&& !x.Categories.Any(c => EF.Functions.ILike(c.Name, $"%{excludeTerm}%")));
}
return orderedQuery;
}
Ecco alcuni esempi della migliore funzionalità di ricerca:
DiSE
✅ Ora corrisponde agli articoli con "DiSE" nel titolo o nel contenuto SSK3caseM SK4insensitiveMSC5
ASP.NET
✅ Trova articoli su ASPM SK1NET (convertito automaticamente in SSK3aspnet" per la ricerca completa
"semantic search"
✅ Trova la frase esatta "la ricerca semantica" negli articoli
ASP.NET -Core
✅ Trova articoli ASPM SK1NET ma esclude quelli che menzionano "Core"
ASP*
✅ Matches "ASP", "AspNETM SK4 "ASPNetCoreMSC6 etcMST7
"full text search" PostgreSQL -MySQL
✅ Trova articoli con la frase esatta "la ricerca del testo completoM SK2 E citazioni di PostgreSQL, ma escludendo gli articoli che menzionano MySQL
La domanda: ASP.NET and Alpine
Prima (bloccatoM SK1
Dopo. (fixed):
La ricerca si integra con la fusione reciproca di classifica (RRFM SK1 come implementata nel articolo di ricerca semantica precedente. RRF qui è usato per La classificazione, non ricordare - combina già- risultati ottenuti dal BMM SK3 interoMSC4 ricerca di testo e ricerca semantica con il vettoreMST5
// Fuse using RRF with category/freshness boosts
var fusedDtos = _ranker.FuseResults(bm25Results, vectorResults, query);
L'algoritmo RRF usa 1/(k+rank) dove k=60 combina i risultati da diverse fonti , poi applica incrementi per:
Per la completa implementazione del RRF e come funziona la ricerca ibrida, consultate La ricerca semantica in azione. Per approfondire le immersioni nelle inserzioni e la somiglianza dei vettoriM SK1 consultate Costruire una "Lawyer GPT" - Parte 3. La stessa infrastruttura ha potere sia per l'utente - per la ricerca che per il rilevamento RAG dell'AI- per il scrittura assistitaM SK3
I nuovi componenti sono registrati come servizi:
services.AddSingleton<SearchQueryParser>();
services.AddSingleton<SearchRanker>();
services.AddScoped<BlogSearchService>();
SearchQueryParser e SearchRanker sono singletons perché'no statolessi,proiettile -sicuriM SK3e possono essere riutilizzati per tutte le richieste BlogSearchService è scoppato perché accessisce il contesto della base di dati.
Lo stesso componente di ricerca semantica viene anche usato dal Avvocato GPT il sistema per recuperare i post di un blog precedente rilevanti per l'AI-scrittura assistita. Vedete Parte 4 per i dettagli sul tubo di ingestione.
La ricerca si basa su un precomputato. SearchVector in una colonna. BlogPosts Tabella:
ALTER TABLE mostlylucid."BlogPosts"
ADD COLUMN "SearchVector" tsvector
GENERATED ALWAYS AS (
to_tsvector('english',
coalesce("Title", '') || ' ' ||
coalesce("PlainTextContent", '')
)
) STORED;
CREATE INDEX idx_blog_posts_search_vector
ON mostlylucid."BlogPosts"
USING GIN ("SearchVector");
Il GIN (Generalized Inverted Index) fornisce una rapida ricerca completa del testo - attraverso un grosso corporato di testo
Mentre ILIKE (caseM SK1insensitive LIKE) è più lento di un'intera
Questo approccio non si sarebbe adattato alla ricerca arbitraria di sottostringi su milioni di linee -ma per gli acronimi mirati il fallback è ' l'appropriato.
ts_rank_cd)PostgreSQL offre due funzioni di classificazione per la ricerca completa.
ts_rank: Frequenza di termine base ( quante volte si presentano i termini)ts_rank_cd: Ripartimento della densità di copertura (come le condizioni si presentano insiemeM SK2Noi usiamo. ts_rank_cd Perché fornisce Relevance simile a BM25- Considerando la vicinanza del termine:
// Order by cover density ranking - rewards term proximity
orderedQuery = searchQuery.OrderByDescending(x =>
x.SearchVector.RankCoverDensity(EF.Functions.ToTsQuery("english", tsQuery)));
Perché? ts_rank_cd è superiore:
| Metrica | ts_rank |
ts_rank_cd |
|---|---|---|
| Algoritmo | Frequenza territoriale | Densità di copertura proximitàMSC3 S |
| Multi-questioni di parole | Conta i termini separatamente | Termini di ricompensa che si presentano insieme |
| Esempio: M SK1docker containers" | Un articolo con "docker" 50 ha un punteggio alto SSK4 Un artigo con "doker containersM SK6 ha una punteggio più alto | |
| La performance | veloce | Marginalmente più lentoM SK2 ancora sfrutta l'indice GIN |
| Comportamento | Calcolo semplice | Approximato BM |
Questo è un l'ottimizzazione di rapidi vincitori - migliore classificazione di rilevanza con un'applicazione zero- calcolo di livelloM SK2 Tutte le classifiche si fanno in PostgreSQL usando l'indice GIN esistente su SearchVector.
Referenze:
Oltre a riparare la funzione di ricerca, alcune ottimizzazioni chiave migliorano la performance:
Le lingue disponibili raramente cambiano ma sono state consultate su ogni richiesta di ricerca. Ora in cassa con il doppio-controllato l'interrutturaM SK2
private static readonly TimeSpan LanguageCacheDuration = TimeSpan.FromHours(1);
private static List<string>? _cachedLanguages;
private static DateTime _languageCacheExpiry = DateTime.MinValue;
private static readonly SemaphoreSlim _cacheLock = new(1, 1);
Impacto: Esclude la domanda di base 1 per ogni richiesta di ricerca
Il codice originale usato foreach loops che creano più clause WHERE. Ora batchiate in espressioni singole:
// BEFORE: Multiple WHERE clauses
foreach (var acronym in acronymTerms)
{
searchQuery = searchQuery.Where(x =>
EF.Functions.ILike(x.Title, $"%{acronym}%"));
}
// AFTER: Single batched WHERE
if (acronymTerms.Count > 0)
{
searchQuery = searchQuery.Where(x =>
acronymTerms.Any(acronym =>
EF.Functions.ILike(x.Title, $"%{acronym}%")));
}
Impacto: SQL più pulito, ~5-10% più veloce per le domande a termineM SK3
Added partial index with INCLUDE columns for frequent access patterns:
CREATE INDEX idx_blog_posts_search_covering
ON mostlylucid."BlogPosts" ("LanguageId", "IsHidden", "ScheduledPublishDate")
INCLUDE ("Id", "Slug", "Title", "PublishedDate")
WHERE "IsHidden" = false;
Impacto: Si accende index-scanna solo - PostgreSQL non ha bisogno di avere accesso alla mappa della tavola , riducendo l'I/O a 20-30% per le domande comuniM SK5
Core EF's Include() loads full navigation entities. Removed where only navigation properties used in WHERE clauses:
// BEFORE: Loads full LanguageEntity into memory
.Include(x => x.LanguageEntity)
.Where(x => x.LanguageEntity.Name == "en")
// AFTER: EF translates navigation property without loading entity
.Where(x => x.LanguageEntity.Name == "en")
Impacto: ~5-10% riduzione della memoriaM SK2 meno dati trasferiti dalla base di dati.
Referenze:
La ricerca crea WHERE clause incrementally usando La composizione dell'albero dell'espressione di EF Core':
IQueryable<BlogPostEntity> searchQuery = baseQuery;
// Each filter added conditionally - PostgreSQL optimizes the final query
if (parsed.Phrases.Count > 0) { searchQuery = searchQuery.Where(...); }
if (!string.IsNullOrWhiteSpace(tsQuery)) { searchQuery = searchQuery.Where(...); }
if (acronymTerms.Count > 0) { searchQuery = searchQuery.Where(...); }
Questo genera un una singola domanda SQL ottimizzata . PostgreSQL' il programmatore di query può usare statistiche e indexi in modo efficace quando vede la completa frase WHEREM SK2
Impatti cumulattivi di tutte le ottimizzazioni:
| Optimizzazione | Impatti di latenza | Imppatti della carica del DB M | |||
|---|---|---|---|---|---|
| Caching linguistico | Minimale | ||||
| Rimuovere Includere() | -5-10% | Meno trasferimento di dati | |||
| Batch ILIKE | -5-10% ≥ | Cleaner SQL | |||
| Indice di copertura | -20-30% | Indiceso-scan solo S | |||
| tsM SK1rank_cd | Analogo | Rilevantere S | |||
| Riscaldamento del cachio (parami di rotazione) | NM SK4A M | Prevede i dati immutabili R |
Aumento totale previsto: 30-50% ricerca più veloce con una significativa riduzione del carico della base di dati.
Don't assume full-la ricerca di testo gestisce tutto: Case a bordo come acroniemi e caratteri speciali hanno bisogno di un trattamento speciale.
Combinare diversi approcci.: La ricerca completaM SK1la ricerca del testo (BM25) + la ricerca semantica SSK5vectoriMSC6 + i fallback della sottostringa forniscono una copertura migliore di qualsiasi singolo metodoMNK8
Google ha formato le aspettative degli utenti: Sostenere le frasi citate, escluzioniM SK2 e cartoline selvatiche fa sembrare la ricerca naturale perché gli utenti sono già familiari con questi operatori
Fornire dei collaterali utili.: Quando la ricerca fallisceM SK1 non ' non mostra nulla - mostra i post recenti e rende chiaro che non è stato trovato nessun corrispondenza
Parlare, don't hack: Un parsatore di query appropriato è più pulito e più mantenibile di una serie di manipolazioni di stringhe.
Leverage PostgreSQL's built-in ranking: Usare ts_rank_cd invece di ts_rank per BM25-relevance simileM SK1 La classifica della densità di copertura considera la vicinanza del termine - una vittoria veloce che migliora la qualità dei risultati con zero applicazione -codè di livelloMSC4
Profilo prima di ottimizzare: I "ovvi" bottiglie di bottiglia (ParsingFTSM SK4 non erano il problema realeMSC5I lookups linguisticiMSL7 include troppoMST8 e gli indici mancanti hanno avuto un impatto più grande che previstoMSM9
Operazioni di base di dati a lotto: Molto foreach loop create separate WHERE clauses generate suboptimal SQL. Usare Any() o All() per batchiare in espressioni singole.
Possibili miglioramenti per il futuro, classificati secondo le preoccupazioniM SK1
Riparazioni alla ricerca:
Riparazioni:
category:ASP.NET l'operatoreafter:2025-01-01 l'operatoreRidefinizioni di classificazione:
Osservabilità:
La ricerca a livello di costruzione- richiede riparare le case a bordo. e Optimizzare la performance. Questo articolo ha riguardato sia l'aggiustazione della PostgreSQL completa, sia le case di edge di ricerca del testo, sia gli acroniemi, sia i termini tecnici, sia Google, sia -, gli operatori di stile, ma anche ), e l'implementazione delle principali ottimizzazioni di performance. ts_rank_cd).
L'implementazione 30-50% ricerca più veloce riducendo il carico della base di dati attraverso:
ts_rank_cd) per una migliore rilevanzaL'intuizione chiave: nessun approccio singolo gestisce tutti i casi. PostgreSQL FTS è eccezionale nella combinazione di parole chiave , la ricerca semantica gestisce le domande concettualiM SK3 e gli fallbacks mirati catturano i casi a margineMSC4 La strata di parizzazione pulita lo collega insiemeMST5 gestisce gli operatori e i termini tecnici prima che raggiungano i motori di ricercaM ST6
L'infrastruttura di ricerca semantica serve a due scopi:
Article connessi:
Documentazione ufficiale:
Tutti i codici disponibili nel blog's Reposito di GitHub.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.