A Go library implementing an FST (finite state transducer)
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
cmd/vellum Fix vellum's imports to couchbase/... from couchbaselabs/... Dec 20, 2017
data refactor slighlty to reduce number of files Apr 18, 2017
docs fix more typos Mar 16, 2017
levenshtein optimize levenshtein addMismatchUtf8States() w/ global all-runes Jul 18, 2018
regexp MB-30264 - expose regexp.DefaultLimit Aug 10, 2018
utf8 utf8.NewSequencesPrealloc() API Jul 18, 2018
vendor add support for dot and svg exporting via vellum cmd-line tool Apr 17, 2017
.travis.yml fix travis script Apr 18, 2017
CONTRIBUTING.md add CONTRIBUTIN.md which describes Couchbase CLA Mar 23, 2017
LICENSE first commit Mar 15, 2017
README.md added example of dot file generation to README Jul 20, 2018
automaton.go Address gometalinter warnings Jun 27, 2018
builder.go transitionPool allows reuse of transition instances Mar 14, 2018
builder_test.go fix unchecked err Apr 18, 2017
common.go first commit Mar 15, 2017
common_test.go first commit Mar 15, 2017
decoder_v1.go fix behavior when empty string has a non-zero value Jun 11, 2018
decoder_v1_test.go optimize decoder.stateAt() with optional preallocated fstState Feb 14, 2018
encoder_v1.go add ability to resuse vellum builders Feb 28, 2018
encoder_v1_test.go fix vellum to have bounded memory use during build phase Apr 15, 2017
encoding.go add ability to resuse vellum builders Feb 28, 2018
example_test.go Fix vellum's imports to couchbase/... from couchbaselabs/... Dec 20, 2017
fst.go optimize FST.get() input slice access Jul 18, 2018
fst_iterator.go simplify FSTIterator.Seek() Jul 18, 2018
fst_iterator_test.go MB-30264 - test Search() w/ automata & beg/end keys Aug 10, 2018
merge_iterator.go fix behavior for merge iterator on empty list May 14, 2017
merge_iterator_test.go Add Reset() method to Iterator Nov 14, 2017
pack.go first commit Mar 15, 2017
pack_test.go first commit Mar 15, 2017
registry.go add ability to resuse vellum builders Feb 28, 2018
registry_test.go fix vellum to have bounded memory use during build phase Apr 15, 2017
transducer.go API and interface streamlining Apr 30, 2017
vellum.go new MergeIterator and Merge building function May 14, 2017
vellum_mmap.go API and interface streamlining Apr 30, 2017
vellum_nommap.go API and interface streamlining Apr 30, 2017
vellum_test.go MB-30987 - added large key benchmark w/ various 4MB to 1K keys Aug 22, 2018
writer.go add ability to resuse vellum builders Feb 28, 2018
writer_test.go first commit Mar 15, 2017

README.md

vellum vellum

Build Status Coverage Status GoDoc Go Report Card License

A Go library implementing an FST (finite state transducer) capable of:

  • mapping between keys ([]byte) and a value (uint64)
  • enumerating keys in lexicographic order

Some additional goals of this implementation:

  • bounded memory use while building the FST
  • streaming out FST data while building
  • mmap FST runtime to support very large FTSs (optional)

Usage

Building an FST

To build an FST, create a new builder using the New() method. This method takes an io.Writer as an argument. As the FST is being built, data will be streamed to the writer as soon as possible. With this builder you MUST insert keys in lexicographic order. Inserting keys out of order will result in an error. After inserting the last key into the builder, you MUST call Close() on the builder. This will flush all remaining data to the underlying writer.

In memory:

  var buf bytes.Buffer
  builder, err := vellum.New(&buf, nil)
  if err != nil {
    log.Fatal(err)
  }

To disk:

  f, err := os.Create("/tmp/vellum.fst")
  if err != nil {
    log.Fatal(err)
  }
  builder, err := vellum.New(f, nil)
  if err != nil {
    log.Fatal(err)
  }

MUST insert keys in lexicographic order:

err = builder.Insert([]byte("cat"), 1)
if err != nil {
  log.Fatal(err)
}

err = builder.Insert([]byte("dog"), 2)
if err != nil {
  log.Fatal(err)
}

