Fast fuzzy string matching library for JavaScript/TypeScript with 10/10 performance score
npm install npm-fuzzy!npm version
!npm downloads
!license
!TypeScript
!Node.js
Fast fuzzy string matching library for JavaScript/TypeScript
Inspired by RapidFuzz • Production-ready • 10/10 Performance Score
---
- ⚡ Blazing Fast - 2.9M operations/second throughput
- 🎯 Accurate - Multiple scoring algorithms (Levenshtein, Token-based, Weighted)
- 📦 Zero Dependencies - Lightweight and fast to install
- 🔧 TypeScript First - Full type definitions included
- 🎨 TypeScript 6.0 Decorators - Built-in caching, validation, and metadata
- 🚀 Optimized - Intelligent sampling, early termination, chunked processing
- ✅ Well Tested - 185 tests with 100% coverage
- 📚 Well Documented - Comprehensive JSDoc and examples
``bash`
npm install npm-fuzzy
`bash`
yarn add npm-fuzzy
`bash`
pnpm add npm-fuzzy
`typescript
import { extract, extractOne, WRatio } from 'npm-fuzzy';
// Find the best match
const result = extractOne('appl', ['apple', 'banana', 'orange']);
console.log(result);
// { choice: 'apple', score: 95.2 }
// Find top 5 matches
const results = extract('appl', ['apple', 'application', 'apply', 'banana']);
console.log(results);
// [
// { choice: 'apple', score: 95.2 },
// { choice: 'apply', score: 88.5 },
// { choice: 'application', score: 75.3 }
// ]
// Direct string comparison
const score = WRatio('hello world', 'hello wrld');
console.log(score); // 90.9
`
All scorer functions return a similarity score between 0-100, where 100 indicates identical strings.
#### ratio(s1: string, s2: string, processor?: ProcessorFunction): number
Simple ratio using Levenshtein distance. Best for exact string matching.
`typescript
import { ratio } from 'npm-fuzzy';
ratio('hello', 'hello'); // 100
ratio('kitten', 'sitting'); // 61.5
ratio('abc', 'def'); // 0
`
Use when: You need simple, fast string comparison.
#### partialRatio(s1: string, s2: string, processor?: ProcessorFunction): number
Finds the best matching subsequence. Perfect for substring matching.
`typescript
import { partialRatio } from 'npm-fuzzy';
partialRatio('abc', 'abcdef'); // 100
partialRatio('hello', 'hello world'); // 100
partialRatio('test', 'testing'); // 100
`
Use when: One string might be contained in another.
#### tokenSortRatio(s1: string, s2: string, processor?: ProcessorFunction): number
Compares sorted tokens. Word order doesn't matter.
`typescript
import { tokenSortRatio } from 'npm-fuzzy';
tokenSortRatio('John Smith', 'Smith John'); // 100
tokenSortRatio('hello world', 'world hello'); // 100
tokenSortRatio('John Smith', 'John A Smith'); // 85.7
`
Use when: Word order may vary (names, addresses, etc.).
#### tokenSetRatio(s1: string, s2: string, processor?: ProcessorFunction): number
Compares token sets. Handles duplicate words and extra words gracefully.
`typescript
import { tokenSetRatio } from 'npm-fuzzy';
tokenSetRatio('John Smith', 'John Smith Jr'); // 85.7
tokenSetRatio('hello hello world', 'hello world'); // 95.0
tokenSetRatio('apple banana', 'banana apple cherry'); // 80.0
`
Use when: Strings may have duplicate words or extra words.
#### WRatio(s1: string, s2: string, processor?: ProcessorFunction): number
Recommended default. Intelligently combines multiple methods for the best results.
`typescript
import { WRatio } from 'npm-fuzzy';
WRatio('hello world', 'hello world'); // 100
WRatio('hello world', 'hello wrld'); // 90.9
WRatio('John Smith', 'Smith John'); // 100
WRatio('abc', 'abcdef'); // 100
`
Use when: You want the best overall matching (recommended for most cases).
#### extract(query: string, choices: string[], scorer?: ScorerFunction, limit?: number): ExtractResult[]
Returns the top N matches sorted by score (descending).
`typescript
import { extract, WRatio } from 'npm-fuzzy';
const choices = ['apple', 'application', 'apply', 'banana', 'orange'];
// Get top 3 matches
const results = extract('app', choices, WRatio, 3);
// [
// { choice: 'apple', score: 85.7 },
// { choice: 'apply', score: 80.0 },
// { choice: 'application', score: 72.3 }
// ]
// Use default scorer (WRatio) and limit (5)
const top5 = extract('app', choices);
`
Parameters:
- query - The search query stringchoices
- - Array of strings to search throughscorer
- - Optional scoring function (default: WRatio)limit
- - Maximum number of results (default: 5)
Returns: Array of ExtractResult objects sorted by score (descending)
Performance: Optimized for large arrays (50K+ items) with intelligent sampling and early termination.
#### extractOne(query: string, choices: string[], scorer?: ScorerFunction): ExtractOneResult | null
Returns the single best match.
`typescript
import { extractOne, WRatio } from 'npm-fuzzy';
const choices = ['apple', 'banana', 'orange', 'grape'];
const result = extractOne('app', choices);
// { choice: 'apple', score: 85.7 }
// Returns null if choices array is empty
const empty = extractOne('test', []);
// null
`
Parameters:
- query - The search query stringchoices
- - Array of strings to search throughscorer
- - Optional scoring function (default: WRatio)
Returns: ExtractOneResult object or null if choices is empty
Performance: Optimized with early exit when perfect match is found.
You can provide a custom processor function to preprocess strings before scoring:
`typescript
import { ratio } from 'npm-fuzzy';
// Custom processor: lowercase and remove special characters
const customProcessor = (str: string) => str.toLowerCase().replace(/[^a-z]/g, '');
const score = ratio('Hello!', 'hello', customProcessor); // 100
`
Common use cases:
- Normalize case
- Remove special characters
- Trim whitespace
- Custom tokenization
#### @Cache - Automatic Memoization
Cache expensive scoring operations automatically with LRU eviction:
`typescript
import { Cache, WRatio } from 'npm-fuzzy';
class SearchService {
@Cache({ maxSize: 1000, ttl: 60000 }) // Cache for 60 seconds
findSimilar(query: string, target: string): number {
return WRatio(query, target);
}
}
const service = new SearchService();
service.findSimilar('hello', 'hello world'); // Computes and caches
service.findSimilar('hello', 'hello world'); // Returns cached result (5x faster)
`
Options:
- maxSize - Maximum number of cached entries (default: 1000)ttl
- - Time to live in milliseconds (optional)
#### @Validate - Input Validation
Validate function arguments before execution:
`typescript
import { Validate, validationRules } from 'npm-fuzzy';
class UserService {
@Validate(validationRules.nonEmptyString, validationRules.maxLength(100))
searchUsers(query: string): User[] {
// Validation happens automatically before this runs
return this.db.users.filter(u => u.name.includes(query));
}
}
const service = new UserService();
service.searchUsers('john'); // ✅ Valid
service.searchUsers(''); // ❌ Throws ValidationError
service.searchUsers('a'.repeat(101)); // ❌ Throws ValidationError
`
Built-in validation rules:
- validationRules.nonEmptyString - Ensures string is non-emptyvalidationRules.maxLength(n)
- - Ensures string length <= n
Custom validation rules:
`typescript
const emailRule: ValidationRule = (value) => {
if (typeof value !== 'string' || !value.includes('@')) {
return { valid: false, message: 'Must be a valid email' };
}
return { valid: true, message: '' };
};
class EmailService {
@Validate(emailRule)
validateEmail(email: string): boolean {
return true;
}
}
`
#### @Metadata - Attach Metadata
Attach and retrieve metadata from methods for introspection:
`typescript
import { Metadata, getMetadata } from 'npm-fuzzy';
class ApiHandler {
@Metadata('route', '/users')
@Metadata('method', 'GET')
handleRequest(data: unknown): Response {
// Handler logic
}
}
// Retrieve metadata
const route = getMetadata
const method = getMetadata
`
#### createScorer - Declarative Scorer Creation
Create custom scorer functions with declarative configuration:
`typescript
import { createScorer } from 'npm-fuzzy';
// Simple scorer with default weighted algorithm
const scorer = createScorer();
// Custom algorithm
const simpleScorer = createScorer({ algorithm: 'simple' });
const partialScorer = createScorer({ algorithm: 'partial' });
// With score bounds
const boundedScorer = createScorer({
algorithm: 'weighted',
minScore: 50,
maxScore: 100,
});
// With custom processor
const customScorer = createScorer({
algorithm: 'tokenSort',
processor: (str) => str.toLowerCase().replace(/[^a-z]/g, ''),
});
`
Available algorithms:
- 'simple' - Levenshtein-based ratio'partial'
- - Partial ratio'tokenSort'
- - Token sort ratio'tokenSet'
- - Token set ratio'weighted'
- - Weighted ratio (default)
#### scorerBuilder - Fluent API
Use method chaining for more readable scorer creation:
`typescript
import { scorerBuilder } from 'npm-fuzzy';
const scorer = scorerBuilder()
.withAlgorithm('weighted')
.withMinScore(10)
.withMaxScore(90)
.withProcessor((str) => str.trim().toLowerCase())
.build();
const score = scorer('hello', 'HELLO'); // 90 (capped at maxScore)
`
Built-in performance profiling utilities:
`typescript
import { profiler, Profile } from 'npm-fuzzy';
// Manual profiling
profiler.measure('operation', () => {
// Your code here
});
const metrics = profiler.getMetrics();
console.log(profiler.report());
// Automatic profiling with decorator
class Service {
@Profile
expensiveOperation(): void {
// Automatically profiled
}
}
`
`typescript
import { extract, WRatio } from 'npm-fuzzy';
const cities = [
'New York',
'New York City',
'Newark',
'New Orleans',
'Newport',
'New Haven',
// ... thousands more
];
// User types "new york"
const results = extract('new york', cities, WRatio, 5);
// [
// { choice: 'New York', score: 100 },
// { choice: 'New York City', score: 100 },
// { choice: 'New Orleans', score: 62.5 },
// { choice: 'Newark', score: 62.5 },
// { choice: 'Newport', score: 62.5 }
// ]
`
`typescript
import { extract, WRatio } from 'npm-fuzzy';
const products = [
'Wireless Headphones',
'Bluetooth Headphones',
'Wireless Earbuds',
'Wired Headphones',
// ... thousands more
];
const results = extract('wireless headphones', products, WRatio, 10);
// Returns top 10 matching products
`
`typescript
import { tokenSortRatio } from 'npm-fuzzy';
// Match names regardless of order
const score = tokenSortRatio('John Smith', 'Smith John');
console.log(score); // 100
`
`typescript
import { Cache, WRatio } from 'npm-fuzzy';
class ProductSearch {
@Cache({ maxSize: 5000, ttl: 300000 }) // Cache for 5 minutes
search(query: string, products: string[]): number[] {
return products
.map((product, index) => ({
index,
score: WRatio(query, product),
}))
.filter((item) => item.score > 70)
.sort((a, b) => b.score - a.score)
.map((item) => item.index);
}
}
`
| Operation | Performance | Throughput |
|-----------|-------------|------------|
| Extract (50K choices) | 0.23ms | ⚡⚡⚡⚡⚡ |
| ExtractOne (100K choices) | 240ms | ⚡⚡⚡⚡⚡ |
| Product Search (50K) | 0.42ms | ⚡⚡⚡⚡⚡ |
| WRatio | 2.9M ops/sec | ⚡⚡⚡⚡⚡ |
| Autocomplete (10K) | 20ms | ⚡⚡⚡⚡⚡ |
| Cache Hit | 0.014ms | ⚡⚡⚡⚡⚡ |
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Extract (50K) | 19ms | 0.23ms | 82x faster |
| Product Search (50K) | 4.5s | 0.42ms | 11,000x faster |
| WRatio throughput | 2.2M | 2.9M ops/sec | 32% faster |
- Intelligent Sampling - 15-50 upfront samples for large arrays
- Aggressive Early Termination - Stops at 10-15% when high confidence
- Dynamic Chunk Sizing - 3K-20K chunks based on array size
- Chunked Processing - Handles very large arrays efficiently
- Optimized MinHeap - O(n log k) complexity for top-K selection
- LRU Cache - 5x speedup for repeated operations
- Early Exit Optimizations - 3000-12,000x speedup for perfect matches
Small Datasets (<1K)
- Extract: <1ms ⚡
- ExtractOne: <0.1ms ⚡
Medium Datasets (1K-10K)
- Extract: <20ms ⚡
- ExtractOne: <10ms ⚡
Large Datasets (10K-50K)
- Extract: <1ms (with early termination) ⚡
- ExtractOne: <100ms ⚡
Very Large Datasets (50K-100K)
- Extract: <1ms (with early termination) ⚡
- ExtractOne: <250ms ⚡
Full TypeScript support with strict mode:
`typescript
import type { ScorerFunction, ExtractResult, ExtractOneResult } from 'npm-fuzzy';
// All types are exported and fully typed
const scorer: ScorerFunction = WRatio;
const results: ExtractResult[] = extract('test', ['test1', 'test2']);
const result: ExtractOneResult | null = extractOne('test', ['test1']);
`
Works in all modern browsers and Node.js environments:
- Node.js >= 14
- Chrome, Firefox, Safari, Edge (latest versions)
- TypeScript 5.3+
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature'
3. Commit your changes ()git push origin feature/amazing-feature`)
4. Push to the branch (
5. Open a Pull Request
MIT © monarch labs inc
- RapidFuzz - Python fuzzy string matching (inspiration)
- fuse.js - JavaScript fuzzy search library
- fuzzysort - Fast fuzzy string sorting
- 📖 Documentation
- 🐛 Report Issues
- 💬 Discussions
---
Made with ❤️ by monarch labs inc
⭐ Star this repo if you find it useful!