A 5-minute mental model. Read this before writing compromise code.
compromise is a rule-based NLP library for English. It tokenizes text, tags each word with
its part-of-speech, and lets you find and transform parts of the text. It is fast,
runs offline in the browser or node, has no dependencies, and makes "good-enough" decisions —
it is not a neural/LLM model and does not call out to one.
import nlp from 'compromise'
let doc = nlp('she sells seashells by the seashore.')
doc.verbs().toPastTense()
doc.text()
// 'she sold seashells by the seashore.'| Object | What it is |
|---|---|
| Document | the whole parsed text, held internally as Term[][] (sentences of terms). |
| View | a selection of terms within a document — the thing every method returns. nlp(text) returns a View of the whole document; .match('#Verb') returns a smaller View. A View still knows about its whole document (.all() zooms back out). |
| Term | a single tokenized word, with .text, .pre/.post (surrounding whitespace/punctuation), .normal, and a Set of tags. |
Almost every method is called on a View and returns a new View, so calls chain:
nlp(text).match('#Person+').first().text()A View points at the underlying document. Transform methods mutate that shared document in place, even though they also return a View:
let doc = nlp('I walk to work')
doc.verbs().toPastTense() // mutates doc, even though we didn't reassign
doc.text() // 'I walked to work' ← doc changed- Read-only methods (
.match,.if,.found,.text,.json,.has, accessors) do not change the document. - Transform methods (
.toPastTense,.replace,.remove,.tag,.normalize, case/whitespace methods …) do. - To work on a copy without touching the original, call
.clone()first:
let past = doc.clone().verbs().toPastTense().text() // doc is untouchedMatching does not cross sentence boundaries by default.
nlp("that's it. Back to Winnipeg!").has('it back') // falseFor multi-sentence / paragraph matching use the compromise-paragraphs plugin.
The default import is the full library. Smaller builds exist for size/speed:
| Import | Includes | Use when |
|---|---|---|
compromise (= compromise/three) |
tokenize + tags + all selections (.nouns(), .verbs(), .people(), .numbers() …) |
default — what you almost always want |
compromise/two |
tokenize + POS tags + contractions | you only need tags + .match(), not the named selections |
compromise/one (= compromise/tokenize) |
tokenize only, no tags | you only need words/sentences split up; #Tag patterns won't work |
import nlp from 'compromise' // full (recommended)
import nlp from 'compromise/two' // tags, no selections
import nlp from 'compromise/tokenize' // tokens onlyThere is no meaningful tree-shaking beyond these tiers — the tagger is greedy and competitive, so pulling individual pieces out is not recommended. Run the full library unless size is critical.
doc.text() // a string
doc.out('array') // array of strings, one per match
doc.json() // rich data: terms, tags, offsets, etc.
doc.debug() // pretty-print the tagging to the console (great for debugging)Three escalating ways to teach compromise new things — see recipes.md and
the .extend() section of the README:
- A lexicon object at parse time:
nlp(text, { kermit: 'FirstName' }) nlp.addWords({...})/nlp.addTags({...})globally.nlp.plugin({...})/nlp.extend({...})— add words, tags, new methods, and post-processing.
- No nested match syntax —
'(modern (major|minor))? general'is not supported. Chain successive.match()calls instead. - No inter-sentence matching without the paragraphs plugin (see above).
- Slashes split into separate words —
nlp('eats/shoots/leaves').has('koala leaves')isfalse. - No dependency/parse tree — transformations are heuristic, not grammar-tree based.
- match-syntax.md — the match mini-language
- tags.md — the full tagset
- api.md — every method
- recipes.md — copy-paste task examples