err = builder.Insert([]byte("fish"), 3)
if err != nil {
  log.Fatal(err)
}

err = builder.Close()
if err != nil {
  log.Fatal(err)
}

Using an FST

After closing the builder, the data can be used to instantiate an FST. If the data was written to disk, you can use the Open() method to mmap the file. If the data is already in memory, or you wish to load/mmap the data yourself, you can instantiate the FST with the Load() method.

Load in memory:

  fst, err := vellum.Load(buf.Bytes())
  if err != nil {
    log.Fatal(err)
  }

Open from disk:

  fst, err := vellum.Open("/tmp/vellum.fst")
  if err != nil {
    log.Fatal(err)
  }

Get key/value:

  val, exists, err = fst.Get([]byte("dog"))
  if err != nil {
    log.Fatal(err)
  }
  if exists {
    fmt.Printf("contains dog with val: %d\n", val)
  } else {
    fmt.Printf("does not contain dog")
  }

Iterate key/values:

  itr, err := fst.Iterator(startKeyInclusive, endKeyExclusive)
  for err == nil {
    key, val := itr.Current()
    fmt.Printf("contains key: %s val: %d", key, val)
    err = itr.Next()
  }
  if err != nil {
    log.Fatal(err)
  }

How does the FST get built?

A full example of the implementation is beyond the scope of this README, but let's consider a small example where we want to insert 3 key/value pairs.

First we insert "are" with the value 4.

step1

Next, we insert "ate" with the value 2.

step2

Notice how the values associated with the transitions were adjusted so that by summing them while traversing we still get the expected value.

At this point, we see that state 5 looks like state 3, and state 4 looks like state 2. But, we cannot yet combine them because future inserts could change this.

Now, we insert "see" with value 3. Once it has been added, we now know that states 5 and 4 can longer change. Since they are identical to 3 and 2, we replace them.

step3

Again, we see that states 7 and 8 appear to be identical to 2 and 3.

Having inserted our last key, we call Close() on the builder.

step4

Now, states 7 and 8 can safely be replaced with 2 and 3.

For additional information, see the references at the bottom of this document.

What does the serialized format look like?

We've broken out a separate document on the vellum disk format v1.

What if I want to use this on a system that doesn't have mmap?

The mmap library itself is guarded with system/architecture build tags, but we've also added an additional build tag in vellum. If you'd like to Open() a file based representation of an FST, but not use mmap, you can build the library with the nommap build tag. NOTE: if you do this, the entire FST will be read into memory.

Can I use this with Unicode strings?

Yes, however this implementation is only aware of the byte representation you choose. In order to find matches, you must work with some canonical byte representation of the string. In the future, some encoding-aware traversals may be possible on top of the lower-level byte transitions.

How did this library come to be?

In my work on the Bleve project I became aware of the power of the FST for many search-related tasks. The obvious starting point for such a thing in Go was the mafsa project. While working with mafsa I encountered some issues. First, it did not stream data to disk while building. Second, it chose to use a rune as the fundamental unit of transition in the FST, but I felt using a byte would be more powerful in the end. My hope is that higher-level encoding-aware traversals will be possible when necessary. Finally, as I reported bugs and submitted PRs I learned that the mafsa project was mainly a research project and no longer being maintained. I wanted to build something that could be used in production. As the project advanced more and more techniques from the BurntSushi/fst were adapted to our implementation.

Are there tools to work with vellum files?

Under the cmd/vellum subdirectory, there's a command-line tool which features subcommands that can allow you to create, inspect and query vellum files.

How can I generate a state transition diagram from a vellum file?

The vellum command-line tool has a "dot" subcommand that can emit graphviz dot output data from an input vellum file. The dot file can in turn be converted into an image using graphviz tools. Example...

$ vellum dot myFile.vellum > output.dot
$ dot -Tpng output.dot -o output.png

Related Work

Much credit goes to two existing projects:

Most of the original implementation here started with my digging into the internals of mafsa. As the implementation progressed, I continued to borrow ideas/approaches from the BurntSushi/fst library as well.

For a great introduction to this topic, please read the blog post Index 1,600,000,000 Keys with Automata and Rust