⚠️ 此页面为自动翻译,翻译可能不完美。
blog-post

如何使用 dict='keywords_32k' 搜索长哈希和 ID

View as markdown

全文搜索通常适用于普通词语:产品名、标题、评论和描述。这类 token 很少长到几十个字符以上。

日志和技术数据则不同。SHA-256 哈希长 64 个字符,而消息 ID、关联键、事件标识符以及某些电子邮件地址可能更长。它们的值通常只有作为整体才有意义:如果尾部丢失,一个 ID 很容易与另一个 ID 混淆。

Manticore Search 为这些场景提供了 dict='keywords_32k'

keywords_32k 从 Manticore Search 27.1.1 起可用。将现有表从 keywords 转换过来时,建议使用 27.1.5 或更新版本。

常规字典的问题

默认情况下,Manticore 使用 dict='keywords'。它的 token 最大长度是归一化后的 42 字节

这个限制按字节计算,不是按字符计算。一个 ASCII 字符占 1 字节,而一个 UTF-8 字符可能占多个字节。

如果某个 token 超过 42 字节,Manticore 会将其截断:

  • 在索引文档时;
  • 在处理搜索查询时。

结果是,查询完整的长值并不一定会返回 0 条结果。因为查询本身也会被截断,文档可能仍然能被找到,但只会按前 42 字节来匹配。

这会带来更严重的问题:两个不同的 ID 如果前 42 字节相同,就会在全文搜索中变得无法区分。并且,也无法通过位于第 42 字节之后的片段来查找某个值。

keywords_32k 改变了什么

dict='keywords_32k' 将归一化后的 token 最大长度提高到32768 字节,也就是 32 KB。

行为dict='keywords'dict='keywords_32k'
Token 最大长度42 字节32768 字节
超出限制的 token被截断跳过并给出警告
前缀和中缀搜索支持支持
超过 42 字节 token 的词形处理Token 已经被截断不适用
RT 表支持支持
普通表支持支持

该设置作用于整张表:

CREATE TABLE events (
  message text,
  event_id text
)
dict='keywords_32k';

同一张表中的普通短词仍然使用已配置的词形处理。超过 42 字节的 token 会以其原始归一化形式存储,不进行 stemming 或 lemmatization。

对于机器标识符来说,这通常正是你需要的:哈希或事件 ID 并没有有用的词干。

何时使用 keywords_32k

当以下两个条件都成立时使用它:

  1. 该值在分词后可能超过 42 字节。
  2. 它必须能够通过 MATCH()、前缀或子串进行搜索。

典型示例包括:

  • SHA-256 和其他长哈希;
  • 事件 ID 和消息 ID;
  • 请求 ID、trace ID 以及其他技术标识符;
  • 长记录键;
  • 本地部分或域部分很长的电子邮件地址;
  • 来自日志的技术值;
  • 包含分隔符的长标识符。

不过,并不是每一次 ID 查找都需要 keywords_32k

如果你只需要精确相等

如果应用总是接收完整 ID,并且只需要检查是否完全相等,那么字符串属性就足够了:

CREATE TABLE events (
  message text,
  event_id string
);

然后使用普通过滤器:

SELECT id, message
FROM events
WHERE event_id =
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08';

如果你既需要精确匹配又需要全文搜索

使用 string attribute indexed

CREATE TABLE events (
  message text,
  event_id string attribute indexed
)
dict='keywords_32k';

在这种情况下,Manticore:

  • 将原始值作为字符串属性存储;
  • 允许你通过 WHERE 进行过滤;
  • 也会为 MATCH() 和通配符搜索建立索引。

对于技术标识符来说,这通常是最方便的 schema。

按完整 token 搜索

创建一张表并加入一个 64 字符的 SHA-256 哈希:

DROP TABLE IF EXISTS events;

CREATE TABLE events (
  message text,
  event_id string attribute indexed
)
dict='keywords_32k';

INSERT INTO events (id, message, event_id) VALUES
(
  1,
  'delivery accepted',
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);

在全文搜索中使用完整的归一化 token:

SELECT id, message
FROM events
WHERE MATCH(
  '@event_id 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);

@event_id 运算符会将搜索限制在指定字段中。如果不加它,Manticore 会在表的所有全文字段中搜索该值。

这里单个简单的字母数字 token 不需要加引号。引号表示短语搜索;它们不会把 MATCH() 变成对原始字符串逐字节的比较。

全文匹配不等于严格相等

MATCH() 作用于分词和归一化后的结果。它可能受到以下因素影响:

  • charset_table
  • 转为小写;
  • blend_chars
  • ignore_chars
  • 词形变化和其他文本处理设置。

如果要严格比较存储的值,请使用字符串属性:

SET collation_connection='binary';

SELECT id, message
FROM events
WHERE event_id =
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08';

binary 会在当前 SQL 会话中启用逐字节字符串比较。这个设置不会影响全文搜索行为。

实用规则很简单:

  • WHERE event_id = ... — 对存储字符串进行严格比较;
  • MATCH('@event_id ...') — 对归一化 token 进行搜索;
  • MATCH('@event_id prefix*') — 前缀搜索;
  • MATCH('@event_id *fragment*') — 子串搜索。

如何检查分词结果

在加载大量数据之前,先确认 Manticore 实际上是否把这个值视为单个 token:

CALL KEYWORDS(
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
  'events'
);

normalized 列应该包含完整的 64 字符哈希。

CALL KEYWORDS 对包含以下内容的值尤其有用:

  • 句点;
  • 连字符;
  • @ 符号;
  • 冒号;
  • 斜杠;
  • 来自不同书写系统的字符。

这样你就可以在索引数据之前检查实际的 token 边界。

前缀和子串搜索

要在 token 内部搜索,请启用 min_infix_len

DROP TABLE IF EXISTS events_infix;

CREATE TABLE events_infix (
  message text,
  event_id string attribute indexed
)
dict='keywords_32k'
min_infix_len='4';

INSERT INTO events_infix (id, message, event_id) VALUES
(
  1,
  'delivery accepted',
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);

前缀搜索:

SELECT id, message
FROM events_infix
WHERE MATCH('@event_id 9f86d081*');

子串搜索:

SELECT id, message
FROM events_infix
WHERE MATCH('@event_id *b2b0b822*');

正的 min_infix_len 也会启用前缀搜索。这个示例使用 4,以避免模式过短、覆盖过宽。

为什么短模式很危险

与常规 dict='keywords' 一样,dict='keywords_32k' 不会预先计算所有可能的子串。相反,它会在查询时将通配符模式展开为匹配的字典词条。

例如,模式 *ab* 可能匹配大量值。它产生的展开越多,且每个匹配词条所包含的文档越多,查询成本就越高。

对于生产系统:

  • 不要允许用户搜索过短的片段;
  • 根据真实数据选择 min_infix_len
  • 使用 expansion_limit 限制展开数量;
  • 用接近生产规模的字典测试性能;
  • @field 将搜索限制在特定字段中。

index_exact_words='1' 本身并不是通配符搜索所必需的。如果你想在排序时区分精确匹配和通配符匹配,通常需要它,并且一般要与 expand_keywords 一起使用。

带分隔符的电子邮件地址和其他值

keywords_32k 只改变 token 的最大长度,不决定 token 从哪里开始、在哪里结束。

默认情况下,句点、@、连字符和其他字符可能会把一个值拆成多个部分。如果电子邮件地址或消息 ID 也必须作为整体建立索引,可以使用 blend_chars

DROP TABLE IF EXISTS mail_events;

CREATE TABLE mail_events (
  sender string attribute indexed,
  subject text
)
dict='keywords_32k'
blend_chars='., @, -'
min_infix_len='4';

INSERT INTO mail_events (id, sender, subject) VALUES
(
  1,
  'alessandro.verylonggeneratedlocalpart@example-corporate-domain.test',
  'delivery accepted'
);

被混合的字符会以两种方式建立索引:

  • 作为完整 token 的一部分;
  • 作为该值中常规部分之间的分隔符。

这样既可以搜索完整的电子邮件地址,也可以搜索其中的单个词。

搜索完整值:

SELECT id, subject
FROM mail_events
WHERE MATCH(
  '@sender "alessandro.verylonggeneratedlocalpart@example-corporate-domain.test"'
);

这里引号很重要,因为 @ 也用于全文查询语法。在短语中,解析器可以将它按混合字符处理。

按片段搜索:

SELECT id, subject
FROM mail_events
WHERE MATCH('@sender *generatedlocalpart*');

检查分词结果:

CALL KEYWORDS(
  '"alessandro.verylonggeneratedlocalpart@example-corporate-domain.test"',
  'mail_events'
);

在真实应用中,传递给 MATCH() 的值必须正确转义。直接把用户输入拼接到查询字符串里,可能会因为 @-|!"* 和其他运算符而改变查询含义。

如何转换现有表

对于 RT 表,可以使用 ALTER TABLE 更改设置:

ALTER TABLE events dict='keywords_32k';

不过,这只会影响在设置变更之后新增或替换的文档。

现有文档不会自动重新分词。它们的长 token 会继续以旧的截断形式存在,直到文档被重新索引。

按以下步骤操作:

  1. 更新 dict
  2. 通过 SHOW CREATE TABLE 验证设置。
  3. 重新索引或重新加载现有文档。
  4. CALL KEYWORDSMATCH() 检查几个长值。

对于普通表:

  1. 将配置改为 dict = keywords_32k
  2. 如果这符合你的更新流程,则使用 ALTER TABLE ... RECONFIGURE 应用设置。
  3. 基于其数据源完整重建该表。

在数据重新索引之前,同一张表中可能同时包含:

  • 旧文档,其中的 token 已被截断;
  • 新文档,其中的 token 是完整的。

这可能会让看起来相同的文档产生不同的结果。

为什么 dict='crc' 不能解决这个问题

dict='crc' 存储的是关键词校验和,而不是原始文本。不过,它并不会增加允许的 token 长度。

常规 42 字节限制的例外,是由 dict='keywords_32k' 专门实现的。

keywordskeywords_32k 字典还会存储词项文本,这使 Manticore 能够针对字典展开前缀和中缀通配符查询。

如果你需要搜索很长的机器标识符,crc 不能替代 keywords_32k

当前局限

在发布时,dict='keywords_32k' 还有若干局限:

  • 不支持 CALL SUGGESTCALL QSUGGEST
  • 不能用于 percolate 表;
  • 超过 42 字节的 token 不会在 snippets 或 highlights 中高亮;
  • indextool --dumpdict 不能导出这种字典;
  • 全文 REGEX 运算符可与 dict='keywords' 一起工作,但不能与 keywords_32k 一起工作。

最后这一点不要与用于过滤字符串属性的 REGEX() 函数混淆。如果字段声明为 string attribute indexed,属性过滤和全文搜索仍然是两种独立机制。

不要索引密钥

能够搜索一个长值,并不意味着你应该把它存进搜索索引。

除非绝对必要,否则不要索引以下内容:

  • API 密钥;
  • bearer token;
  • 会话 cookie;
  • 私钥;
  • 密码和重置令牌;
  • 其他可用于访问系统的数据。

keywords_32k 解决的是搜索问题,但不能阻止值被用户、备份、查询日志或系统管理员读取。

如果某个密钥必须按其精确值匹配,提前计算合适的哈希并只存储哈希会更安全。

快速检查清单

启用 keywords_32k 之前,请检查:

  1. 归一化 token 是否真的超过了 42 字节?
  2. 你是否需要全文或通配符搜索,而不只是 WHERE value = ...
  3. CALL KEYWORDS 是否把完整值视为一个 token?
  4. 对于句点、连字符、@ 和其他分隔符,你是否需要 blend_chars
  5. min_infix_len 是否足够大?
  6. 通配符展开的数量是否受限?
  7. 旧文档是否已经重新索引?
  8. 该字段是否不包含密钥数据?
  9. 应用是否没有依赖高亮、SUGGEST、percolate 或全文 REGEX

小结

dict='keywords_32k' 解决的是一个特定问题:它允许全文索引存储最长 32768 字节的归一化 token,而不是常规的 42 字节。

它非常适合长哈希、事件 ID、消息 ID、电子邮件地址以及其他机器标识符。请记住三点:

  • keywords_32k 会增加 token 长度,但不会改变分词规则;
  • 对完整 token 使用 MATCH() 不等于对原始字符串做严格比较;
  • 设置变更后,现有文档必须重新索引。

如果你只需要精确相等,请使用字符串属性。如果你既需要精确匹配又需要部分值搜索,请将 string attribute indexeddict='keywords_32k' 一起使用。

文档

安装Manticore Search

在 Linux 或 macOS 上用一条命令安装 Manticore Search:

curl https://manticoresearch.com | sh

有关高级安装选项,请参阅完整安装指南手册

安装Manticore Search