1 个不稳定版本

0.1.0 2024年6月1日

#727 in 文本处理

MIT 许可证

70KB
1.5K SLoC

vidyut-kosha

一个紧凑的梵文词典

vidyut-kosha 定义了一个键值存储,可以将数千万个梵文单词及其屈折数据紧凑地映射。根据应用的不同,存储成本可以低至每个单词1字节。这种存储效率的代价是查找时间的增加,但在实际应用中,我们发现这种增加是可以忽略不计的,并且非常值得在其他方面的效率提升。

为了您的方便,vidyut-kosha 包含了辅助脚本,可以自动构建一个有趣且全面的词典。有关详细信息,请参阅下面的 使用说明 部分。

这个 crate 是 Ambuda 项目的一部分,正在积极开发中。如果您喜欢我们的工作并希望为其做出贡献,我们鼓励您 加入我们的 Discord 服务器,在那里您可以结识其他梵文程序员和爱好者。

概述

梵文是一种高度屈折的语言,任何合理的梵文单词列表至少包含数千万个单词。在实践中,梵文程序会对这个单词列表进行各种妥协,以避免性能下降。

一个常见的妥协是将不规则单词原样存储,并将规则单词仅存储为其词根形式。然后在查询时,我们通过手动维护的前缀和后缀表来猜测查询的潜在词根。

这种方法是可行的,但有两个主要的缺点

  • 速度。 以查询 ubhe 为例,可能会得到候选词 ubhaubhAubhiubhubhe,所有这些都必须与存储进行比对。

  • 过度生成。 通过使用前缀和后缀表,我们可能会接受像 *sundarA (对于 sundarI) 或 *putrakA (对于 putrikA) 这样的无效单词。这对于某些应用可能是有用的,但通常这是一个基于需要而非选择做出的决定。

vidyut-kosha 避免了这些弱点。它精确存储单词,避免了过度生成。尽管单键查找稍微慢一些,但它避免了多次查找的问题,这使得它在许多应用中更快。

然而,我们必须付出以下代价

  • 我们必须提前构建字典。如果我们想更改字典,我们必须重新构建它。构建需要几分钟才能完成。

  • 我们必须能够将我们的值转换为64位整数。这听起来很艰巨,但在实践中很简单。(如果您计划将 vidyut-kosha 用作屈折变化字典,我们的库已经为您无缝管理了这种转换。)

使用方法

详细说明即将到来。现在,尝试运行以下命令

$ make create_kosha

如果您想使用特定的词表,可以直接使用构建器API,如下所示

use vidyut_kosha::{Kosha, Builder};
use vidyut_kosha::morph::*;

let mut builder = Builder::new("output-dir").unwrap();
builder.insert("Bavati", &Pada::Tinanta(Tinanta {
    dhatu: Dhatu("BU".to_string()),
    purusha: Purusha::Prathama,
    vacana: Vacana::Eka,
    lakara: Lakara::Lat,
    pada: PadaPrayoga::Parasmaipada,
}));
builder.finish().unwrap();

let kosha = Kosha::new("output-dir");

设计

尽管 vidyut-kosha 可以支持任何键的编码类型,但我们强烈建议使用 SLP1WX,以实现更好的空间效率和查询性能。

我们底层的数据结构是在 fst crate 中实现的 有限状态转换器 (FST)。关于FST如何工作的详细信息,我们建议阅读 fst crate 创建者的这篇博客文章,特别是标题为 "有限状态机作为数据结构" 的部分。

简而言之,FST通过存储共享的前缀和后缀来泛化 Trie 数据结构。FST的构建算法将自动找到这些前缀和后缀,并尽可能重用保存的前缀和后缀。

FST有几个重要的约束,其中两个在这里最相关

  1. FST中的值必须满足特定的代数。Rust的 fst crate 通过要求值是64位整数来满足这个代数。所以简单来说,我们必须能够将我们需要存储的任何信息转换为64位整数。

  2. FST中的键必须全部唯一。但是梵文单词不一定唯一,我们必须能够存储重复的键。

解决编码约束

对于屈折变化字典的使用场景,我们想存储两种类型的信息:枚举数据文本数据

枚举数据 包括类别如人称、数、性别、格等。每个类别可以取我们事先知道的几个可能的值之一。我们可以简单地将此类数据转换为整数值。例如,我们可以用四种可能的值之一来表示单词的 vacana(数):ekadvibahunone,如果由于某种原因 vacana 是未知的。因此我们可以将 eka 映射到 1,dvi 映射到 2,依此类推。

文本数据 包括基础形式的基本词干或词根,我们无法事先合理地列举它。我们通过查找表方法来编码此数据:如果我们将所有字符串附加到一个列表中,则该字符串在列表中的索引就是其整数值。通过这种方法,我们以老式的方式存储这些字符串的代价。但是,词元的列表比单词的列表小得多,因此空间需求是微不足道的。

一旦我们将所有信息映射到整数值,我们就可以将64位整数视为位字段,并使用适当的位移来相加值。例如,以下是我们的tinantas方案的早期版本,该方案仅使用32位。

OOLLLLppVVaadddddddddddddddddddd

O: part of speech (2 bits = 4 possible values)
L: lakara (4 bits = 16 possible values)
p: purusha (2 bits = 4 possible values)
V: vacana (2 bits = 4 possible values)
a: pada + prayoga (2 bits = 4 possible values)
d: dhatu ID (20 bits = ~1 million possible values)

编码约束的一个实际后果是我们不能将单词与完全任意的数据相关联。但如果我们精心组织数据,我们就有足够的空间将每个单词与有趣的信息相关联。

解决唯一性约束

只有当梵文词典支持重复键时才是实用的。为了解决唯一性约束,我们使用以下解决方案,这在实践中(出人意料地)效果很好。

fst crate将键视为无符号8位整数的向量,这意味着键中的每个“字母”可以取0到255之间的值。由于ASCII范围从65开始,我们可以在0到64的范围内编码额外的信息,这让我们可以标记重复项。

例如,我们可能会这样存储gacchati的各种形式:

  • gacchati,
  • gacchati+ 0
  • gacchati+ 1

以此类推,直到必要时为gacchati + 64

由于在实践中65个重复项还不够,我们简单地扩展了附加到键的字节范围

  • gacchati,
  • gacchati+ 0 0
  • gacchati+ 0 1,
  • gacchati+ 0 2

以此类推,直到必要时为gacchati + 64 64

在查找时,我们首先查找查询词——比如说gacchati——然后依次检查gacchati + 0 0等等,直到我们不再找到任何结果。这种查找可能看起来非常昂贵,但由于FST的结构,它实际上非常便宜。

依赖项

~6–15MB
~153K SLoC