#nlp #text-processing #annotation #linguistics #xml

bin+lib folia

High-performance library for handling the FoLiA XML format (Format for Linguistic Annotation)

6 releases

0.0.6 Nov 16, 2020
0.0.5 Sep 29, 2020
0.0.3 Aug 12, 2020
0.0.2 Oct 3, 2019

#27 in Science

38 downloads per month
Used in deepfrog

GPL-3.0+

495KB
8K SLoC

Crate Docs Build Status Language Machines Badge Project Status: Active – The project has reached a stable, usable state and is being actively developed.

This is a high-performance Rust library for handling the FoLiA XML format, a rich format for linguistic annotation.

This library is currently in alpha stage, it may already be used to read FoLiA documents and to create documents from scratch. Note that this library does not yet implement validation!. You will have to ensure your FoLiA documents are valid by running another FoLiA validator, as this library does not yet guarantee producing valid FoLiA.

For a comparison of FoLiA libraries and a list of implemented features, see FoLiA Implementations.

Installation

Add folia to your project's Cargo.toml.

Usage

Reading from file and querying all words:

extern crate folia;

use folia;

//load document from file
let doc = folia::Document::from_file(filename, folia::DocumentProperties::default()).expect("parsing folia");
//Build a query, here you can match on any attribute
let query = folia::Query::select().element(folia::Cmp::Is(folia::ElementType::Word));
//Turn the query into a specific selector
let selector = folia::Selector::from_query(&doc, &query).expect("selector");

//Run the selector
for word in doc.select(selector, folia::Recursion::Always) {
    //print the ID and the text
    println!("{}\t{}",
        word.id().or(Some("No-ID")),
        word.text(&folia::TextParameters::default())
    );
}

A common pattern is to query in two stages, methods like get_annotation(), get_annotations() provide shortcut alternatives to select(). Let's output Part-of-Speech tags:

//Run the selector
for word in doc.select(selector, folia::Recursion::Always) {
    if let Some(pos) = word.get_annotation(folia::AnnotationType::POS, folia::Cmp::Any, folia::Recursion::No) {
        println!(pos.class().unwrap());
    }
}

We can create a document from scratch, all new elements can be added using the high-level annotate() method:

let doc = folia::Document::new("example", folia::DocumentProperties::default()).expect("instantiating folia");
let root: ElementKey = 0; //root element always has key 0
//add a sentence, returns its key
let sentence = doc.annotate(root,
                    folia::ElementData::new(folia::ElementType::Sentence).
                    with_attrib(folia::Attribute::Id("s.1".to_string())) ).expect("Adding sentence");

doc.annotate(sentence,
             ElementData::new(ElementType::Word)
             .with_attrib(Attribute::Id("word.1".to_string()))
             .with_text("hello".to_string())
            ).expect("Adding word 1");

doc.annotate(sentence,
             ElementData::new(ElementType::Word)
             .with_attrib(Attribute::Id("word.2".to_string()))
             .with_text("world".to_string())
            ).expect("Adding word 2");

Let's add a named entity for the above two words:

doc.annotate(sentence,
             ElementData::new(ElementType::Entity)
             .with_attrib(Attribute::Set("adhoc".to_string()))
             .with_attrib(Attribute::Class("greeting".to_string()))
             .with_span(&[ "word.1", "word.2" ])
).expect("adding entity");

Note that this will work regardless of the first parameter (sentence), as the span is explicitly provided: annotate() will automatically find out where add the layer (if needed).

If you have an element's key (a numerical internal identifier), you can easily obtain a FoliaElement instance:

if let Some(element) = doc.get_element(key) {

}

If you have it's official ID, you can do:

if let Some(element) = doc.get_element_by_id("example.s.1.w.1") {

}

Declarations

All annotation types need to be declared in FoLiA, but the library does that for you automatically as long as you don't set DocumentProperties.autodeclare to false. Explicit declarations are done using Document.declare(). Here is a simple set-less declaration:

doc.declare(folia::AnnotationType::SENTENCE, &None, &None, &None);

Here a more elaborate one:

doc.declare(folia::AnnotationType::POS, Some("https://somewhere/my/pos/set".to_string()), &None, &None);

Provenance

FoLiA v2 comes with extensive provenance support, so this library implements that as well. You can associate an active processor by setting it in folia::DocumentProperties:

    let processor = Processor::new("test".to_string()).autofill();
    let doc =  Document::new("example", DocumentProperties::default().with_processor(processor)).expect("document");

Switching processors on-the-fly can be done with doc.active_processor(processor_key). Any declarations made after activating a processor will automatically assign that processor.

Benchmarks

As the primary goal of this library is to provide a high-performance library, we ran some limited benchmarks against the other more mature and more feature complete FoLiA libraries: FoliaPy, written in Python, and libfolia, written in C++.

Tested on a Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz, Linux 5.3

Note: The folia-rust implementation does only a minimal validation whereas the others do a a complete shallow validation on parsing, including also a text consistency validation.

Benchmarks on a +-100MB FoLiA document

(bosb002gide03_01.nederlab.folia.xml)

Parse from file into a full memory representation (DOM)

Implementation CPU Memory Peak Memory
foliapy v2.2.1 60.9 s 2083 MB -
libfolia v2.3 14.7 s 2656 MB 2681 MB
folia-rust v0.0.1 2.6 s 531 MB 622 MB

Selecting and iterating over all words

Implementation CPU Memory Peak Memory
foliapy v2.2.1 1.46 s - -
libfolia v2.3 0.84 s - -
folia-rust v0.0.1 0.122 s - -

Serialisation (without disk writing)

Implementation CPU Memory Peak Memory
foliapy v2.2.1 77.7 s - -
libfolia v2.3 5.06s - -
folia-rust v0.0.1 1.14s - -

Dependencies

~2.9–4MB
~70K SLoC