#annotations #nlp #linguistics #query-language #data-model

stam

STAM是一个处理文本 standoff 注释的强大库。这是一个Rust库。

19个版本 (重大更改)

0.14.2 2024年7月15日
0.13.0 2024年5月14日
0.12.0 2024年3月28日
0.8.0 2023年10月19日
0.4.0 2023年3月27日

#82 in 文本处理

Download history 148/week @ 2024-05-10 32/week @ 2024-05-17 335/week @ 2024-05-24 34/week @ 2024-05-31 6/week @ 2024-06-07 3/week @ 2024-06-14 1/week @ 2024-06-28 22/week @ 2024-07-05 134/week @ 2024-07-12 14/week @ 2024-07-19 61/week @ 2024-07-26 5/week @ 2024-08-02

86 每月下载量
用于 2 crates

GPL-3.0-only

1MB
22K SLoC

stam logo

Crate Docs GitHub build GitHub release Project Status: Active – The project has reached a stable, usable state and is being actively developed. Technology Readiness Level 7/9 - Release Candidate - Technology ready enough and in initial use by end-users in intended scholarly environments. Further validation in progress.

STAM库

STAM是一个独立的 standoff 文本注释数据模型。这是一个使用 Rust 工作的软件库,是 STAM 的主要库/参考实现。它旨在根据 STAM 规范 和大多数扩展来实现完整模型。

您可以使用这个库做什么?

  • 保留、构建和操作文本及其注释的高效内存存储
  • 通过编程或通过 STAM 查询语言 搜索注释、数据和文本。
    • 通过数据、文本内容、文本片段之间的关系(重叠、嵌入、相邻等)搜索注释
    • 搜索文本(包括通过正则表达式),并找到针对找到的文本选择的注释。
    • 在数据(集合、键、值)中搜索并找到使用数据的注释。
    • 有关文本偏移的基本文本操作(在分隔符处分割文本、删除文本)。
    • 在不同类型的偏移量之间进行转换(绝对、相对于其他结构、UTF-8 字节与 unicode 代码点等)
  • 从/到 STAM JSON、STAM CSV 或优化的二进制(CBOR)表示形式读取和写入资源及注释
    • 底层 STAM 模型 力求清晰简单。它是灵活的,并不承诺任何除了 standoff 注释之外的其他词汇表或注释范式。

此 STAM 库旨在作为构建处理文本 standoff 注释的进一步应用程序的基础。我们实现了所有低级逻辑,这样您就不再需要这样做,可以专注于您实际的应用程序。库的编写以性能为目标。

安装

stam 添加到项目的 Cargo.toml

$cargo add stam

使用

导入库

use stam;

或者如果您更喜欢没有命名空间

use stam::*;

加载包含注释存储的 STAM JSON 文件

fn your_function() -> Result<(),stam::StamError> {
    let store = stam::AnnotationStore::from_file("example.stam.json", stam::Config::default())?;
    ...
}

我们假设本节中所有示例都返回一种函数,该函数返回 Result<_,stam::StamError>

注释存储是您的 workspace,它包含所有资源、注释集(即键和注释数据),当然还有实际的注释。它是一个基于内存的存储,您可以将尽可能多的内容存储其中(只要它适合内存:)。

在实例化注释存储时,您可以传递一个配置(stam::Config()),该配置指定了各种参数,例如要生成哪些索引。使用各种 with_() 方法(一种构建模式)来设置各种配置选项。

检索项目

您可以通过与期望返回类型名称相似的名称的方法来检索项目

let annotation =  store.annotation("my-annotation").or_fail()?;
let resource = store.resource("my-resource").or_fail()?;
let annotationset: &stam::AnnotationDataSet = store.annotationset("my-annotationset").or_fail()?;
let key = annotationset.key("my-key").or_fail()?;
let data = annotationset.annotationdata("my-data").or_fail()?;

所有这些方法都返回一个 Option<ResultItem<T>>,其中 T 是 STAM 模型中的一个类型,如 AnnotationTextResourceAnnotationDataSetDataKeyTextSelection。方法 or_fail() 将其转换为 Result<T,StamError>,而方法 ? 安全地将其展开为 ResultItem<T> 或进一步传播错误。

