说起搜索, 大家脑子里会蹦出哪些关键词呢?
全文检索 , 中文分词 , 分词矢量 , 搜索条件 , 关键词匹配 , tf/idf , ranking , bm25 , elasticsearch , tantivy , Lucene , tsvector , tsquery , GIN索引 , rum索引 , zoombdb索引
可能差不多也就这些?
虽然PG内置了全文检索功能, 但是中文分词要依赖外部, ranking也有其局限性, 所以在PG的插件生态中依旧有很多插件厂商前赴后继的推出高级的分词插件, 例如vectorchord-bm25, 专注于中文分词的pg_jieba. 还有本文提到的paradedb.
让我们来看看 paradedb (号称PG用户的ElasticSearch替代品) 近期发布的 v0.20.0:更简洁,更快速! 到底如何?
内容来自: https://www.paradedb.com/blog/paradedb-0-20-0
每一个构建现代化应用程序的开发者都面临着同样的困境:您的用户需要实时搜索 (real-time search) 和分析 (analytics) ,但您的数据库架构却迫使您在一致性 (consistency) 和性能 (performance) 之间做出选择。故事通常始于“只是一个简单的搜索框”,但不可避免地演变成复杂的分布式架构 (distributed architecture) ,带来最终一致性 (eventual consistency) 、运营开销 (operational overhead) ,以及对搜索结果是否反映现实的持续担忧。
当 Ming 和我创立 ParadeDB 时,我们有一个简单的愿景:开发者不应该被迫在 ACID 事务 (ACID transactions) 的安全性与现代搜索 (modern search) 的强大功能之间做出选择。今天,通过 ParadeDB 0.20.0,我们正在使这个愿景变得更具性能 (performant) 且更易于使用。
ParadeDB 0.20.0 在对实际应用程序至关重要的三个方面实现了突破:消除了分析差距 (analytics gap) 的搜索聚合 (search aggregations) 、减少认知开销 (cognitive overhead) 的更简洁 API (Application Programming Interface) ,以及随您的应用程序扩展而非反作用于它的写入性能 (write performance) 。
现代应用程序通常需要的不仅仅是搜索:它们需要在搜索结果上进行分面 (facets) 、计数 (counts) 、直方图 (histograms) 和其他分析 (analytics) 。虽然专用搜索引擎 (dedicated search engines) 擅长聚合 (aggregations) ,但当您基于事务型数据库 (transactional database) 进行构建时,通常需要对查询规划器 (query planner) 进行一番“体操”,才能在不二次访问索引 (indexes) 的情况下让事情运转起来。
我们有一段时间以来一直拥有基于搜索查询的快速聚合 (aggregations) 功能,但这并不是我们经常谈论的事情。在 0.20.0 版本中,我们彻底改进了这项功能,使搜索分面 (search faceting) (计算记录组的计数)成为一等公民 (first-class citizen) 。
我们的搜索分析 (search analytics) 由一个新的 pdb.agg() 函数提供支持,该函数可以通过两种方式使用:
这两种变体都接管了部分查询规划和执行 (query planning and execution) ,将尽可能多的工作推送到 ParadeDB 索引 (ParadeDB index) 内部。这消除了对多重查询、公用表表达式 (CTEs) 或手动聚合 (manual aggregations) 的需求。
我们的聚合 (aggregations) 在大型搜索结果集(数百万条记录)方面表现出色,对于超过 4000 万条结果,搜索分面 (search faceting) 的性能比次优方法(ParadeDB 搜索返回所有结果,然后馈送到 PostgreSQL 聚合器)快了一个数量级(完整的详细信息将发布在另一篇搜索聚合 (search aggregations) 文章中)。
4200 万条结果的分面(搜索和聚合)查询时间

