在 Ecto 中集成全功能搜索引擎 ParadeDB
现有搜索方案的局限
现有针对 Postgres 的文本搜索方法存在明显局限:
- 基础字符串操作:
LIKE/ILIKE、模糊匹配、三元组等仅适用于短字符串的模糊匹配,不适合长文本搜索。
- Postgres 全文搜索:集成方便,但功能与排序能力有限。
- 专用搜索引擎:如 Elasticsearch、Solr(基于 Lucene/JVM)或 Meilisearch(基于 Tantivy/Rust)功能强大,支持 BM25 排序和复杂查询,但需与数据库同步数据,增加 ETL 管道和运维复杂度。
ParadeDB 与 Ecto 的集成
ParadeDB 是 Postgres 扩展集,通过 pgrx 嵌入 Tantivy 引擎,提供高级搜索与分析功能,且无需数据同步。作者探索了在 Elixir 的 Ecto 库中支持 ParadeDB 的可行性,并构建了示例项目(使用警察通话录音数据)。
集成优势
- 免同步、ACID 一致:搜索索引在迁移中创建,数据增删改时自动更新,保持事务一致性。
- 查询灵活组合:搜索语句可与常规 SQL/Ecto 查询无缝结合,实现条件过滤、关联和字段选择。
- 开发体验优异:借助 Ecto 的现有功能,提供比独立搜索引擎客户端更强大的组合能力。
查询示例
基本搜索:查找含 "drive" 或 "driving" 的记录。
from(c in Call, search: parse(c, "transcript:drive")) |> Repo.all()
组合查询:限制通话时长并选择字段。
from(
c in Call,
search: parse(c, "transcript:drive"),
search: int4range(c.call_length, 5, 10, "[]"),
select: {c.id, c.transcript}
) |> Repo.all()
复杂布尔搜索:包含 "ambulance" 且高权重匹配 "ALS"/"BLS" 或包含短语。
query = from(c in Call,
search: boolean([must: [
parse(c, "transcript:ambulance"),
disjunction_max([
boost(parse(c, "transcript:(BLS ALS)"), 10),
parse(c, "transcript:(fall)"),
phrase(c.transcript, ["routine", "response"], ^slop)
])
]]))
Repo.all(query)
关联查询:与关联表结合,进行条件过滤和选择。
query
|> join(:inner, [c], tg in assoc(c, :talk_group))
|> where([_, tg], ilike(tg.tag, "%dispatch%"))
|> search([c, _], int4range(c.call_length, nil, 20, "(]"))
|> select([c, tg], %{ id: c.id, text: c.transcript, ... })
|> Repo.all()
当前状态与未来计划
- 当前实现:尚未达到生产就绪,还未支持 ParadeDB 的高级功能(如 BM25 评分、混合搜索、聚合等)。
- 未来方向:作者计划维护一个下游分支(
moosieus/ecto 和 moosieus/ecto_sql),持续集成 Ecto 更新,并希望社区参与贡献。
- ParadeDB 潜力:作为较新的项目,其团队活跃,功能仍在快速演进。
问答澄清
- 为何不使用 fragments:ParadeDB 查询需要深度组合能力,超出 fragments 的范畴。
- Postgrex 扩展:ParadeDB 使用标准数据类型,无需特殊编解码扩展。
- 分支更新策略:下游分支将定期合并 Ecto 官方更新。
总结:在 Ecto 中集成 ParadeDB 有望将全功能搜索引擎直接嵌入 Postgres 应用,消除同步负担,提升开发效率与查询灵活性,但仍需进一步完善。