npm.io
3.3.0 • Published 1 week ago

modern-ahocorasick

Licence
MIT
Version
3.3.0
Deps
0
Size
320 kB
Vulns
0
Weekly
0
Stars
21

modern-ahocorasick

Native extensions add dynamic dictionaries, Unicode folding streams, character boundaries, tokenization/replacement streams, Node/Web adapters, filters, previews and a double-array backend. See the extension guide. No runtime dependencies are added.

Match many keywords in Unicode text with Aho–Corasick. Compile a dictionary once, then find, count or replace matches using ranges that work with JavaScript's slice().

npm CI MIT license

English · 简体中文

Documentation · Interactive workbench · API reference

Install and find matches

npm install modern-ahocorasick
# or: pnpm add modern-ahocorasick
import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['cat', '猫'])
const text = '😀cat和猫'

const matches = matcher.search(text)
// [
//   { pattern: 'cat', patternIndex: 0, start: 2, end: 5, data: undefined },
//   { pattern: '猫', patternIndex: 1, start: 6, end: 7, data: undefined },
// ]

matches.map(({ start, end }) => text.slice(start, end))
// ['cat', '猫']

Matching respects complete Unicode grapheme clusters, including emoji and combining characters. Results use original-text UTF-16 offsets: start is inclusive and end is exclusive. Reuse the matcher across texts; construct a new one to change its dictionary.

Choose a method

You need Method Result
Check whether any keyword occurs match(text) Boolean; stops at the first hit
Count every occurrence count(text) Number; includes overlaps and duplicate entries
Count each dictionary entry countByPattern(text) Counts in input order; includes zeroes and duplicates
Match incoming chunks createStream(options?) Stateful writes, EOF flush and cancellation
Return one match without collecting findFirst() / findAt() Low-allocation first-hit queries
Save or load a dictionary serialize() / AhoCorasick.deserialize() Versioned, validated compiled data
Distribute a compiled artifact serializeArtifact() / deserializeArtifact() Profile-validated Worker/Serverless payload
Collect matched ranges search(text, options?) Array of independent match objects
Read matches as needed iterate(text, options?) Lazy iterator over match objects
Replace non-overlapping matches replace(text, replacement, options?) New string

count() avoids creating match objects. iterate() avoids collecting a result array and stops scanning when you stop consuming it. It accepts a complete string, not a stream of chunks; the iterator retains the text and dictionary.

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['he', 'she', 'hers'])

matcher.match('ushers') // true
matcher.count('ushers') // 3 — 'she', 'he' and 'hers' overlap
matcher.countByPattern('ushers') // [1, 1, 1]

for (const hit of matcher.iterate('ushers')) {
  console.log(hit.pattern) // 'she'
  if (hit.pattern === 'she') {
    break
  }
}

For full signatures and validation behavior, see the API reference.

Additional matching tools

countByPattern(), whole-word options, streams, compiled persistence and the optional text adapter require v3.1.0 or later.

matcher.countByPattern('ushers') // [1, 1, 1]
new AhoCorasick(['cat']).match('concatenate', { wholeWord: true, locale: 'en' }) // false
const restored = AhoCorasick.deserialize(matcher.serialize())
const stream = restored.createStream()
const hits = [...stream.write('ush'), ...stream.write('ers'), ...stream.finish()]

For normalization or full Unicode case folding, import the separate TextMatcher constructor from modern-ahocorasick/text. It maps results back to original UTF-16 ranges, including expansions such as ß → ss. It also provides transformed createStream(), createTokenStream(), createReplaceStream(), serialize() and deserialize(). The default entry keeps exact matching and does not load the folding table.

See the API guide for word boundary rules, metadata codecs, stream buffering/cancellation and conversion costs. Stream tails are bounded explicitly and never silently truncated.

Select overlaps deliberately

search() and iterate() default to all. Use a leftmost strategy when ranges must not overlap, for example before highlighting text.

Strategy Selection Order
all Every occurrence, including overlaps and duplicate entries End ascending; then pattern length descending; then input order
leftmost-first Earliest start; input order breaks same-start ties Start ascending, non-overlapping
leftmost-longest Earliest start; longest pattern wins same-start ties, then input order Start ascending, non-overlapping
import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['a', 'ab', 'bc'])

matcher.search('abc', { strategy: 'leftmost-first' }).map(hit => hit.pattern)
// ['a', 'bc']
matcher.search('abc', { strategy: 'leftmost-longest' }).map(hit => hit.pattern)
// ['ab']

“Longest” breaks ties at the same start; it does not choose the longest match anywhere in the text. Adjacent matches are retained. With all, the first result is the earliest-ending match, which is not necessarily the leftmost one. Non-overlapping iteration may look ahead by the longest keyword's grapheme length.

Attach metadata and replace text

Dictionary entries can be strings or { pattern, data } records. Each input entry keeps its patternIndex, so duplicate keywords remain distinct. Metadata is caller-owned and retained by reference, not deep-cloned. Changing input records or returned match objects does not reconfigure the matcher.

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick([
  { pattern: 'cat', data: { replacement: '猫' } },
  { pattern: 'dog', data: { replacement: '狗' } },
])

