Vocab, StringStore and hashes

spaCy · Expert · 12 min

Hashes explain the int-versus-string attributes, shared memory between Docs and why the vocab grows in long-running services.

spaCy never stores the same string twice. The `StringStore` (nlp.vocab.strings) maps every string to a 64-bit hash and back: strings["coffee"] gives the hash, strings[hash] the text. Hashes are deterministic, so the same word has the same id in every process and pipeline, which lets data be exchanged as plain integers.

The `Vocab` holds the StringStore, the vectors table, morphology and Lexemes. A Lexeme is the context-independent entry for a word type, with flags like is_stop, is_alpha, like_num, lower_, shape_ that are the same wherever the word appears. A Token points to its Lexeme for these attributes and adds context-dependent ones (tag, dependency, entity).

Because Docs store token data as compact C structs holding hashes and pointers, they're memory-efficient and doc.to_array([LOWER, POS, ENT_TYPE]) exports attributes as a NumPy uint64 array in one call. doc.count_by(attr) counts values quickly.

A consequence: to decode a hash you need a StringStore that has seen the string. Docs deserialised from bytes in another process need the strings shipped too, which DocBin handles.

In a long-running service, every new string (typos, ids, URLs) is added to the StringStore and stays there, so memory grows slowly. Mitigate by periodically reloading the pipeline, or process with a fresh nlp in worker processes that are recycled.

Example

import spacy
from spacy.attrs import IS_ALPHA, LOWER, POS

nlp = spacy.load("en_core_web_sm")
strings = nlp.vocab.strings

h = strings["coffee"]
print(h)                              # 3197928453018144401
print(strings[h])                     # coffee
print(spacy.blank("de").vocab.strings["coffee"] == h)   # True: same hash anywhere

doc = nlp("I love coffee and coffee loves me")
print(doc[2].orth == doc[4].orth == h)                  # True
lex = nlp.vocab["coffee"]
print(type(lex).__name__, lex.is_alpha, lex.shape_)    # Lexeme True xxxx

arr = doc.to_array([LOWER, POS, IS_ALPHA])
print(arr.shape, arr.dtype)           # (7, 3) uint64
counts = doc.count_by(LOWER)
print(counts[h])                      # 2

n = len(strings)
nlp("zxqv-unique-id-91")
print(len(strings) > n)               # True: new strings accumulate

Remember

  • StringStore maps strings to deterministic 64-bit hashes and back
  • Lexemes hold context-free word attributes; Tokens add context
  • doc.to_array and count_by export attributes efficiently
  • The StringStore grows with every new string in long-running services

Watch out for

  • Sending hashes to another process and trying to decode them with a StringStore that never saw those strings.

Check yourself

  1. Why is the hash of 'coffee' the same in two unrelated spaCy pipelines?
  2. Which attribute is stored on the Lexeme rather than the Token?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: spacy.io/api/stringstore