ResultItem<T> 类型持有 对 T 的引用,其生命周期与存储相同,它还持有对存储本身的引用。您可以在所有 ResultItem<T> 实例上调用 as_ref() 以获得一个与存储生命周期相同的直接引用,这公开了一个更底层的 API。本身始终公开一个高层的 API,这在大多数情况下是您想要的。

TextSelection 的包装稍微特殊一点,而不是使用 ResultItem<TextSelection>,我们通常使用一个更专业的类型 ResultTextSelection

添加项目

将资源添加到现有存储中

let resource_handle = store.add( stam::TextResource::from_file("my-text.txt", store.config()) )?;

类似模式适用于 AnnotationDataSet

let annotationset_handle = store.add( stam::AnnotationDataSet::from_file("myset.json", store.config()) )?;

方法 add 直接添加项目,这意味着它们必须已经被构建。然而,许多 STAM 数据结构都有关联的构建器类型,并且不是直接实例化的。我们使用 annotate() 而不是 add() 来将注释添加到现有存储中

let annotation_handle = store.annotate( stam::AnnotationBuilder::new()
           .with_target( stam::SelectorBuilder::TextSelector("testres", stam::Offset::simple(6,11))) 
           .with_data("testdataset", "pos", "noun") 
)?;

这里我们看到一个使用构建器模式来构建其关联类型实例的Builder类型。实际的实例将由底层存储构建。

类似于AnnotationDataSetsTextResource的结构也有构建器,您可以通过在构建器上调用build()方法来使用它们与add()方法一起产生最终类型。

let annotationset_handle = store.add(
                   stam::AnnotationDataSetBuilder::new().with_id("testdataset"))
                                                 .with_data_with_id("pos", "noun", "D1").build()?)?;

现在让我们从头开始创建一个存储和注解,并使用显式填充的AnnotationDataSet

let store = stam::AnnotationStore::new(stam::Config::default())
    .with_id("test")
    .add( stam::TextResource::from_string("testres", "Hello world"))?
    .add( stam::AnnotationDataSet::new().with_id("testdataset")
           .add( stam::DataKey::new("pos"))?
           .with_data_with_id("pos", "noun", "D1")?
    )?
    .with_annotation( stam::Annotation::builder() 
            .with_id("A1")
            .with_target( stam::SelectorBuilder::textselector("testres", stam::Offset::simple(6,11))) 
            .with_existing_data("testdataset", "D1") )?;

这里同样是同样的东西,但这里的AnnotationDataSet是隐式填充的。

let store = stam::AnnotationStore::default().with_id("test")
    .add( stam::TextResource::from_string("testres".to_string(),"Hello world"))?
    .add( stam::AnnotationDataSet::new().with_id("testdataset"))?
    .with_annotation( stam::AnnotationBuilder::new()
            .with_id("A1")
            .with_target( stam::SelectorBuilder::textselector("testres", stam::Offset::simple(6,11))) 
            .with_data_with_id("testdataset","pos","noun","D1")
    )?;

实现将确保尽可能重用任何已存在的AnnotationData,因为不重复数据是STAM模型的核心特性之一。

还有一个AnnotationStoreBuilder,您可以使用它来实现整个注解存储的构建器模式。

文件序列化

您可以将整个注解存储(包括所有集合和注解)序列化到一个STAM JSON文件。

store.to_file("example.stam.json")?;

或者到一个STAM CSV文件(这实际上将为集合和注解创建单独的衍生CSV文件)。

store.to_file("example.stam.csv")?;

迭代器 & 搜索

遍历存储中的所有注解,并以按注解和按注解的文本的简单制表符分隔格式输出数据。

for annotation in store.annotations() {
    let id = annotation.id().unwrap_or("");
    for data in annotation.data() {
        // get the text to which this annotation refers (if any)
        let text: Vec<&str> = annotation.text().collect();
        print!("{}\t{}\t{}\t{}", id, data.key().id().unwrap(), data.value(), text.join(" "));
    }
}

以下是返回迭代器的重要方法概述,迭代器反过来又返回ResultItem<T>实例(或ResultTextSelection)。表格分为两部分,上半部分是遵循STAM所有权的简单方法,下半部分利用了计算出的各种反向索引

