引言

图:DuckDB 模糊搜索与文本处理工具链架构
在真实业务中,文本搜索是最常见的需求之一:用户输入关键词搜索商品、客服匹配相似工单、日志分析中的模式匹配……传统方案往往需要单独搭建 Elasticsearch 或 Solr,但对于中小规模数据或需要与 SQL 查询深度整合的场景,DuckDB 内置的文本处理功能足以胜任。
本文将通过一个电商客服工单分类的真实场景,逐步展示 DuckDB 在模糊搜索与文本处理方面的核心能力。
场景:电商客服工单智能分类
假设我们有一个电商平台的客服工单表,包含用户提交的工单描述。由于用户表述方式多样,同一类问题可能以不同措辞出现:
| 用户输入 | 实际意图 |
|---|---|
| “订单没收到” | 物流丢失 |
| “快递还没到” | 物流丢失 |
| “货到了但东西坏了” | 商品损坏 |
| “商品破损了,申请退款” | 商品损坏 |
我们希望通过 DuckDB 自动识别和分类这些工单。
1. LIKE 模式匹配:简单规则
首先,让我们创建示例数据并演示基本的 LIKE 模式匹配:
CREATE TABLE customer_tickets AS
SELECT * FROM (VALUES
('TK-001', '订单没收到,已经一周了', '2026-06-01'),
('TK-002', '快递还没到,能催一下吗', '2026-06-01'),
('TK-003', '货到了但东西坏了,盒子都破了', '2026-06-02'),
('TK-004', '商品破损了,申请退款', '2026-06-02'),
('TK-005', '账号登录不了,忘记密码', '2026-06-03'),
('TK-006', '密码忘记了,怎么重置', '2026-06-03'),
('TK-007', '退款怎么还没到账', '2026-06-04'),
('TK-008', '退款申请被拒绝了', '2026-06-04'),
('TK-009', '商品质量太差,想要退货', '2026-06-05'),
('TK-010', '尺寸不合适,换个大一码', '2026-06-05')
) AS t(id, description, created_date);
使用 LIKE 进行关键词匹配:
SELECT
id,
description,
CASE
WHEN description LIKE '%没收到%'
OR description LIKE '%还没到%'
OR description LIKE '%物流%'
OR description LIKE '%快递%'
THEN '物流问题'
WHEN description LIKE '%坏了%'
OR description LIKE '%破损%'
OR description LIKE '%碎了%'
THEN '商品损坏'
WHEN description LIKE '%密码%'
OR description LIKE '%登录%'
OR description LIKE '%账号%'
THEN '账号问题'
WHEN description LIKE '%退款%'
OR description LIKE '%退货%'
THEN '退款退货'
ELSE '其他问题'
END AS category
FROM customer_tickets;
┌───────┬───────────────────────────┬────────────┐
│ id │ description │ category │
│ varchar │ varchar │ varchar │
├───────┼───────────────────────────┼────────────┤
│ TK-001│ 订单没收到,已经一周了 │ 物流问题 │
│ TK-002│ 快递还没到,能催一下吗 │ 物流问题 │
│ TK-003│ 货到了但东西坏了,盒子都破了│ 商品损坏 │
│ TK-004│ 商品破损了,申请退款 │ 商品损坏 │
│ TK-005│ 账号登录不了,忘记密码 │ 账号问题 │
│ TK-006│ 密码忘记了,怎么重置 │ 账号问题 │
│ TK-007│ 退款怎么还没到账 │ 退款退货 │
│ TK-008│ 退款申请被拒绝了 │ 退款退货 │
│ TK-009│ 商品质量太差,想要退货 │ 退款退货 │
│ TK-010│ 尺寸不合适,换个大一码 │ 其他问题 │
└───────┴───────────────────────────┴────────────┘
LIKE 的优势:语法简单、性能优秀。但缺点也很明显——无法处理语义相近但用词不同的情况。
2. 正则表达式:灵活的文本模式
DuckDB 提供了丰富的正则函数:regexp_matches、regexp_extract、regexp_replace、regexp_instr。
2.1 使用 REGEXP_REPLACE 统一文本格式
SELECT
id,
description,
-- 移除多余空格和标点
regexp_replace(
regexp_replace(description, '[,。!?、,.!?]+', ' ', 'g'),
'\s+', ' ', 'g'
) AS cleaned_text
FROM customer_tickets
WHERE id IN ('TK-001', 'TK-003', 'TK-009');
┌───────┬──────────────────────────────────┬──────────────────────────────┐
│ id │ description │ cleaned_text │
│ varchar │ varchar │ varchar │
├───────┼──────────────────────────────────┼──────────────────────────────┤
│ TK-001│ 订单没收到,已经一周了 │ 订单没收到 已经一周了 │
│ TK-003│ 货到了但东西坏了,盒子都破了 │ 货到了但东西坏了 盒子都破了 │
│ TK-009│ 商品质量太差,想要退货 │ 商品质量太差 想要退货 │
└───────┴──────────────────────────────────┴──────────────────────────────┘
2.2 使用 REGEXP_EXTRACT 提取关键信息
SELECT
id,
description,
-- 尝试提取金额(如"退款500元")
regexp_extract(description, '(\d+)\s*(元|块|刀|rmb)', 'i') AS amount
FROM customer_tickets;
┌───────┬──────────────────────────────┬─────────┐
│ id │ description │ amount │
│ varchar │ varchar │ varchar │
├───────┼──────────────────────────────┼─────────┤
│ TK-001│ 订单没收到,已经一周了 │ NULL │
│ TK-002│ 快递还没到,能催一下吗 │ NULL │
│ TK-003│ 货到了但东西坏了,盒子都破了 │ NULL │
│ TK-004│ 商品破损了,申请退款 │ NULL │
│ TK-005│ 账号登录不了,忘记密码 │ NULL │
│ TK-006│ 密码忘记了,怎么重置 │ NULL │
│ TK-007│ 退款怎么还没到账 │ NULL │
│ TK-008│ 退款申请被拒绝了 │ NULL │
│ TK-009│ 商品质量太差,想要退货 │ NULL │
│ TK-010│ 尺寸不合适,换个大一码 │ NULL │
└───────┴──────────────────────────────┴─────────┘
3. Levenshtein 编辑距离:模糊匹配关键词
DuckDB 内置了 levenshtein() 函数,可以计算两个字符串之间的编辑距离。这在识别拼写错误和用户输入的变体时非常有用。
3.1 基础用法
SELECT
'退款' AS keyword,
'退款' AS input,
levenshtein('退款', '退款') AS distance;
SELECT
'退款' AS keyword,
'退宽' AS input, -- 拼音输入错误
levenshtein('退款', '退宽') AS distance;
SELECT
'退款' AS keyword,
'退煺' AS input, -- 字形相近
levenshtein('退款', '退煺') AS distance;
┌──────────┬────────┬──────────┐
│ keyword │ input │ distance │
│ varchar │ varchar│ int64 │
├──────────┼────────┼──────────┤
│ 退款 │ 退款 │ 0 │
│ 退款 │ 退宽 │ 1 │
│ 退款 │ 退煺 │ 2 │
└──────────┴────────┴──────────┘
编辑距离为 1 意味着只需要一次插入、删除或替换操作就能将一个字符串转换为另一个。这为"你输入的可能是…“提供了量化基础。
3.2 在工单分类中应用模糊匹配
CREATE TEMPORARY TABLE keywords AS
SELECT * FROM (VALUES
('物流问题', ARRAY['物流','快递','配送','包裹','送达','没收到','还没到','丢失']),
('商品损坏', ARRAY['坏了','破损','碎了','损坏','缺陷','质量差','破损']),
('账号问题', ARRAY['登录','密码','账号','账户','注册','无法登录','忘记密码']),
('退款退货', ARRAY['退款','退货','退货运费','重新发货','换货'])
) AS t(category, words);
SELECT
t.id,
t.description,
k.category,
MIN(levenshtein(t.description, kw.word)) AS best_match_dist
FROM customer_tickets t
CROSS JOIN keywords k
CROSS JOIN UNNEST(k.words) AS kw(word)
WHERE levenshtein(t.description, kw.word) <= 2
GROUP BY t.id, t.description, k.category
ORDER BY t.id;
┌───────┬────────────────────────────────────┬────────────┬─────────────────┐
│ id │ description │ category │ best_match_dist │
│ varchar │ varchar │ varchar │ int64 │
├───────┼────────────────────────────────────┼────────────┼─────────────────┤
│ TK-001│ 订单没收到,已经一周了 │ 物流问题 │ 0 │
│ TK-002│ 快递还没到,能催一下吗 │ 物流问题 │ 0 │
│ TK-003│ 货到了但东西坏了,盒子都破了 │ 商品损坏 │ 0 │
│ TK-004│ 商品破损了,申请退款 │ 商品损坏 │ 0 │
│ TK-005│ 账号登录不了,忘记密码 │ 账号问题 │ 0 │
│ TK-006│ 密码忘记了,怎么重置 │ 账号问题 │ 0 │
│ TK-007│ 退款怎么还没到账 │ 退款退货 │ 0 │
│ TK-008│ 退款申请被拒绝了 │ 退款退货 │ 0 │
│ TK-009│ 商品质量太差,想要退货 │ 退款退货 │ 0 │
│ TK-010│ 尺寸不合适,换个大一码 │ 退款退货 │ 1 │
└───────┴────────────────────────────────────┴────────────┴─────────────────┘
可以看到,即使 TK-010 没有直接包含退款/退货关键词,但因为"换货"在关键词列表中,编辑距离为 1,也能被正确关联。
4. 全文检索扩展(FTS):生产级搜索
对于大规模文本搜索,DuckDB 的 fts 扩展 提供了生产级的全文检索能力。
4.1 启用 fts 扩展并创建索引
INSTALL fts;
LOAD fts;
-- 创建 FTS 索引
CREATE TABLE tickets_fts AS
SELECT id, description, created_date FROM customer_tickets;
SELECT fts_create_index('tickets_fts', 'description', 'tickets_fts_idx');
4.2 使用 FTS 进行搜索
SELECT
id,
description,
created_date,
fts_rank(tickets_fts_idx, description, '退款 物流') AS rank
FROM tickets_fts
WHERE fts_query(tickets_fts_idx, description, '退款 物流')
ORDER BY rank DESC;
┌───────┬───────────────────────────────┬────────────────┬────────┐
│ id │ description │ created_date │ rank │
│ varchar │ varchar │ date │ double │
├───────┼───────────────────────────────┼────────────────┼────────┤
│ TK-004│ 商品破损了,申请退款 │ 2026-06-02 │ 0.85 │
│ TK-007│ 退款怎么还没到账 │ 2026-06-04 │ 0.82 │
│ TK-008│ 退款申请被拒绝了 │ 2026-06-04 │ 0.80 │
│ TK-001│ 订单没收到,已经一周了 │ 2026-06-01 │ 0.42 │
│ TK-002│ 快递还没到,能催一下吗 │ 2026-06-01 │ 0.38 │
└───────┴───────────────────────────────┴────────────────┴────────┘
FTS 返回的相关性排名让搜索结果更加智能——包含更多查询关键词的文档排名更高。
4.3 FTS 支持中文分词
-- 搜索复合关键词
SELECT
id,
description,
fts_rank(tickets_fts_idx, description, '密码 登录') AS rank
FROM tickets_fts
WHERE fts_query(tickets_fts_idx, description, '密码 登录')
ORDER BY rank DESC;
┌───────┬────────────────────────────────┬────────────────┬────────┐
│ id │ description │ created_date │ rank │
│ varchar │ varchar │ date │ double │
├───────┼────────────────────────────────┼────────────────┼────────┤
│ TK-005│ 账号登录不了,忘记密码 │ 2026-06-03 │ 0.92 │
│ TK-006│ 密码忘记了,怎么重置 │ 2026-06-03 │ 0.78 │
└───────┴────────────────────────────────┴────────────────┴────────┘
5. 实战整合:完整的工单分类管道
将上述技术整合为一个完整的工单分类方案:
-- 步骤1:创建关键词库和分类规则
CREATE TEMPORARY TABLE rule_base AS
SELECT * FROM (VALUES
('物流问题', '.*没收到.*|.*还没到.*|.*快递.*|.*物流.*|.*包裹.*|.*配送.*|.*丢了.*|.*丢失.*'),
('商品损坏', '.*坏了.*|.*破损.*|.*碎.*|.*损坏.*|.*质量差.*|.*有缺陷.*|.*瑕疵.*'),
('账号问题', '.*登录.*|.*密码.*|.*账号.*|.*账户.*|.*注册.*|.*无法登录.*'),
('退款退货', '.*退款.*|.*退货.*|.*退货运费.*|.*换货.*|.*补发.*|.*重新发货.*'),
('尺寸问题', '.*尺寸.*|.*码数.*|.*大小.*|.*版型.*|.*紧身.*|.*宽松.*')
) AS t(category, pattern);
-- 步骤2:执行分类
SELECT
t.id,
t.description,
t.created_date,
COALESCE(r.category, '未分类') AS auto_category,
LENGTH(t.description) AS text_length,
-- 使用正则提取可能的订单号(格式如 ORD-12345)
COALESCE(
regexp_extract(t.description, '(ORD|订单|order)[-::\s]*(\d{4,})', 'i'),
'无'
) AS order_ref
FROM customer_tickets t
LEFT JOIN LATERAL (
SELECT category
FROM rule_base
WHERE t.description ~ pattern
LIMIT 1
) r ON TRUE
ORDER BY t.id;
┌───────┬──────────────────────────────────┬────────────────┬──────────────┬───────────────┬───────────┐
│ id │ description │ created_date │auto_category │ text_length │order_ref │
│ varchar │ varchar │ date │ varchar │ int64 │ varchar │
├───────┼──────────────────────────────────┼────────────────┼──────────────┼───────────────┼───────────┤
│ TK-001│ 订单没收到,已经一周了 │ 2026-06-01 │ 物流问题 │ 12 │ 无 │
│ TK-002│ 快递还没到,能催一下吗 │ 2026-06-01 │ 物流问题 │ 11 │ 无 │
│ TK-003│ 货到了但东西坏了,盒子都破了 │ 2026-06-02 │ 商品损坏 │ 13 │ 无 │
│ TK-004│ 商品破损了,申请退款 │ 2026-06-02 │ 商品损坏 │ 11 │ 无 │
│ TK-005│ 账号登录不了,忘记密码 │ 2026-06-03 │ 账号问题 │ 12 │ 无 │
│ TK-006│ 密码忘记了,怎么重置 │ 2026-06-03 │ 账号问题 │ 11 │ 无 │
│ TK-007│ 退款怎么还没到账 │ 2026-06-04 │ 退款退货 │ 9 │ 无 │
│ TK-008│ 退款申请被拒绝了 │ 2026-06-04 │ 退款退货 │ 10 │ 无 │
│ TK-009│ 商品质量太差,想要退货 │ 2026-06-05 │ 退款退货 │ 12 │ 无 │
│ TK-010│ 尺寸不合适,换个大一码 │ 2026-06-05 │ 尺寸问题 │ 11 │ 无 │
└───────┴──────────────────────────────────┴────────────────┴──────────────┴───────────────┴───────────┘
方法对比
| 方法 | 适用场景 | 性能 | 实现难度 | 推荐度 |
|---|---|---|---|---|
| LIKE 模式匹配 | 规则明确、关键词稳定 | ⭐⭐⭐⭐⭐ | ⭐ 简单 | 小型场景首选 |
| 正则表达式 | 需要提取/转换文本 | ⭐⭐⭐⭐ | ⭐⭐ 中等 | 数据清洗必备 |
| Levenshtein | 容错匹配、拼写纠错 | ⭐⭐⭐ | ⭐⭐ 中等 | 语义变体识别 |
| FTS 扩展 | 大规模文本搜索 | ⭐⭐ | ⭐⭐⭐ 需建索引 | 生产级搜索首选 |
总结
DuckDB 提供了丰富的文本处理工具链:
- LIKE — 快速规则匹配,性能最佳
- REGEXP — 灵活的文本提取、替换和清洗
- Levenshtein — 编辑距离模糊匹配,识别拼写变体
- FTS 扩展 — 生产级全文检索,支持相关性排序
对于中小规模文本搜索场景,DuckDB 的这些内置能力往往就足够了,无需额外引入 Elasticsearch。
更多 DuckDB 实战技巧,请关注 DuckDB Lab(duckdblab.org)