matcher.replace('cat and dog', hit => hit.data.replacement)
// '猫 and 狗'
matcher.replace('cat', '__CODE_BLOCK_5__amp;') // '__CODE_BLOCK_5__amp;' — string replacements are literal

replace() defaults to leftmost-longest and also accepts leftmost-first. It rejects all. A callback receives (match, originalSubstring) and must return a string. Replacement runs once over the original ranges; inserted text is not searched again. The library produces strings and ranges, not HTML; escape text according to your rendering framework when highlighting.

Unicode and compatibility

Matching is case-sensitive and does not normalize Unicode. é and e\u0301 are different patterns, and e does not match inside e\u0301. Grapheme boundaries follow the runtime's ICU/Unicode version. Read Unicode and indices for examples and coordinate conventions.

Environment Requirement
JavaScript runtime ES2022 support and Intl.Segmenter; no bundled polyfill
Modules ESM default import or direct CommonJS require()
TypeScript 5.3+ for the bundled declarations
const AhoCorasick = require('modern-ahocorasick')

const matcher = new AhoCorasick(['cat'])
matcher.match('cat') // true
import type { Match, PatternInput } from 'modern-ahocorasick'
import AhoCorasick from 'modern-ahocorasick'

const patterns: PatternInput<{ id: string }>[] = [
  { pattern: 'cat', data: { id: 'animal-cat' } },
]
const matcher = new AhoCorasick(patterns)
const matches: Match<{ id: string }>[] = matcher.search('cat')

An empty dictionary is valid. Empty keywords throw RangeError; invalid runtime arguments throw TypeError. Counts above Number.MAX_SAFE_INTEGER throw RangeError. Development Node.js requirements are separate from consumer runtime requirements.

Upgrade from v2

v3 is available on npm. search() now returns individual match objects with UTF-16 ranges instead of grouped tuples. The default constructor export, direct CommonJS require() and match(text): boolean remain supported.

Follow the v2 → v3 migration guide and consult the changelog.

Performance

Aho–Corasick compiles a reusable dictionary. For g input graphemes and z occurrences, all-match search takes O(g log(d + 1) + z) time, excluding runtime segmentation costs; d is the maximum transition degree. count() uses aggregate counts in O(g log(d + 1)) scan time; match() can stop early. Search retains its result array, while non-overlapping selection uses a candidate window bounded by the longest keyword. Replacement still allocates output text.

Performance depends on the dictionary, input, output density and runtime. See the reproducible benchmarks for workload-specific results, memory measurements and comparison limitations.

Contribute

See the contributing guide for local development, validation, releases and documentation deployment. Report a bug with a small reproduction and your runtime version.

License and credits

MIT. Originally forked from BrunoRB/ahocorasick, based on Aho and Corasick's “Efficient string matching: an aid to bibliographic search”. Modern library maintained by SonOfMagic.

The optional text adapter includes Unicode 17 case-folding data under the Unicode License V3.

Ranges, anchoring and compile statistics

Every whole-text query accepts start, end and anchored. Offsets are original UTF-16 positions in a half-open range; defaults are 0, text.length and false. Coordinates must be safe integers within the input, ordered and at original grapheme boundaries, otherwise a RangeError is thrown. A non-boolean anchored throws TypeError. An empty range has no matches. Anchoring accepts only hits beginning exactly at start.

import AhoCorasick from 'modern-ahocorasick'

const matcher = new AhoCorasick(['abc', 'bc'])
matcher.search('!abc!', { start: 2, end: 4, anchored: true })
// [{ pattern: 'bc', patternIndex: 1, start: 2, end: 4, data: undefined }]
matcher.replace('!abc!', 'X', { start: 2, end: 4 }) // '!aX!'
matcher.getStats() // frozen, cached scalar diagnostics

search, iterate, match, count, countByPattern, replace and tokenize share the contract, including /unicode, /fast, /unicode-fast and /text. Range filtering happens before overlap selection; whole-word and constructor boundary rules still inspect the complete input. Replacement and tokens retain text outside the range. Folding/normalization never changes the coordinate system. This is a semantic range filter; it does not promise work proportional only to the selected span. Streams reject these offline options (even anchored: false).

getStats() on the default and derived compiled constructors returns backend (compact or double-array), patternCount (duplicates included), stateCount (root included, unused DAT slots excluded), transitionCount (trie edges), alphabetSize, maxPatternUnits, unit (grapheme or folded-codepoint) and typedArrayBytes. Folded units are codepoints after Unicode folding, so ß has two units there. Byte counts include retained typed scan/auxiliary arrays, including unused allocated slots; they exclude strings, Maps, JS objects, temporary build allocations, and native ICU. They are not total heap usage. Values are compiled once; reads do not traverse tables. Compact deserialization recreates identical statistics; serializing /fast uses the existing portable compact format, so restored backend/storage statistics describe compact storage. /text is a mapping adapter, not a compiled-constructor subclass, and does not expose getStats().

The cross-language source assessment and measurements live in the repository's docs/research/ directory. This batch borrows range/diagnostic and contiguous-output design ideas; the default backend remains compact and no native runtime dependency is added.

Keywords