方法 T 描述
AnnotationStore.annotations() Annotation 存储中的所有注解
AnnotationStore.resources() TextResource 存储中的所有资源
AnnotationStore.datasets() AnnotationDataSet 存储中的所有注解集合
AnnotationDataSet.keys() DataKey 集合中的所有键
AnnotationDataSet.data() AnnotationData 集合中的所有数据
Annotation.data() AnnotationData 与注解相关的数据
------------------------------------- ----------------------- -------------------------------------
TextResource.textselections() TextSelection 资源中所有已知的文本选择(1)
TextResource.annotations() Annotation 使用TextSelectorAnnotationSelector引用此文本的注解
TextResource.annotations_as_metadata() Annotation 通过ResourceSelector引用资源的注解
AnnotationDataSet.annotations() Annotation 使用此集合的所有注解
AnnotationDataSet.annotations_as_metadata() Annotation 通过DataSetSelector引用此集合的注解
Annotation.annotations() Annotation 通过AnnotationSelector引用当前注解的注解
Annotation.annotations_in_targets() Annotation 通过AnnotationSelector被当前注解引用的注解
Annotation.textselections() Annotation 目标文本选择(通过TextSelectorAnnotationSelector
AnnotationData.annotations() Annotation 使用此数据的所有注解
DataKey.data() AnnotationData 使用此键的所有注解数据
TextSelection.annotations() Annotation 针对此文本选择的所有注解
------------------------------------- ----------------------- -------------------------------------

备注

  • (1) 与已知的文本选择,我们指的是注解引用的文本部分。
  • 左列中大部分方法仅在第二部分表格的ResultItem<T>中实现,而不是&T
  • 这个库始终使用迭代器,因此采用惰性求值。这种方法更高效且内存占用更少,因为您无需等待所有结果收集(和堆分配)后再进行计算。

STAM中的主要命名迭代器属性包括

迭代器属性 T 产生迭代器的函数
注解迭代器 Annotation annotations() / annotations_in_targets()
数据迭代器 AnnotationData data() / find_data()
文本选择迭代器 TextSelection textselections() / related_text()
资源迭代器 AnnotationData resources()
键迭代器 DataKey keys()
------------------------------------ ----------------------- -----------------------------------------------

这些迭代器公开了一个API,允许进行各种转换和过滤操作:您通常可以使用第三列中的方法将一种类型的迭代器转换为另一种类型。同样,您可以通过同名方法从ResultItem实例获得迭代器。

所有这些迭代器都有一个拥有集合对应物(Handles<T>),它将整个集合保存在内存中,项目通过引用存储来保存,因此减少了空间开销。您可以通过.to_handles()从前者转换为后者,并通过.items()从后者转换到格式。

迭代器属性 集合
注解迭代器 注解
数据迭代器 数据
资源迭代器 资源
文本选择迭代器 文本选择
键迭代器
------------------------------------ -----------------------

迭代器可以通过过滤器进行扩展,它们在构建模式中应用,并返回一个仍然实现相同属性但应用了过滤器的迭代器

过滤方法 描述
filter_annotation(&ResultItem<Annotation>) 单个注解的过滤
filter_annotations(注解) 多个注解的过滤
filter_annotationdata(&ResultItem<AnnotationData>) 单个数据项的过滤
filter_data(数据) 多个数据项的过滤
filter_key(&ResultItem<DataKey>) 数据键的过滤
filter_value(value) 数据值的过滤,参数可以是各种类型
---------------------------------------------- ----------------------

所有这些迭代器都是惰性迭代器,也就是说,除非被消费,否则不会做任何事情。一旦开始迭代,内部缓冲区可能会分配。

当您不关心实际的项目,只是想测试是否存在结果时,请使用这些迭代器上可用的test()方法。

为了提高性能,您可以将.parallel()添加到迭代器中,任何后续的迭代器方法(如通用的map()filter()),将在多个核心上并行运行。

示例

示例:检索所有具有名词(虚构模型)的注解

let dataset = store.dataset("linguistic-features").or_fail()?;
let key = dataset.key("part-of-speech").or_fail()?;
let annotationsiter = key.data().filter_value("noun".into()).annotations();

还可以按照以下方式完成,通过略微不同的路径达到相同的结果。有时一种版本的性能可能比另一种更好,这取决于你的数据是如何建模的。

let annotationsiter = key.annotations().filter_value("noun".into());

示例:测试一个单词是否被标记为名词(虚构模型)

let dataset = store.dataset("linguistic-features").or_fail()?;
let key = dataset.key("part-of-speech").or_fail()?;
if word.annotations().filter_key(&key).filter_value("noun".into()).test() {
   ...    
}

搜索数据

上述方法已经允许找到数据,但AnnotationStore和AnnotationDataSet中的find_data()方法提供了一个快捷方式,可以快速获取数据实例(通过一个DataIter)。

示例

let data = store.find_data("linguistic-features", "part-of-speech", "noun".into()).next()

在这里以及之前的示例中,我们使用into()方法将一个&str强制转换为DataOperator::Equals(&str)。还有其他可用的数据运算符,允许进行各种类型和各种比较(相等、不等、大于、小于、逻辑和/或等)。

搜索文本

以下方法可用于搜索文本,它们返回产生ResultItem<T>项的迭代器。

方法 T 描述
TextResource.find_text() TextSelection 在资源的文本中查找特定的子字符串。
TextSelection.find_text() TextSelection 在指定的文本选择中查找特定的子字符串。
TextResource.find_text_regex() TextSelection 同上,但作为基于正则表达式的强大搜索。
TextSelection.find_text_regex() TextSelection 同上,但作为基于正则表达式的强大搜索。
------------------------------------- ----------------------- -------------------------------------

related_text()方法允许查找与当前一个或多个文本选择处于某种关系的文本选择。它接受一个TextSelectionOperator作为参数,以区分各种变体。

  • Equals - 两个集合恰好覆盖相同的文本选择,并且所有这些都被覆盖(参照textfabric的==),交换律,传递律
  • Overlaps - A中的每个文本选择与B中的某个文本选择重叠(参照textfabric的&&),交换律
  • Embeds - B中的所有文本选择都被A中的某个文本选择嵌入(参照textfabric的[[)
  • Embedded - A中的所有文本选择都被B中的某个文本选择嵌入(参照textfabric的]])
  • Before - A中的每个文本选择在B中的文本选择之前(参照textfabric的<<)
  • After - A中的每个文本选择在B中的文本选择之后(参照textfabric的>>)
  • Precedes - A中的每个文本选择在B之前;它结束在B中至少一个文本选择开始的地方。
  • Succeeds - A中的每个文本选择在B之后;它开始在哪里至少一个A中的文本选择结束。
  • SameBegin - A中的每个文本选择从B中的文本选择开始的地方开始
  • SameEnd - A中的每个文本选择从B中的文本选择结束的地方开始

变体通常是通过在 TextSelectionOperator 上调用辅助函数(变体的简单小写名称)来构建的,例如:TextSelectionOperator::equals()

例如,选择句子中的所有单词(在这种情况下,句子可以是 AnnotationTextSelection

let dataset = store.dataset("structure-type").or_fail()?;
let key_word = dataset.key("word").or_fail()?;
for word in sentence.related_text(stam::TextSelectionOperator::embeds()).annotations().filter_key(key_word) {
    ...
}

查询

除了编程搜索之外,您还可以通过 STAM 查询语言 (STAMQL) 来表达查询。请注意,这会因额外的开销而产生性能惩罚。

let query: Query = "SELECT ANNOTATION ?a WHERE DATA myset type = phrase;".try_into()?;
let iter = store.query(query);
let names = iter.names();
for results in iter {
    if let Ok(result) = results.get_by_name(&names, "a") {
       if let QueryResultItem::Annotation(annotation) = result {
          ...
        }
    }
}

API 参考文档

请参阅 API 参考文档 以深入了解所有结构、特性和方法,并附带一些示例。

扩展

此库实现了以下 STAM 扩展

  • STAM-CSV - 使用 CSV 定义了替代序列化格式。
  • STAM-Query - 定义了 STAM 查询语言。
  • STAM-Transpose - 定义了在不同资源之间链接相同的文本部分。
  • STAM-Textvalidation - 定义了一种机制,以确保注释目标可以检查更改。

Python 绑定

此库包含 Python 绑定,请参阅 此处

致谢

这项工作是在 KNAW 人文集群数字基础设施部门 进行,并由 CLARIAH 项目(CLARIAH-PLUS,NWO 奖励 184.034.023)作为共享开发路线图 FAIR 注释轨迹的一部分资助。

依赖关系

~10MB
~182K SLoC