DuckDB实战:模糊搜索与文本处理

深入掌握 DuckDB 的模糊搜索和文本处理功能:LIKE 模式匹配、正则表达式、Levenshtein 编辑距离、全文检索扩展,以及在一个真实电商搜索场景中的完整应用。

引言

架构图

图: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_matchesregexp_extractregexp_replaceregexp_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 提供了丰富的文本处理工具链:

  1. LIKE — 快速规则匹配,性能最佳
  2. REGEXP — 灵活的文本提取、替换和清洗
  3. Levenshtein — 编辑距离模糊匹配,识别拼写变体
  4. FTS 扩展 — 生产级全文检索,支持相关性排序

对于中小规模文本搜索场景,DuckDB 的这些内置能力往往就足够了,无需额外引入 Elasticsearch。


更多 DuckDB 实战技巧,请关注 DuckDB Lab(duckdblab.org)

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计