BloomSearch:使用Bloom过滤器搜索关键字


面向海量数据集的分层布隆过滤器关键词搜索引擎,使用布隆过滤器替代B树实现数据索引!

BloomSearch通过可插拔存储接口提供极低的内存使用率和低冷启动搜索。

  • 内存效率:Bloom过滤器具有恒定的大小,无论数据量如何
  • 可插拔存储:适用于任何后端的DataStore和MetaStore接口(可以相同或独立)
  • 快速过滤:通过分区、最小最大索引和布隆过滤器进行分层修剪
  • 灵活的查询:使用AND/OR组合符按field、token或field:token进行搜索
  • 分解的存储和计算:未绑定的摄取和查询吞吐量
非常适合日志、JSON文档和高基数关键字搜索。

背景上下文
这个周末我被一个叫"布隆过滤器"的学霸神器给惊艳到了!这玩意儿简直就是处理海量数据的扫地僧啊!

简单来说,它就像个不会吃内存的贪吃蛇:

  • - 普通数据库索引(比如B树)会随着数据变多越来越胖
  • - 但布隆过滤器永远保持苗条身材,特别适合在"垃圾堆里找金针菇"的场景(比如查日志、搜文档)

Google Chrome和Bing都在用它的亲戚BitFunnel!我以前也玩过这技术,但直到最近要给海量关键词建搜索引擎时才真正"真香"了 —— 结果一研究就掉进技术兔子洞了!

(掏出小本本)现在我把这黑科技打包成了Go语言版的"BloomSearch",贼拉好用:
1. 内存省到哭:用"布隆过滤器+流式扫描"组合拳
2. 百搭存储:随便接MySQL/Redis/文件系统都行
3. 智能瘦身三连招:分区存储+范围索引+过滤器剪枝
4. 搜索语法贼灵活:支持字段、关键词、组合搜索
5. 性能炸裂:读写分离设计,吞吐量直接起飞

我还偷偷搞了个专属文件格式...嘘!

这工具专门治各种不服:
- 海量数据?小case!
- 高并发?洒洒水啦!
- 内存不够?不存在的!

目标是在分布式系统里做到每秒100亿行的查询速度,顺便把公司的日志存储费从"切肾价"打到"奶茶价"(0.003美元/GB)!

虽然还能继续优化,但现在已经香到我想原地转圈圈了~

原理
使用Bloom过滤器替换当前日志?Bloom过滤器非常擅长确定某个东西不存在于集合中。你在这件事上的立场是什么?换句话说,如何查询布隆过滤器后面的数据集?

用布隆过滤器搞日志查询就像在超市用「商品探测器」—— 虽然不能直接告诉你货架上具体有啥,但能瞬间告诉你「肯定没有啥」!

举个栗子:
比如你这条JSON日志:

json
{
  "user": {
    "name": "John",
    "tags": [
      {"type": "user"},
      {"role": "admin"}
    ]
  }
}
会被拆解成三把「探测枪」:
1. user.name 探测器 → 存了 "John"
2. user.tags.type 探测器 → 存了 "user"
3. user.tags.role 探测器 → 存了 "admin"

查询时的骚操作:
情况1:查 包含"John"的日志
  - 直接让「值探测器」扫射,如果返回「不存在」→ 直接跳过文件
  - 如果返回「可能存在」→ 再开箱检查真实数据

 情况2:查 user.tags.type字段存在的日志
  - 掏出「字段存在探测器」biubiubiu~
  - 没有信号?立刻抛弃整个文件!

情况3:查 user.tags.type="user"且role="admin"的日志
  - 先让两个「字段值探测器」交叉火力覆盖
  - 只有两个探测器都亮绿灯,才需要翻真实数据

核心秘籍:
1. 三层过滤网:每个文件配备三套布隆过滤器(字段存在/值存在/字段值组合)
2. 快速淘汰:任何一层返回「绝对不存在」就直接跳过IO操作
3. 精确验证:只有通过所有过滤器检测的,才需要加载真实数据细查

这就像高考阅卷:
- 布隆过滤器是「机读卡识别」→ 快速筛掉明显不合格的
- 真实数据查询是「人工阅卷」→ 只对潜力股精细评判

所以用布隆过滤器不是要替代存储,而是给存储戴上一个「智能头盔」(索引)!既能防住90%的无用查询,又能保证100%的准确性(虽然可能有误报,但绝不会漏网)

布隆过滤器
布隆过滤器是一种用于测试集合成员关系的概率数据结构。它们保证没有假阴性,但允许可调的假阳性。无论数据量如何,都具有恒定的大小,具有极快的查找速度和最小的内存使用。

搜索类型
BloomSearch支持针对JSON文档的三种类型的搜索:

给定的示例日志记录:

{"level": "error", "service": "auth", "message": "login failed", "user_id": 123}
{
"level": "info", "service": "payment", "message": "payment processed", "amount": 50.00}
{
"level": "error", "service": "payment", "message": "database timeout", "retry_count": 3}

字段搜索-查找包含特定字段路径的记录:

// Find all records with "retry_count" field
query := NewQueryWithGroupCombinator(CombinatorAND).Field(
"retry_count").Build()

令牌搜索-查找任何位置包含值的记录:

// Find all records containing "error" in any field
query := NewQueryWithGroupCombinator(CombinatorAND).Token(
"error").Build()

Field:token search -查找特定字段中具有特定值的记录:

// Find all records where <code>.service: "payment"</code>
query := NewQueryWithGroupCombinator(CombinatorAND).FieldToken(
"service", "payment").Build()

分布式查询处理(问题)
查询处理自然会分解为独立的行组任务,这些任务可以分布在多个节点上。由于结果是异步流回的,没有顺序保证,这就创建了一个完美的可并行化的工作负载。

  1. 构建查询-协调器使用bloom条件和预过滤器构建查询
  2. 预过滤器MetaStore -协调器使用分区和MinMax索引(如果可能)识别候选文件
  3. 将工作分散到对等体-协调器将行组处理任务分散到可用对等体
  4. 对等进程行组-每个对等进程独立执行布隆过滤器测试和行扫描
  5. 将结果流回协调器-对等点通过唯一的查询ID将匹配的行直接流到协调器


极客辣评


是否意味着不能查询子字符串或模糊搜索?

BloomSearch引擎就像个智能炒菜机,TokenizerFunc就是你的「切菜刀法」,决定食材(数据)怎么预处理!

默认刀法:空格斩

json
{"name": "John Smith"}
➡️ 被剁成:[{Path: "name", Values: ["john", "smith"]}]
就像把"John Smith"按空格切丝,然后:
- 存字段名:"name"(菜品种类)
- 存单词:"john"、"smith"(食材)
- 存组合:"name:john"、"name:smith"(特定菜谱)

查询时的秘密:必须用同款刀法!
- 如果你用「空格斩」存了"John Smith"
- 查询时却用「整块红烧」搜索"john smith" → 会漏锅!
(这就是为什么Tokenizer必须一致)

️ 高级定制刀法示例:
1. 模糊搜索:用「三明治刀法」(trigrams)
   - "hello" → ["hel", "ell", "llo"]
2. 词根搜索:用「去尾刀法」(stemming)
   - "running" → ["run"]
3. 中文搜索:用「庖丁解牛刀」(分词器)
   - "我爱北京" → ["我", "爱", "北京"]

核心设计哲学:
- 精准狙击:专治「我知道要找A+B+C,给我精确匹配」的场景
- 不做联想:不像搜索引擎会猜你想要"John→Johnny"
- 可扩展性:通过换刀法支持特殊场景(但默认配置已够香)

(突然抄起锅铲)举个实战例子:

go
// 自定义分词器:按驼峰切割
myTokenizer := func(value string) []string {
    return regexp.Split(value, "(?=[A-Z])") 
}
// "userName" → ["user", "Name"]
这样就能实现字段级驼峰搜索啦!但记住——存数据和查数据必须用同一把刀,否则会炒出黑暗料理!


布隆过滤器最终还是要扫行数据,但整个过程就像用三层安检筛掉99%的无关区域

图书馆寻宝四部曲:
1. 分区筛选(书架区域)
   - 先锁定「计算机类书籍区」(排除文史哲)
2. MinMax索引(书架标签)
   - 确认这个书架有ISBN 100-200的书(跳过其他书架)
3. 布隆过滤器(书本指纹)
   - 快速检查「这本书可能有我要的公式」(10毫秒)
4. 行组扫描(翻书验证)
   - 实际翻阅的只有1-2本书(而不是整个图书馆!)

⚡ 性能对决时刻:

| 方案         | 索引大小  | 查询速度       | 适用场景              |
|--------------|-----------|----------------|-----------------------|
| B-Tree       | O(N)      | O(log N)       | 精确查询+频繁更新     |
| 布隆过滤器   | O(1)      | 近似O(1)       | 存在性检查+海量数据   |

关键洞察:
- B-Tree像查字典:每次查询稳定快,但字典本身越来越厚
- 布隆过滤器像金属探测仪
  - 先快速扫整个沙滩(排除没有金属的区域)
  - 最后只在可能有宝藏的地方挖(行组扫描)

看看现实世界的优化效果:

text
原始数据:1TB日志
↓ 分区裁剪 → 剩余100GB
↓ MinMax索引 → 剩余10GB 
↓ 布隆过滤器 → 最终扫描10MB
这时候的「行组扫描」其实只检查不到0.001%的数据

就像这篇日志系统设计详解说的:
> "现代日志系统通过分层过滤,把TB级查询变成MB级扫描"

(敲重点)所以布隆过滤器方案的精髓是:
1. 用极小内存代价(比如1MB布隆过滤器能代表1TB数据)
2. 实现前置暴力过滤
3. 让最后的精确查询变成「针尖上跳舞」

就像COVID核酸检测:
- 布隆过滤器是快速抗原检测(先大规模筛阴性)
- 行扫描是PCR检测(只对可疑样本精细检测)
这样整体效率反而比全员PCR高十倍!