Custom components and extension attributes

spaCy · Advanced · 15 min

Custom components let you package business logic as part of the pipeline so every Doc comes out fully annotated.

A component is any callable that takes a Doc and returns a Doc. Register it so spaCy can add it by name and save it with the pipeline:

@Language.component("flag_urgent")
def flag_urgent(doc):
    doc._.urgent = any(t.lower_ in {"asap", "urgent"} for t in doc)
    return doc

nlp.add_pipe("flag_urgent", last=True)

For components with settings or state (a lookup table, a model), use `@Language.factory("name", default_config={...})` on a function that receives nlp, name and the config values and returns the component object. Config values can then be set in add_pipe(..., config={...}) or a training config file.

Extension attributes add your own fields to Doc, Token and Span under the ._ namespace, keeping them apart from built-in attributes:

  • attribute: Doc.set_extension("urgent", default=False), writable per object
  • getter: Token.set_extension("is_color", getter=lambda t: t.lower_ in COLORS), computed on access
  • method: Span.set_extension("count", method=lambda span, w: ...)

Register extensions once at import time; force=True overwrites an existing one (useful in notebooks).

Components should only add information, be deterministic and avoid heavy work at import. Place them relative to others with before=/after= and remember dependencies: a component reading ent_type_ must run after ner. Extension values are stored in doc.user_data and serialised with the Doc when using DocBin(store_user_data=True).

Example

import spacy
from spacy.language import Language
from spacy.tokens import Doc, Span
Doc.set_extension("urgent", default=False, force=True)
Span.set_extension("is_money_big", force=True,
                   getter=lambda s: s.label_ == "MONEY" and "million" in s.text)

@Language.component("flag_urgent")
def flag_urgent(doc):
    doc._.urgent = any(t.lower_ in {"asap", "urgent"} for t in doc)
    return doc

@Language.factory("keyword_tagger", default_config={"keywords": ["refund"]})
def make_keyword_tagger(nlp, name, keywords):
    kw = set(keywords)
    def tagger(doc):
        doc.user_data["keywords"] = [t.text for t in doc if t.lower_ in kw]
        return doc
    return tagger
nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("flag_urgent", after="ner")
nlp.add_pipe("keyword_tagger", config={"keywords": ["refund", "invoice"]})
doc = nlp("URGENT: refund the $3 million invoice ASAP.")
print(doc._.urgent, doc.user_data["keywords"])          # True ['refund', 'invoice']
print([(e.text, e._.is_money_big) for e in doc.ents])   # [('$3 million', True), ('ASAP', False)]

Remember

  • A component is a Doc -> Doc callable registered with @Language.component
  • Use @Language.factory for configurable or stateful components
  • Extensions on Doc/Token/Span live under ._ (attribute, getter, method)
  • Mind component order and dependencies with before=/after=

Watch out for

  • Registering extensions inside the component function, which runs on every Doc; register once at import.

Check yourself

  1. When should you use @Language.factory instead of @Language.component?
  2. Which extension type computes its value on each access?

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

Official docs: spacy.io/usage/processing-pipelines#custom-components