This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Wednesday, 14 January 2026
Поиск - одна з тих характеристик, яких недооцінюють усі. . Це виглядає банально, поки реальні користувачі не починають друкувати справжні запитання. концептуально right but textually wrong. When search fails in those cases , users donMSC2t think M SK3edge case" МSK5they think the site is broken
Я подивився на те, Відкрите пошук, працювали над пошуком повного тексту PostgreSQL з ним ' чудові векторні речі, але ніколи не були дійсно щасливі з ним заплутаний я особливо зараз я будую пошуковий інструмент в прозораРАГ я думав, що повинен нарешті це виправити. Чи це так, чи ні - це інша справа.
Ця стаття не про те, як побудувати пошуковий двигун з нуля. МСК1 Це МСК2 про виправлення острих країв повного PostgreSQL. МSK3 пошук текстів у реальному виробничому systemie. . Я МСК5 розглянемо певні недоліки, які я бачив., чому вони трапились? , і прагматичне виправлення, яке необхідно для того, щоб пошук поводився так, як вже очікують користувачі.
Збудування на основі попередньої роботи: Цей artykuł розширює застосування семантичного пошуку що додала гібридну пошукову систему з реципрочною об 'єднанням рангів (RRFM SK1 Те, що раніше описувало основу - поєднання PostgreSQL повного МSK3 текстового пошуку з Qdrant векторним пошуком M SK4 Цей artykuł вирішує крайні випадки, яких Implementation пропустила : акроніми, які не співпадають с МСК6 не співпадають с MСК7 технічні терміни з особливими символами М СК8 і природні питання, які розбиває Parser Postgre SQL M СК9
Семантична пошукова інфраструктура має двоє цілей: МСК0, вона дає доступ до сайту, МСК1, користувача, МSK2 і орієнтується на пошук і надає шар пошуку для Адвокат GPT РАG-система -допомічник, який пише нові записи за допомогою сайту МSK1 наявний контент як база знань МSK2 Для глибших подивів на базову технологію , читайте Збудову " Адвоката ГПЗМSK1 МСК2 Частину МСК3 на вбудовах та векторних пошуках.
Коли пошук не давав результатів, система показувала випадкові старі статті замість допоміжних підказокМSK1 Це було порушенням принципу найменшого здивування МСК2 користувачі очікували або відповідних результатів, або ж чіткого MСК3 не знаходилось співпадінняМ СК4 повідомлення з останніми постами як підказкиM СК5 Це змусило виглядати так, ніби пошук спрацювало МСК6 просто погано М СК7
Справа: Modiфіковано BlogSearchService.HybridSearchWithPagingAsync() щоб виявити, коли пошук повертає нуль результатів і повертається до показу останніх постів, запорядкованих по даті падіння. Добавлено NoMatchFound прапором до BasePagingModel таким чином інтерфейс може відображати важливе повідомлення, як: "Ненайдено співпадінняM SK1 Ви маєте на увазі одне з цих?"
// 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);
}
У стандартній конфігурації пошуків тексту в англійській мові PostgreSQL' акроніми відображаються як акроніми ,, але звичайність та закінчування роблять короткі, випадки - значні терміни ненадійними у практиціM SK4 Коли ви шукаєте MSC5DiSE",, він зводиться до lowercased (нижчі літери) на МSK7diseMNK8 і тоді англійський словник може відкинути його або присвоити маловажливе слово(акронім)Mスク9, що призводить до повного МSK10 пошуку тексту, який пропустить статті, які чітко містять "DiSeMRK12 у назвах та змістіMS13
Справа: Добавлено розпізнавання акронімів | | ( | терміни | МSK2 | символи з верхніми літерами ILIKE. Це додає повного - пошуку тексту, а не замінює його.
// 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}%"));
Поиски на "ASPM SK1NET" або "CMSC4 не спрацюють, тому що в Parserі для текстового пошуку PostgreSQLМSK5 відображають періоди та хеш-символи як делімітаториMスク6 розділяючи МSK7АSPМСК8NET" на \ "\ASP\MСК11\ і \ МСК12\ NET\ MСК13\ як окремі символи. Це означало, що пошук точного терміну не спрацьовує.
Справа: Створили SearchQueryParser що визначає звичайні технічні терміни і замінює їх пошуковими версіями:
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 сприймає " і " як застереження і виключає їх з пошуку. МSK2 Поєднано з проблемою спеціального символу МSK3 природним запитом, таким як | " | ASP | МSK5 | NET та | Alpine \ " | в основному стане пошуком для |
Справа: Implemented a Google -style query parser that handles stop words intelligently and supports advanced search operators
Я реалізував SearchQueryParser клас, який аналізує питання з підтримкою для:
"exact match" - шукає точну фразу-unwanted - не включає результати, що містять цей термінASP* - порівнює "ASPM SK2 МSK3АSPNET", "ASPNetCoreMSC6 і т.д.Розшифровувач використовує компіліровану модель регекса для символізації запиту:
[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());
}
}
}
Програміст генерує структуровані дані, які потім перетворюються на PostgreSQL to_tsquery тому що ми генеруємо структурований синтакс - не websearch_to_tsquery тому що ми - МСК0 - вже самі розшифровували операторів, - МSK1 - даючи нам більше контролю над тим, як об 'єднуються умови, - .
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;
}
Чому б не просто використовувати
websearch_to_tsquery? Тому що він все ще не працює на технічному рівні, акронімиM SK1 та домені- специфічна синтаксиса MSC3 і не дає ніяких Hakів для гібридного рейтингу чи падінь . Розшифровуючи самих себеMska5 ми можемо обговорити ці крайні випадки перед тим, як вони досягнуть PostgreSQLMска6
І BuildSearchQuery метод був повністю переписан, щоб використовувати структуру розшифровки запитів. Зауважте, що baseQuery вже відфільтровані по мові, діапазон датиМSK1 і видимість M SK2 ми' ми додаємо пошукMSC4конкретні умови нагорі :
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;
}
Ось кілька прикладів покращених пошукових можливостей:
DiSE
✅ Тепер збігає статті з МSK1DiSEM SK2 в заголовку або змісті
ASP.NET
✅ Знаходить статті про ASPM SK1NET (автоматично конвертоване в МSK3aspnet" для повногоMSC5текстового пошукуMスク6
"semantic search"
✅ знаходить точну фразу " пошук семантики МSK2 в статьях
ASP.NET -Core
✅ знаходить статті ASPM SK1NET, але не включає ті, що згадують "Core"
ASP*
✅ Matches МSK1ASPM SK2 "AspNET", "ASPNetCoreMSC6 і т.д.
"full text search" PostgreSQL -MySQL
✅ знаходить статті з точною фразою " пошук у повному тексті МSK2 І згадки PostgreSQL МSK3, але за винятком статей, які стосуються MySQL
Запитання: ASP.NET and Alpine
До (перерванаM SK1
Після (фіксованийM SK1
Поиск інтегрується з об 'єднанням реаквівалентного рангу (RRFM SK1, як вказано в попередній семантичний пошук article. РР тут для рейтинг, не запам 'ятовує МSK1 він вже об' єднує МSK2 отримані результати з BM 25 повний ХМSK4 пошук тексту та векторного семантичного пошуку
// Fuse using RRF with category/freshness boosts
var fusedDtos = _ranker.FuseResults(bm25Results, vectorResults, query);
Алгоритам RRF використовує 1/(k+rank) де k=60 об 'єднує результати з багатьох джерел ,, тоді застосовує підштовхування для МSK2
Для повної implementaції RRF і того, як працює гібридний пошук, читайте Семантичний пошук в дії. Для глибших занурень у вбудова та векторну подібність Збудову " Адвоката ГПЗМSK1 МСК2 Частину МСК3. Схожа інфраструктура дає можливість як для користувача - орієнтуватися на пошук, так і для отримання RAG для штучного інтелекту
Нові компоненти зареєстровані як послуги:
services.AddSingleton<SearchQueryParser>();
services.AddSingleton<SearchRanker>();
services.AddScoped<BlogSearchService>();
SearchQueryParser і SearchRanker єシングルтонами, тому що вони ' є безладними МSK1 нитка МSK2 безпечні , і можуть бути повторно використані в різних запитах BlogSearchService застосовується, тому що він має доступ до контексту бази даних.
Цей самий семантичний компонент пошуку також використовується Адвокат GPT система, яка відображає важливі минулі блогові записи для штучного інтелекту-авторизоване письмоM SK1 См. Частина 4 для більш детальної інформації про канал запилення.
Поиск базується на SearchVector в столбці BlogPosts таблиця:
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");
GIN (Ogólnий Індекс Inverted МSK1 надає швидке повнеM SK2 пошук тексту через великі текстові Корпорації.
В той час ILIKE (caseM SK1insensitive LIKE) is slower than full
Цей підхід не збільшуватиме масштаб до произвольного пошуку підтримками на мільйони рядків - але для цільового зворотнього відхилення акронімів це ' потрібне МSK2
ts_rank_cd)PostgreSQL пропонує дві рейтингові функції для повного-текстового пошукуM SK1
ts_rank: Початкова частота вираження термінівts_rank_cd: Рейтинг щільності оболонки ( наскільки близькі умови виглядають разом МSK2Ми використовуємо ts_rank_cd тому що він надає Різниця між BM25- і з огляду на близькість терміну
// Order by cover density ranking - rewards term proximity
orderedQuery = searchQuery.OrderByDescending(x =>
x.SearchVector.RankCoverDensity(EF.Functions.ToTsQuery("english", tsQuery)));
Чому? ts_rank_cd superior:
Метрічний МSK1 ts_rank |
ts_rank_cd |
|
|---|---|---|
| Алгоритм | Двічна частота МSK1 щільність покритості МSK2приблизність ) мSK4 | |
| Багато-запитів на слова | Вираховує умови окремо МSK1 Правила винагороди, які з 'являються разом МSK2 | |
| До прикладу: " контейнери для дротівM SK2 | Artykuł з МSK1докеромM SK2 МSK3 часто набирає високих балів | Artykuł із мSK5доkkerовими контейнерами" разом набирає більше балів |
| Результати | Швидкий | Трохи повільніший МSK2 все ще спирається на індекс ГИН МSK3 |
| Подібність | Прості підрахунки МSK1 Приблизні дані BM МSK2 |
Це швидка оптимізація - кращий рейтинг релевантності з нульовим застосуванням -вирахунки рівняM SK2 Всі рейтинги відбуваються в PostgreSQL, використовуючи наявний індекс GIN на SearchVector.
Ссылки:
Окрім відновлення пошукової функції, деякі ключові оптимізації покращують ефективністьM SK1
Доступні мови рідко змінюються, але їх запитували на кожному пошуковому запиті.
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);
Вплив: Виключає 1 базу даних для кожного запиту
Використовений оригінальний код foreach цикли, що створюють багатоklausників, де застосовуються clauses WHERE. Тепер поєднані в одиниці виразівM SK1
// 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}%")));
}
Вплив: Прозоріший SQL, ~5-10% швидший для wieluM SK3term queriesMSC4
Додали частину індексу з колонами INCLUDE для частого доступу:
CREATE INDEX idx_blog_posts_search_covering
ON mostlylucid."BlogPosts" ("LanguageId", "IsHidden", "ScheduledPublishDate")
INCLUDE ("Id", "Slug", "Title", "PublishedDate")
WHERE "IsHidden" = false;
Вплив: Включає indeks- тільки сканує - PostgreSQL не мусить ' мати доступу до табличного купуля МSK2, зменшуючи IM SK3O на МSK4 для поширених запитів
Корінь EF's Include() завантаження повних об 'єктів навігації. Виключено, коли тільки ориентації навігуації є використані в пунктах де .
// 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")
ВпливМSK0 ~5-10% скорочення пам 'яті МSK2 менше даних, передаваних з бази даних .
Ссылки:
Поиск будує, де clauses інкрементально використовують Конструкція дерева вираження 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(...); }
Це генерує один оптимізований SQL запрос замість багатого циклу подорожей. PostgreSQL' запланувальник запитів може використовувати статистику та індекси ефективно, коли бачить цілу klauzulę де .
Об 'ємний вплив усіх оптимацій:
| МSK0 Оптимізація | Вплив на тривалість МSK2 Вказ на завантаження DB | |
|---|---|---|
| Caching мови МSK1 Minimal МSK2 -1 query/request M | ||
| Позбутись У тому числі МSK1 МSK2 ♫ ♫ -5-10% ♫ | ||
| Batch ILIKE | ||
| Індекс коригування МSK1 МSK2 | Індексус | |
| МСК0 tsМSK1rank_cd M SK3 Схоже МSK4 Краще співпадіння | ||
| Установка в буфері МSK1маршрутні парами МSK2 | N мSK4 A M | Захищає застарілі дані МСК6 |
Загалом очікувана покращення: 30-50% швидший пошук з значним зниженням завантаження бази даних.
Не припускайте, що пошук текстів повний: Машини з краями, такі як акроніми та особливі символи, потребують спеціального обробкиM SK1
Об 'єднати кілька підходів: ПолнеM SK1 пошук тексту (BM25) МSK4 семантичний пошук МSK5 векториMSC6 + зворотній відхилення підтримки дає краще покрытие, ніж будь-яка інша методика
Google формує очікування користувачів: підтримка зацитованих речей , винятків M SK2 і wildcards робить пошук природнім, тому що користувачі вже знайомі з цими операторами
Допоможіть відхиленням: Коли пошук зазнає невдачіM SK1 не показує' не показує нічого - показує останні пости і робить зрозуміло, що не було знайдено жодного співпадіння
Розібрати, donM SK1t hack: Правильний розшифровувач запитів є чистішим і більш підтримливим, ніж ряд маніпуляцій струн
Лівержування PostgreSQL'сбудовані-в рейтингі: Використовувати ts_rank_cd замість ts_rank для BM25-подібна значущістьM SK1Рейтинг щільності обкладинки вважає близькість терміну - швидкий перемога, що покращує якість результатів з нульовим застосуванням
Профиль перед оптимізацією: Очевидні " перешкоди МSK3 Цифровий аналізFTS МSK4 не були проблемою, МSK5 не є справжньою проблемою мSK6 пошуки мов M, надмірне включає в себе МСК8 і недосяжні індекси мали більший вплив, ніж очікувалося ммSK9
Операції з базою даних: Множество foreach цикли, що створюють окремі clauses, де генеруються неoptimalні SQL. Usage Any() або All() для поєднання в одиниці виразів.
Можні покращення для майбутнього
Поліпшення пошуку:
Поліпшення аналізу:
category:ASP.NET операторafter:2025-01-01 операторПереробка рейтингу:
Обсервабельність:
Виготовлення будівельних будівель requires fixing edge cases і оптимізувати продуктивність. Цей artykuł охватив як fixing PostgreSQL full, так і cases of text search edge. (, acronyms, ,, technical terms, МSK5, Google,-, operators of style, ), implementing key performance optimizations. ts_rank_cd).
Втілення досягає 30-50% швидший пошук зменшуючи завантаження бази даних за допомогою:
ts_rank_cd) для кращої значущостіНайголовніша ідея: жоден єдиний підхід не вирішує всі випадкиM SK1 PostgreSQL FTS чудово працює з співпадінням ключів кліпу , семантичний пошук відповідає за концептуальні питання МSK3 та цільові відхилення охоплюють крайні випадки M SK4 чиста warstwа аналізу об 'єднує їх , об' єднає оператори та технічні терміни перед тим, як вони дістаються до пошукових машин MSC6
Семантична пошукова інфраструктура працює для двох цілей:
Подібні статті:
офіційна документація:
Весь код доступний в блог's Репозитори GitHub.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.