pdb.agg() 还可以与一个 JSON 参数一起使用,该参数与 Elasticsearch 聚合 API (Elasticsearch aggregations API) 非常相似,并且在许多情况下被映射到普通的 SQL 结构 (plain SQL constructs) (如 COUNT(*))。
让我们看一些示例。
最基本的调用根本不使用 pdb.agg(),它只使用一个 COUNT(*),该调用会被捕获并使用搜索聚合 (search aggregation) 进行规划。
SELECT count(*)
FROM mock_items
WHERE description ||| 'shoes';
count
-------
3
(1 row)
这也可以通过窗口函数 (window function) 来完成,它会添加一个额外的列,包含整个结果集的计数(而不仅仅是返回的行)。
SELECT id, description,
count(*) OVER (),
FROM mock_items
WHERE description ||| 'shoes'
ORDERBY rating DESC
LIMIT2;
id | description | count
----+---------------------+-------
3 | Sleek running shoes | 3
5 | Generic shoes | 3
(2 rows)
一个更复杂的示例使用了 JSON API 来表达对 TopN 查询 (TopN query) 的术语分面 (terms facet) ,然后也作为一个额外的 JSON 列添加(此数据在每行中重复,我们可能会在未来进行优化)。当您想要显示 TopN 搜索结果 (TopN search results) ,同时也展示整个搜索查询的分布 (distribution) 时,这非常有用。在一个高效的索引遍历 (index pass) 中完成此操作是 Elasticsearch API 的一个优点,我们很喜欢并将其在 SQL 中实现。
SELECT id, description,
pdb.agg('{"terms": {"field": "rating"}}') AS facets
FROM mock_items
WHERE description ||| 'shoes'
ORDERBY rating DESC
LIMIT2;
id | description | facets
----+---------------------+---------------------------------------------
3 | Sleek running shoes | {"buckets": [{"key": 4, "doc_count": 1},
{"key": 5, "doc_count": 1},
{"key": 3, "doc_count": 1} ]}
3 | Generic shoes | {"buckets": [{"key": 4, "doc_count": 1},
{"key": 5, "doc_count": 1},
{"key": 3, "doc_count": 1} ]}
(2 rows)
在幕后,这些查询使用我们的 BM25 索引 (BM25 indexes) 进行全文搜索 (full-text search) 和列式分析 (columnar analytics) 。您可以获得跨数百万文档的亚秒级分面计算 (facet calculations) ,同时保持与您的事务性数据 (transactional data) 的完美一致性 (consistency) 。
v2 API: Less Magic, More Clarity
如果这些聚合 (aggregation) 功能难以使用,那么它们的重要性就会大打折扣。因此,在进行性能工作的同时,我们也将 v2 API 提升为默认体验,其差异在于更直观的 SQL 用户体验 (SQL UX) 立刻显现。
v2 API 最显著的优势之一是它消除了大多数其他搜索系统中存在的模式重复 (schema duplication) 。使用 Elasticsearch,您必须定义两次数据结构:一次在您的应用程序数据库 (application database) 中,另一次在您的 Elasticsearch 映射 (Elasticsearch mapping) 中:
// Elasticsearch mapping - separate from your database schema
{
"mappings": {
"properties": {
"id": { "type": "integer" },
"description": { "type": "text", "analyzer": "english" },
"category": { "type": "keyword" }
}
}
}
即使是我们自己的 v1 API 也需要单独的 JSON 配置 (JSON configuration) 来配置索引 (indexes) 。v2 API 倾向于我们对“ 搜索数据库 (search database) ”的定义,并从您的 PostgreSQL 表模式 (PostgreSQL table schema) 中推断出大部分索引模式 (index schema) 。您直接在索引创建语句 (index creation statement) 中定义文本处理 (text processing) 和搜索行为 (search behavior) ,从而消除了维护并行模式定义 (parallel schema definitions) 的需要。
许多用户由于分词器 (tokenizers) 配置不正确,在他们的前几个查询中看到了意外的行为。在 v0.20.0 中,我们改进了分词器默认设置 (tokenizer defaults) ,并使其在创建索引 (indexes) 时成为第一方体验 (first-party experience) ,消除了任何歧义。现在索引创建 (Index creation) 更简洁,在行内指定了分词器 (tokenizers) 和选项:
CREATE INDEX search_idx ON mock_items
USING bm25 (id,
title,
(description::pdb.simple('stemmer=english',
'stopwords_language=english')),
category::pdb.literal)
WITH (key_field='id');
搜索也得到了改进,引入了新的合取 (conjunction) 运算符 &&& 和析取 (disjunction) 运算符 |||,以及访问常见功能如加权 (boosting) 的简单方法。
SELECT id, pdb.score(id), description, category
FROM mock_items
WHERE description ||| 'shoes'::pdb.boost(2) ORcategory ||| 'footwear'
ORDERBY score DESC
LIMIT5;
id | score | description | category
----+-----------+----------------------+----------
5 | 7.660918 | Generic shoes | Footwear
3 | 6.872712 | Sleek running shoes | Footwear
4 | 6.872712 | White jogging shoes | Footwear
13 | 1.9362776 | Sturdy hiking boots | Footwear
23 | 1.9362776 | Comfortable slippers | Footwear
(5 rows)
请关注另一篇展示所有新的 v2 API 功能的文章。现在,您可以在我们的文档 (documentation) 中探索完整的 API 参考 (API reference) 。
我们从 0.19.0 到 0.20.0 的性能 (performance) 故事揭示了我们对事务性系统 (transactional systems) 中搜索工作方式的看法的真相。传统的搜索引擎 (Traditional search engines) 将更新视为昂贵的操作,需要仔细的批处理 (batching) 和预定的维护窗口 (scheduled maintenance windows) 。乍一看,这似乎是合理的,因为搜索索引 (search indexes) 是复杂的数据结构,需要时间进行重组 (reorganize) 。
潜在的问题源于像 Lucene 和 Tantivy 这样的搜索引擎 (search engines) 处理数据的方式。这些系统使用不可变数据结构 (immutable data structures) 来处理它们的段 (segments) ,这意味着更新文档中的单个字段需要重写该段 (segment) 中的所有其他字段,从而产生写入放大 (write amplification) 。
但这对于实际应用程序来说恰恰是错误的。大多数搜索工作负载 (search workloads) 并不是批量导入 (bulk imports) ;它们是用户创建内容、修改记录和与您的系统交互时的单个更新流 (streams of individual updates) 。ParadeDB 将这些小型、频繁的更新视为一等公民 (first-class citizens) ,其性能应与插入 (inserts) 一样好。
我们的解决方案基于 0.19.0 中的两个关键优化 (optimizations) ,现在在我们的 LSM 引擎 (LSM engine) 中默认启用:可变段 (mutable segments) 和后台合并 (background merging) 。
可变段 (Mutable segments) 引入了一个专门为写入 (writes) 优化的小型缓冲区 (small buffer) 。我们不是立即将更改写入不可变段 (immutable segments) ,而是在此缓冲区 (buffer) 中存储指向新行的指针 (pointer) ,完全消除了写入放大 (write amplification) 问题。一旦缓冲区 (buffer) 变满,后台合并 (background merging) 会将其序列化 (serialize) 为不可变结构 (immutable structure) ,而不会阻塞正在进行的写入操作 (write operations) 。后台合并 (Background merging) 将所有合并 (merging) ( 包括可变段合并 (mutable segment merging) )移至后台 (background) ,并增加了我们专用于此任务的工作线程 (workers) 数量。
自从我们在 0.19.0 中首次引入这些功能以来,单行更新性能 (single row update performance) 提高了两个数量级以上;这种改进可以改变您对事务性搜索 (transactional search) 的看法。
具有 BM25 索引的宽表上的每秒更新次数 (Updates Per Second)

同样,您可以期待一篇包含所有数据结构 (data-structure) 详细信息的后续文章。
版本 0.20.0 通过直接在 PostgreSQL 中添加快速搜索聚合 (search aggregations) 、简化的 v2 API 和显着改进的写入性能 (write performance) ,巩固了 ParadeDB 作为 Elasticsearch 替代品 (alternative to Elasticsearch) 的地位。对于重视事务一致性 (transactional consistency) 和运营简单性 (operational simplicity) 的团队来说,我们提供了一种实现现代搜索功能 (modern search capabilities) 的途径,而无需管理独立系统 (separate systems) 的复杂性。
准备好看到搜索聚合 (search aggregations) 和新的 API 投入实际应用了吗?试试 ParadeDB 0.20.0,体验搜索 (search) 、分析 (analytics) 和事务 (transactions) 无缝协作的感觉。
最后, 希望PG的生态越来越发达, 让PG用户能在一个数据库中就完成所有业务需求, 即节省了数据库成本, 又减轻了运维负担和开发复杂度!