过滤器提取:正则表达式词边界匹配
本文介绍了我们如何通过词边界正则表达式匹配短语到过滤器的映射,从自然语言搜索查询中提取结构化产品过滤器。
问题:从自然语言到结构化过滤器
当用户搜索“带16GB内存的迷你电脑”时,我们需要提取:
{
"Form Factor": "Mini PC",
"Main Memory": "16"
}
这些结构化过滤器支持:
-
产品过滤:仅显示匹配的产品
-
分面导航:显示可用的过滤器选项
-
查询页面生成:创建SEO优化的页面
-
相关搜索:查找相似的查询
挑战在于处理用户表达相同意图的所有不同方式。
算法:词边界正则表达式匹配
步骤1:加载短语映射
我们加载SEO流水线生成的短语到过滤器的映射。这些映射将短语连接到过滤器值,允许从短语查找到对应的过滤器键和值。
我们反转这个结构以便快速查找,这样我们可以按短语而非过滤器进行搜索:
phrase_to_filter = {
"16gb ram": ("Main Memory", "16"),
"16 gb ram": ("Main Memory", "16"),
"mini pc": ("Form Factor", "Mini PC"),
"mini computer": ("Form Factor", "Mini PC")
}
步骤2:规范化查询
将查询转换为小写以进行不区分大小写的匹配。
步骤3:使用词边界匹配短语
对于映射中的每个短语,我们使用正则表达式中的词边界锚点检查它是否出现在查询中。
\b锚点确保我们匹配完整的单词,而不是子字符串:“mini pc”匹配“mini pc with ram”,但不匹配“minipc”(无空格);“16gb”匹配“16gb ram”,但不匹配“216gb”(部分匹配)。
步骤4:收集所有匹配项
我们遍历所有短语并收集匹配的过滤器。对于“mini pc with 16gb ram”,这会生成像{"Form Factor": "Mini PC", "Main Memory": "16"}这样的过滤器。
为什么使用词边界?
词边界防止错误匹配:
没有词边界时:
-
“i5”会匹配“i5000”(错误)
-
“ram”会匹配“program”(错误)
-
“pc”会匹配“pcie”(错误)
使用词边界时:“i5”匹配“i5 processor”✅但不匹配“i5000”❌;“ram”匹配“16gb ram”✅但不匹配“program”❌。\b锚点确保我们只在单词边缘匹配。
处理多个匹配项
如果多个短语匹配同一个过滤器,则最后一个匹配项胜出:
# 查询: "mini pc small computer"
# "mini pc" 和 "small computer" 都映射到 "Form Factor:Mini PC"
# 结果: {"Form Factor": "Mini PC"} (去重后)
如果多个短语匹配同一过滤器的不同值,则最后一个匹配项胜出:
# 查询: "8gb 16gb ram"
# "8gb" → Main Memory:8
# "16gb" → Main Memory:16
# 结果: {"Main Memory": "16"} (最后一个匹配胜出)
实际上,用户很少指定冲突的值,所以这不是问题。
短语优先级
短语按照它们在映射中出现的顺序进行匹配。由于映射是按相似度(最高优先)和长度(最短优先)排序的,因此会先检查质量更高的匹配项。然而,由于我们遍历所有短语,顺序不会影响最终结果——最后一个匹配项胜出。
性能优化
缓存
短语到过滤器的映射在启动时加载一次并缓存在内存中,避免了每个请求的重复磁盘I/O。
Valkey 后备方案
我们首先尝试从Valkey(Redis的分支)加载映射,如果Valkey未命中则回退到JSON文件:
- 检查Valkey以进行快速的内存查找
- 如果Valkey未命中,则从JSON加载
- 将数据存储到Valkey并设置缓存过期时间
这减少了后续请求的延迟。
正则表达式匹配
模式使用带有词边界锚点的re.search()来高效地匹配完整的单词,避免错误的子字符串匹配。
与搜索服务集成
过滤器提取实现为一个API服务端点,该端点接受搜索查询并返回提取的过滤器。
该服务:
- 接受搜索查询作为输入
- 将短语到过滤器的映射加载到内存中
- 使用词边界对短语进行规范化处理和匹配
- 返回结构化的过滤器键值对
主Web服务器调用此服务以从用户查询中提取过滤器:
filters = extract_filters_from_query("mini pc 16gb ram")
# 返回: {"Form Factor": "Mini PC", "Main Memory": "16"}
这种关注点分离允许:
-
独立扩展:过滤器提取可以在单独的服务器上运行
-
缓存隔离:服务管理自己的映射缓存
-
服务重启:服务可以独立重启而不影响主Web服务器
详情请参阅搜索服务架构。
查询日志记录
每次过滤器提取都会被记录下来,供SEO流水线使用。这些日志会反馈到SEO流水线中,用于发现新的查询模式并随着时间的推移改进短语映射。
使用场景
查询页面 (/q/)
查询页面从URL slug中提取过滤器,以确定要显示哪些产品。
搜索API (/api/search)
搜索API从搜索查询中提取过滤器,以查找并返回匹配的产品。
自动补全
自动补全建议提取过滤器,以在搜索建议旁边提供过滤器预览。
相关搜索
相关搜索生成使用提取的过滤器来查找相似的查询:
查询: "mini pc 16gb ram"
→ 过滤器: {"Form Factor": "Mini PC", "Main Memory": "16"}
→ 查找具有相似过滤器的查询
→ 建议: "mini pc 32gb ram", "mini pc 16gb ssd"
详情请参阅相关搜索生成。
错误处理
缺少映射
如果未加载短语映射,则返回空过滤器。这可以防止在SEO流水线尚未运行时发生崩溃。
无效查询
空查询或仅包含空格的查询返回空过滤器。
正则表达式错误
我们使用正则表达式转义来在正则表达式匹配之前清理短语,防止短语中的特殊字符导致语法错误。
性能特征
由于使用词边界正则表达式匹配和积极的缓存策略,过滤器提取运行高效:
-
映射加载在启动时进行一次
-
每次查询的提取受CPU限制(正则表达式匹配)
-
缓存的内存使用保持在可管理范围内
-
生产环境中的缓存命中率很高
词边界正则表达式比子字符串搜索更快,因为正则表达式引擎可以高效地跳过不匹配的位置。
局限性
短语顺序依赖性
我们按照迭代顺序匹配短语,但顺序无法保证。如果两个短语重叠,最后一个匹配项胜出:
# 查询: "mini pc"
# 短语: ["mini", "mini pc"]
# 如果最后检查"mini",它会覆盖"mini pc"
实际上,这种情况不会发生,因为:
-
较长的短语更具体,在排序后的映射中排在前面
-
重叠的短语通常映射到相同的过滤器值
无短语组合
我们不将多个短语组合成单个过滤器值:
# 查询: "dual core quad core"
# 结果: {"Cores": "4"} (最后一个匹配胜出)
# 不是: {"Cores": ["2", "4"]} (多个值)
这是有意为之——用户很少为同一个过滤器指定多个值。
无否定处理
我们不处理否定(例如,“不带Windows的迷你电脑”)。否定在搜索查询中很少见,因此目前不支持。
参考文献
技术概念
Python文档
-
re.search() - Python文档
-
re.escape() - Python文档
相关文章
总结
我们使用词边界正则表达式匹配从搜索查询中提取过滤器:
-
加载映射:从SEO流水线加载短语到过滤器的映射
-
规范化查询:转换为小写
-
匹配短语:使用
\b词边界匹配完整单词 -
收集过滤器:构建过滤器键值对字典
-
积极缓存:内存缓存 + Valkey后备方案
-
记录查询:反馈到SEO流水线
该算法简单、高效,并能处理SEO流水线生成的所有短语变体。词边界防止错误匹配,同时允许灵活的短语匹配。结果是强大的过滤器提取功能,为查询页面、搜索、自动补全和相关搜索提供支持。