Yaket is a TypeScript keyword extraction library that ports the core YAKE pipeline into a form that works in Node, browser-style bundles, and Cloudflare Workers.
It is designed for teams that want upstream-like YAKE behavior, deterministic results, and a typed API that can plug into ingestion pipelines such as Bobbin or future consumers such as flux-search.
Yaket is an independent TypeScript port/reimplementation of the YAKE approach and is based on the upstream Python YAKE project:
- Original project: INESCTEC/yake
The underlying research is:
- Ricardo Campos, Vitor Mangaravite, Arian Pasquali, Alipio Jorge, Celine N. S. Santos, and Adam Jatowt.
YAKE! Keyword Extraction from Single Documents using Multiple Local Features.
Information Sciences 509 (2020), 257-289.
DOI:
10.1016/j.ins.2019.09.013
Yaket aims to preserve the core YAKE behavior where practical, while adapting the implementation to JavaScript/TypeScript runtimes and edge-compatible packaging.
- Upstream-shaped YAKE core:
KeywordExtractor,DataCore,SingleWord,ComposedWord, and the core scoring/dedup flow are implemented in TypeScript. - Edge-safe extraction path: stopwords are bundled, and the extraction core avoids Node-only runtime dependencies.
- Pipeline-friendly API: one-shot extraction, reusable extractor instances, Bobbin-compatible adapter output, and document-oriented helpers are all available.
- Surface-form preserving results: returned
keywordvalues keep the observed case from the source text, whilenormalizedKeywordcarries the normalized matching form for downstream logic. - Verification-heavy: regression fixtures, Python parity checks, property-based tests, Cloudflare runtime tests, and a benchmark harness are checked in.
Yaket extracts weighted keywords from a single document using a YAKE-style local-feature pipeline.
It is designed for cases where you want:
- a deterministic keyword extractor
- no LLM dependency
- browser/edge compatibility
- a practical JavaScript alternative to the Python YAKE package
Requires Node.js 20+
npm install @ade_oshineye/yaketimport { extract } from "@ade_oshineye/yaket";
const keywords = extract(
"Google is acquiring data science community Kaggle.",
{ language: "en", n: 3, top: 5 },
);
console.log(keywords);Expected shape:
[
["science community Kaggle", 0.022868570857866696],
["community Kaggle", 0.04778970771086575],
]Install from npm:
npm install @ade_oshineye/yaketThe package ships ESM output and exposes Worker/browser-safe entry points:
@ade_oshineye/yaket@ade_oshineye/yaket/browser@ade_oshineye/yaket/worker
At a high level, Yaket:
- preprocesses text into sentences and tokens
- generates single-word and multi-word candidates up to
n - scores single words using local YAKE-style features such as frequency, spread, position, casing, and co-occurrence-derived relations
- scores composed phrases from those single-word scores
- deduplicates the ranked list with
seqm,levs, orjaro
See docs/architecture.md for the pipeline structure and docs/algorithm-drift.md for known deviations from upstream YAKE.
Common options:
| Option | Meaning | Default |
|---|---|---|
language |
language code | en |
n |
maximum n-gram size | 3 |
top |
number of results to return | 20 |
dedupFunc |
dedup function (seqm, levs, jaro) |
seqm |
dedupLim |
dedup threshold | 0.9 |
windowSize |
co-occurrence window | 1 |
stopwords |
explicit stopword iterable override | bundled set for language |
For the complete public API, see docs/api-reference.md.
Canonical option names:
languagededupLimdedupFunc—seqm,levs, orjarowindowSize
Yaket 0.6 dropped the snake_case aliases (lan, dedup_lim, dedup_func,
windowsSize, window_size), the extract_keywords() method, and the
dedup-function value aliases (leve, jaro_winkler, sequencematcher).
Existing 0.5.x consumers should follow docs/migration-bobbin-0.6.md.
If you prefer the most concise one-shot API, extract() is a re-export of extractKeywords().
import { KeywordExtractor } from "@ade_oshineye/yaket";
const extractor = new KeywordExtractor({
language: "en",
n: 3,
top: 10,
});
const keywords = extractor.extractKeywords(
"Cloudflare Workers process requests close to users.",
);import { extractKeywordDetails } from "@ade_oshineye/yaket";
const details = extractKeywordDetails("Machine learning improves software delivery.", {
language: "en",
n: 2,
top: 5,
});extractKeywordDetails() returns:
type KeywordResult = {
keyword: string;
normalizedKeyword: string;
score: number;
ngramSize: number;
occurrences: number;
sentenceIds: number[];
};keyword preserves the source-text surface form; normalizedKeyword is the normalized comparison key used for deduplication and downstream matching.
import { extractFromDocument, serializeDocumentKeywordResult } from "@ade_oshineye/yaket";
const result = extractFromDocument({
id: "doc-1",
language: "en",
title: "Edge runtimes",
body: "Cloudflare Workers process requests close to users.",
});
const serialized = serializeDocumentKeywordResult(result);Document helpers also support lightweight pipeline hooks:
beforeExtractText(text, context)for pre-normalization before extractionafterExtractKeywords(keywords, context)for post-ranking pipeline shaping
Bobbin-compatible adapter
import { extractYakeKeywords } from "@ade_oshineye/yaket";
const keywords = extractYakeKeywords(
"Platform ecosystems reward integration.",
5,
3,
);This preserves Bobbin's current output shape:
type BobbinYakeResult = {
keyword: string;
score: number;
};import { extractKeywordDetails } from "@ade_oshineye/yaket";
const details = extractKeywordDetails("models model models", {
n: 1,
candidateNormalizer: {
normalize(token) {
return token.endsWith("s") ? token.slice(0, -1) : token;
},
},
lemmatizer: {
lemmatize(token) {
return token;
},
},
});Available extension points:
TextProcessor(combinedsplitSentences+tokenizeWordssurface)SentenceSplitter(justsplit(text) => string[], supplied via thesentenceSplitteroption)Tokenizer(justtokenize(text) => string[], supplied via thetokenizeroption)StopwordProviderSimilarityStrategyCandidateNormalizerLemmatizerSingleWordScorerMultiWordScorerKeywordScorercandidateFilter
sentenceSplitter and tokenizer can be supplied independently when you only
want to override one half of the text-processing pipeline. They take precedence
over a textProcessor for the half they cover, so the other half keeps the
bundled (or textProcessor-supplied) behavior.
lemmatizer stays hook-based in Yaket. Upstream-style string backends such as "spacy" or "nltk" are intentionally not implemented in the extraction core.
Yaket also exports:
YakeResultYakeOptions
The two first-class internal scoring hooks are:
singleWordScorerfor replacing the internal YAKE single-word score formulamultiWordScorerfor replacing the internal YAKE multi-word score formula
Example:
import { extractKeywordDetails } from "@ade_oshineye/yaket";
const details = extractKeywordDetails("agent swarms coordinate teams", {
language: "en",
n: 2,
multiWordScorer: {
score(candidate) {
return candidate.size === 2 ? 0.001 : 10;
},
},
});Yaket memoizes similarity scores used by the seqm, levs, and jaro
dedup paths in bounded module-level caches. To isolate cache state — for
long-running edge workers, tests, or per-request caching — create your own
cache and pass it to a KeywordExtractor (or directly to a similarity
helper):
import {
KeywordExtractor,
createSimilarityCache,
sequenceSimilarity,
} from "@ade_oshineye/yaket";
const cache = createSimilarityCache({ maxSize: 5_000 });
const extractor = new KeywordExtractor({
language: "en",
n: 3,
top: 10,
similarityCache: cache,
});
extractor.extractKeywords("Edge runtimes power modern serverless platforms.");
console.log(cache.stats()); // { distance, ratio, sequence, jaro }
cache.clear();
// Direct use is supported for callers wiring custom dedup logic.
sequenceSimilarity("alpha", "alpha", cache);Each SimilarityCache owns four bounded Maps — one per helper:
distance and ratio for Levenshtein, sequence for sequenceSimilarity
(the seqm dedup path), and jaro for jaroSimilarity.
The module-level helpers clearSimilarityCaches() and
getSimilarityCacheStats() continue to operate on the default cache for
backwards compatibility.
import { STOPWORDS, getStopwordText, supportedLanguages } from "@ade_oshineye/yaket";
console.log(supportedLanguages.includes("en"));
console.log(getStopwordText("en").split("\n").length > 0);
console.log(STOPWORDS.en.includes("the"));Language lookup uses the first two letters of the requested language code. If a specific stopword list is unavailable, Yaket currently resolves to an empty stopword list.
To extend or replace stopwords without mutating global state:
import { createStaticStopwordProvider, createStopwordSet } from "@ade_oshineye/yaket";
const stopwords = createStopwordSet("en", { add: ["yaket"], remove: ["the"] });
const provider = createStaticStopwordProvider({
en: stopwords,
pt: ["um", "uma"],
});STOPWORDS is exported as a frozen map of bundled raw stopword text for users who want direct access to the packaged lists.
import { TextHighlighter, extractKeywords } from "@ade_oshineye/yaket";
const keywords = extractKeywords("Machine learning improves software delivery.");
const highlighted = new TextHighlighter().highlight(
"Machine learning improves software delivery.",
keywords,
);yaket --text-input "Google is acquiring Kaggle" --language en --ngram-size 3 --top 5 --verboseSupported flags:
--text-input--input-file--language--ngram-size--dedup-func--dedup-lim--window-size--top--verbose--help
Yaket keeps the extraction core free of runtime filesystem access and Node-only extraction dependencies.
Verification currently includes:
- source guards for extraction modules
- browser-target bundling smoke tests
- a real Cloudflare Workers test lane via
@cloudflare/vitest-pool-workers
Run it with:
npm run test:cloudflareThe repository includes a benchmark harness that compares:
- Yaket
- upstream Python YAKE
- the original Bobbin YAKE-like implementation
- a simple TF-IDF baseline
Current checked-in reports:
docs/benchmarks/komoroske-2026-04-06.mddocs/benchmarks/multilingual.md
Additional dataset-oriented benchmark support is available for Inspec and SemEval-style evaluation via scripts/benchmark-datasets.ts.
npm run benchmark:datasetsA per-language Python-YAKE parity benchmark (English, German, Spanish, French, Italian, Portuguese, Dutch, Russian, Arabic) is available via:
npm run benchmark:multilingualA bundle-size report (worker-target, ESM, gzipped) is available via:
npm run bundle-sizeThis is a Node-only script that uses esbuild and writes
docs/benchmarks/bundle-size.md. The test/bundle-size.test.ts test
asserts the gzipped bundle stays inside the documented edge budget.
Run it with:
npm run benchmark- Architecture overview:
docs/architecture.md - API reference:
docs/api-reference.md - Use cases:
docs/use-cases.md - Algorithm drift:
docs/algorithm-drift.md - Lemmatization evaluation:
docs/lemmatization-evaluation.md - Bobbin / 0.5.x → 0.6 migration:
docs/migration-bobbin-0.6.md - Dataset benchmarks:
docs/benchmarks/inspec-semeval.md - Bobbin integration guide:
docs/integrations/bobbin.md - Generic pipeline guide:
docs/integrations/pipelines.md - Releasing guide:
docs/releasing.md - Contributing:
CONTRIBUTING.md - Roadmap:
docs/roadmap.md - Deferred work:
TODO.md - Audit notes:
docs/audits/implementation-audit-2026-04-16.md
- The tokenizer is close to YAKE, but still not a literal
segtokport. - Dedup
seqmbehavior is still approximate rather than a byte-for-byte Python clone. - Multilingual support covers 34 bundled stopword languages and head-parity locks against upstream Python YAKE 0.7.x for
pt,de,es,it,fr,nl,ru,ar(single-paragraph and multi-document corpora). The remaining drift is bit-level float-precision in the scoring math at exact-tie positions — seedocs/algorithm-drift.mdfor the documented residuals. - Bobbin adapter validation now covers the Bobbin YAKE, topic-extractor, topic-system, and extraction-quality tests in the reference Bobbin checkout, but that validation still needs to be kept current as Bobbin evolves.
| Tool | Strength | Tradeoff vs Yaket |
|---|---|---|
| TF-IDF | simple, cheap, corpus-aware | less phrase-aware and less YAKE-like on single documents |
| RAKE | simple phrase extraction | weaker local-feature scoring and usually cruder ranking |
| KeyBERT | embedding-based semantic relevance | larger dependency/runtime cost and often slower |
| Yaket | deterministic YAKE-style local-feature extraction in JS | still has some drift from upstream Python YAKE in tokenization and some heuristic edge cases |
For a concrete checked-in comparison, see the Komoroske benchmark report.
Yaket is especially well-suited for:
- blog/CMS/knowledge-base tagging
- newsletter and article topic extraction
- search indexing and hybrid retrieval metadata
- RAG chunk enrichment without an LLM call
- browser extensions and client-side page analysis
- chat and Slack bot topic tagging
See docs/use-cases.md for more detail.
An interactive demo page lives in demo/index.html and is intended to be served through GitHub Pages.
GitHub Pages URL:
https://adewale.github.io/yaket/
npm install
npm run typecheck
npm test
npm run test:cli:coverage
npm run test:cloudflare
npm run build
npm run check:package
npm run benchmark
npm run verifytest/python-parity.test.ts performs a live comparison against upstream Python YAKE when PYTHONPATH points at a YAKE checkout. The default path used during local development is /tmp/yake.
Mutation testing is configured via Stryker and can be run separately when you want a slower audit-focused pass:
npm run test:mutation- If you need corpus-wide topic modeling rather than single-document keyword extraction.
- If you need production-grade lemmatization out of the box today.
- If exact upstream Python tokenization parity across all languages is a hard requirement right now.
MIT