在真正用户开始输入真实查询之前,它看起来微不足道。 @ - @ acronnyms@ MS K2 一半的- 记得技术术语@,_ punctuation}-#Heavy names like @"_ ASPQ.NET}(或搜索) 概念上 但文字错误@. 当这些案例的搜索失败时 @ , @ 用户d '}t think @ MPK3_ 前置案例 @"} @ @ I-# 他们认为网站被打破了 @MS K6 @
从我观察这个网站开始以来, 它就一直困扰着它。 开放搜索, @Q}在 PostgreSQL 的完整文字搜尋中使用它@'#%s flatal 矢量素材, 但從來都不對此感到高興。 已窃听 特别是现在,I'm在 清晰RAG 区域包 我觉得我应该把它修好 是否是另一个问题 @. @
此篇文章涉及从零开始建立搜索引擎@. @ it'}有关在真实生产系统中修补 PostgreSQL full @-}文本搜索的尖锐边缘... .\ I'* 会走过特定失败, 我打中了%,}为什么它们会发生?,\ 和使搜索变得符合用户已经期待的方式所需的实用修正 {-}包括缩略语处理@MS K9}技术术语解析_,\ 短语 search\ ,} 排除=,} 以及 Google-}样式操作器\ MPK14}
以先前工作为基础本篇文章延伸至 语义搜索实施 这篇文章修正了执行失败的边缘案例: <:> 缩略语, 它与{'<,}技术术语和特殊字符匹配;自然查询是PostgreSQZL=9s passer break_.}
语义搜索基础设施具有双重用途@: @ it power the site='}%s user_-facing search 和 提供该数据的检索层。 GPT律师 RAG 系统 @-a 写作助理, 用网站@'#现有内容作为知识库来起草新职位=%. GPT "+#–+部分 嵌入和矢量搜索@. @%
当一个搜索返回没有结果时, 没有反馈@, @ 系统显示随机的旧文章而不是有用的建议@ .} 这违反了最不出人意料的原则 {-} 用户期待相关结果或明确的 {"}no 匹配"} mostK4 message 与最近的文章相匹配作为建议}.}这让搜索看起来有效了 只是严重地{.
固定: 修改 BlogSearchService.HybridSearchWithPagingAsync() 以在搜索返回零结果后进行检测,然后返回到显示按日期排列的最近职位 NoMatchFound 显示为 BasePagingModel 所以 UI 可以显示一个合适的消息, 如 @ " @ no count found@ MS K1_ _ 您指的是其中之一吗? @ ?"}
// 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 ' @%s 默认英文文本搜索配置技术索引缩略语@,}但正常化和断开使得短,案例=-}在实际操作中重要术语不可靠 *.}当您寻找“"DISE}",}它被小写到‘"dise"}然后英语字典可以丢弃它或指定低重量的{,},从而导致完整- text搜索以错过标题和内容中明显包含 &"DiseMSMK12}的文章}
固定: 添加了缩略语检测@(Terms @≤6字符,上面有字母}) 以及用 PostgreSQL#'s 表示对子串敏感 ILIKE@._% 此功能补充完整@-# text search, 而不是取代它 @MS K2} <请参看下面的性能考量: 为什么这可以接受):
// 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}%"));
搜索 @"+ASP.+NET}或@"\C#"}将会失败,因为 PostgreSQL='}文本搜索解析器将时段和散列符号作为分隔符 {,} 将 &"ASP}.NET"拆分为 @MS K10ASS_"}和MS K12Net}MS13}这意味着要查找准确的术语 *%'#t working.}
固定*:* 创建 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 将 @"和"作为停止单词处理, 从搜索中删除@.。 加上特殊字符问题,一个自然查询, 如 @MS K4ASP}.NET 和 Alpine"基本上会成为搜索 *"Alpine " 仅
固定: 實施了一個 Google {-} 樣式的查詢解析器, 可以智慧地處理停止語言, 並支援高级搜尋操作員.}
我实施了一个 SearchQueryParser 类中使用支持":"来切换查询的类别
"exact match" - 搜索确切的短语-unwanted - 不包括包含此术语的结果ASP* @-Q 匹配@"ZASP",QQ#"ASPNET}","+ASPCree",等解析器使用已编译的正gex 模式表示查询@ :
[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'%s tscquery syntax} 我们使用的数据 to_tsquery 因为我们正在生成结构化的语法 *\ -\ {不是} websearch_to_tsquery 因为我们已经解析了操作员 我们自己 , 给我们更多的控制 如何将条件合并
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? 因为它在技术术语“,”上仍然失败, 缩略语“ ,” 和域名“ -” 特定语法“ @-” 没有提供混合排名或回补的钩子@.” 。 通过自我剖析“MS K5” 我们能够在这些边缘案例到达 PostgreSQL.之前处理它们。
缩略 BuildSearchQuery 完全重写的方法使用解析查询结构@.}请注意 baseQuery 使用语言已过滤@,}日期范围 @ ,和可见度 @ I- we'在上方添加搜索@ -特定条件\ :
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
@✅ 现在将文章和标题或内容中的 "DISE"匹配@(case{-#inchnize)
ASP.NET
@✅}尋找關於 ASP.NET {(} 自动轉換成{MS K3aspnet"}的文章, 完整@- text search_)
"semantic search"
“✅”在文章中查找准确的短语@ " @semantic search@"
ASP.NET -Core
@✅ 找到 ASP.NET 文章,
ASP*
✅Q配对@"ZASP",Q @"ASPNET}","*ASPNetCore#",等等
"full text search" PostgreSQL -MySQL
“✅”查找精确短语为“"”的文章,全文搜索@"和提到PostgresSQL,}但排除提及 MySQl 的文章
查询: ASP.NET and Alpine
之前 (Broken):
之后 (Fixed):
该搜索与在《公约》中执行的对等阵列合并 +(RRRF) 先前的语义搜索文章.RRF在这里用于 排行排名, 記不起來- 它將 BMQ}-
// Fuse using RRF with category/freshness boosts
var fusedDtos = _ranker.FuseResults(bm25Results, vectorResults, query);
RRF算法的使用 1/(k+rank) k=60 来合并多个源的结果@ , 然后对\ MS K2应用助推
RRF 的完整实施及混合搜索如何运作 <,>见 语义搜索在动作中. 更深的潜入嵌入和矢量相似性=, 见 GPT "+#-+部分“.” 与“-”相同的基础设施授权用户“MS K1” 搜索和 AAG 检索 AI- 辅助书写@.}
新组件注册为服务@:
services.AddSingleton<SearchQueryParser>();
services.AddSingleton<SearchRanker>();
services.AddScoped<BlogSearchService>();
SearchQueryParser 和 SearchRanker 它们是单吨,因为它们 @'re Nationalitate@,}线性 @MS K2#seany_,}可以按请求重新使用=.} BlogSearchService 因为它访问数据库上下文@. @%
同一语义搜索组件也用于 GPT律师 系统以检索 AI- 辅助撰写@.\ 过去相关博客文章的系统 部分#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 {(} 通用反向索引} )} 提供快速完整搜索, 通过大文本 Corpora.}
期间 ILIKE (case-灵敏度如 @) 慢于全@-text search}, it{'}因为%:所以需要缩略语
此方法不会扩大为任意的子串搜索范围, 覆盖数以百万计的行@ - @} but for 目标缩略语 found it'_s applicate
ts_rank_cd)PostgreSQL 提供两个排位函数, 用于 full @ -_ text search@ MS K1}
ts_rank“:”基本用词频率“(”多少次显示“MS K2”ts_rank_cd“:”覆盖密度排行榜“(” 是否接近条件一起出现?“MS K2”我们用 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 属于上端@: @%
| Metric | ts_rank |
ts_rank_cd |
|---|---|---|
| 算数 @ | 定期频率@ | 覆盖密度 #( 近似性#) |
| 多@ - @ 字查询 | 计为单数 | 奖赏 并列 |
| 例如:: "docker 容器" | +#"@docker @"}#50+1+Docker conventions #"docker annex*$"#加起来是高分数{ | # |
| 业绩表现 实绩 “ | 快速”“ | 边际变慢”「, 仍能影响 GIN 指数 @ |
| 行为行为 @ | _简单计数@ | _Apbounds BM25}# |
这是 速赢优化 @ - 更好的相关性排序, 没有应用@ MS K1\ 级别计算@ MPK2} 所有排名都发生在 PostgreSQL, 使用现有的 GIN 指数 SearchVector.
参考文献::
除了修复搜索功能@,}几个关键优化可以改善性能 @:
可用的语言很少改变, 但在每个搜索请求中都被询问@ . @ 现在用双-}\ checked locking\ MS K2}缓存
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);
影响:消除了每个搜索请求的数据库查询@.
使用的原代码 foreach 环形循环创建多个条款区@. 现在被分解成单表达式 @:
// 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% ” 为多@- 加快时间查询的速率
为经常访问模式添加了 INCLUDE 列中的部分指数@:
CREATE INDEX idx_blog_posts_search_covering
ON mostlylucid."BlogPosts" ("LanguageId", "IsHidden", "ScheduledPublishDate")
INCLUDE ("Id", "Slug", "Title", "PublishedDate")
WHERE "IsHidden" = false;
影响*: 启用 索引@-}只扫描 - PostgreSQL doesn't MS K1} t need to access the table heap, 减少用于常见查询的I/O
EF 核心 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")
影响@: {~5-10% 内存减少}, 减去从数据库传输的数据 □.
参考文献::
正在逐步使用条款 EF 核心@' @%s 表达式树组成:
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(...); }
这产生 a 优化的 SQL 查询 “. PostgreSQL'}查询规划器在看到“.”条款的完整时,可以有效地使用统计和索引。
所有优化的累积影响
| QDB 负载撞击 * | * | |
|---|---|---|
| @ | _ 语言缓存 @ I | } 微小的 minimal @ MPK2 * MS K3} 查詢@ /} 請求@ |
| 移除包含 @() _ | @#-5-10% | 减数据传输@MS K5 |
| 批量 ILIGE MS K1 NSK2 I | 清洁 SQL | |
| 包含索引 @ | _-20-30% # | 指数#- 仅扫描@ |
| {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}相似的 | ||
| 缓存修补 @(route params@) MS K3 N/A @ | 防止停滞数据 @MS K6 |
预期改善总额: *#30-50% 快速搜索 大量减少数据库负载@. @%
Donó't 假设全部@-}文本搜索处理一切诸如缩写和特殊字符等:边缘案例需要特别处理@.
结合多种办法: full- text search @(BM}25)@+ 语义搜索{(VictorsMS K6 +子字符串回溯提供比任何单一方法更好的覆盖*.
Google塑造的用户期望值@:# 支持引用的短语@, 排除%,\ 和通配符使搜索感觉自然, 因为用户已经熟悉这些操作器 @MS K3}
提供有益的后退@ : @ * 当搜索失败时 ',# dono\ ' t 显示任何不显示 MS K3 > 显示最近的文章, 并声明找不到匹配的@.\
{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}小黑客: 适当的查询解析器比一系列字符串操作更清洁、更便于维护@.
使用 PostgreSQL' 建立@ - 排名: 使用 ts_rank_cd 代替 ts_rank 对于 BMQ25- 类似关联性@.覆盖密度排序认为“时间接近” @-是一个速赢,通过零应用来提高结果质量-级别代码
优化前的配置文件@: @MSK0@Obvious@"瓶颈 @MS K3_FTS 解析#)}是真正的问题吗?
批次数据库业务: 多 foreach 创建独立的循环,让条款产生亚优度 SQL.}使用 Any() 或 All() 组合成单表达式@. @%
未来可能作出的改进,}按关注分类*:
检索回收器改进:
剖析增强:
category:ASP.NET 操作员after:2025-01-01 操作员精炼:
可观察性:
建筑生产 - 高级搜索需要固定边缘案例 和 优化性能@. @% 此篇文章覆盖了以下两个功能: 修补 PostgreSQL full @ -} text sort search scription case {acronnyms,}技术术语 ,Google-}风格操作员MS K7}以及执行关键性能优化 *caching},}批量=,}涵盖索引>,} ts_rank_cd).
执行成果 *#30-50% 快速搜索 正在通过 @ :\ 减少数据库负载
ts_rank_cd*) 更贴切关键洞察@:}没有单一的方法能处理所有案例 . PostgreSQL FTS 在关键词匹配 < , 语义搜索中能够处理概念查询\ MS K3 和有针对性的后退抓边端病例. 干净的解析层将它连接起来\ , 处理操作员和技术条件,然后他们到达搜索引擎\□.}
语义搜索基础设施具有双重用途:
相关文章 @:
正式文件=:
所有可用代码 MPK0s GitHub 主目錄.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.