fuse-swift keeps fuse.js scoring on Apple platforms and Linux
A lightweight fuzzy-search library, with zero dependencies
At a glance
- What is it?
- fuse-swift is a Swift port of the fuse.js fuzzy search library with no dependencies, three ways to declare a searchable key, and a release gate that compares its output against the JavaScript original. The 2.0 line is still at a release candidate, and match offsets land in transform space rather than original text space.
- Who is it for?
- Adopt fuse-swift when you want fuse.js ranking behaviour in a Swift app without reimplementing the bitap scoring, and stay on fuse.js in a web project since the Swift port earns its keep by keeping the same scores in a language that has no equivalent.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 154 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A port whose goal is identical scores, not a new algorithm
fuse-swift is described as the official Swift port of fuse.js, with byte-equivalent results, an idiomatic Swift API, and syncs with each upstream release. The scoring model is not reinvented. The same bitap search, the same thresholds and the same penalties apply, which means a fuzzy search ranked in Swift and one ranked in JavaScript produce the same order for the same input.
That goal decides the audience. If you are building a native client and want a search box that ranks as users expect from a web application, or if you want one ranking implementation shared across a web front end and a native app, the port gives you identical behaviour in one place.
The library has zero dependencies, which matters on Apple platforms where adding a package can mean adding a transitive tree. It targets iOS 15 and newer, macOS 12 and newer, tvOS 15 and newer, watchOS 8 and newer, visionOS 1 and newer, and Linux. It requires Swift 6.0 or newer, and the documentation states that it builds clean under -strict-concurrency=complete -warnings-as-errors.
The upstream lineage is the fuse.js project by the same author, and the repository topics list bitap, fuzzy matching, fuzzy search and weighted search, which is an accurate summary of what sits under the hood.
FuseOptions exposes the bitap scoring knobs with documented defaults
The search behaviour is controlled by a FuseOptions value whose defaults are documented, and the documentation gives each option a default and a note. A few of them are worth reading before you tune anything.
isCaseSensitive defaults to false, in which case pattern and text are JS-equivalent lowercased before the bitap scan. ignoreDiacritics defaults to false; when enabled, both pattern and text run through a literal port of fuse.js's three-step diacritic strip, described as NFD plus a scalar range filter plus a NON_DECOMPOSABLE_MAP.
The scoring controls are threshold at 0.6, which is the maximum per-text bitap score allowed for isMatch to be true, where 0 requires an exact substring and 1 matches anything, and distance at 100, the penalty applied per character away from the expected location, which is 0 by default. ignoreLocation drops the position penalty entirely, and ignoreFieldNorm drops the field length normalisation, which the documentation explains is what makes long and short fields rank by raw bitap score.
Two options affect how results are produced rather than how they are scored. findAllMatches defaults to false, meaning the bitap scan stops covering the full text length once a match is found. minMatchCharLength defaults to 1 and sets the minimum run length emitted in FuseMatch.indices, which matters when you highlight matches and short runs would look noisy.
Three ways to point at a field, and why the choice is yours
For keyed object search, each FuseKey names where the searchable text comes from, and the library offers three accessor forms, which is the part of the API that is more flexible than a typical Swift port would be.
The keyPath form is a type-safe Swift KeyPath into a stored property. It is compile-time checked and does not require the element type to be Encodable. The path form takes a single dotted string or an array of segments and is walked at runtime against the encoded JSON of the element, which requires Element: Encodable or a custom getFn. The closure form is a @Sendable function taking the element and returning a String, an optional String, or an array of Strings, and it exists for fields that need computation or that come from non-Encodable types.
Each key carries a weight, and the weight rules are strict: all initialisers throw on weight <= 0, so every call site needs try. That is a deliberate design choice, since a zero or negative weight is a configuration mistake that would silently distort ranking, and the compiler enforces the handling rather than letting it slide.
Because the weight of each field decides how much it contributes, the weighted example in the documentation searches a title at 0.7 and a first name at 0.3, and a query of stve still lands on The Lock Artist.
Match offsets arrive in transform space, not original space
This is the sharpest edge in the library and the documentation is unusually explicit about it.
With includeMatches enabled, each FuseMatch.indices carries a list of FuseRange values holding start and end UTF-16 offsets that cover the matched runs. The pattern is rendering-agnostic: slice the original string by the ranges and wrap the matched segments, whether the output is ANSI, HTML mark, AttributedString or something else.
The catch is where those offsets live. They are UTF-16 offsets in the transformed text, meaning after case folding and diacritic stripping. For inputs whose length is preserved by the configured transforms, the offsets map one to one back to the original. The documentation names the cases where that holds: ASCII text under the defaults, or any text with isCaseSensitive set to true and ignoreDiacritics set to false.
Where the transforms change length, the mapping breaks. The documented examples are the Turkish dotted capital I when lowercased, NFD decomposed accents with ignoreDiacritics enabled, and the German capital sharp S expanded to ss during diacritic stripping. Those inputs need a transform-aware helper that recomputes offsets in original space. A highlight function written for the simple case will quietly mark the wrong characters for those users, which is a bug that only shows up in one language or one font, and it is worth handling explicitly rather than hoping the audience is ASCII.
First use: a string array, then keyed objects
The smallest example searches an array of strings. FuseOptions is created with includeScore enabled, the search is constructed over the collection, and results come back with a reference index, the item and a score:
import Fuse
let fruits = ["apple", "orange", "banana", "pear", "grape", "kiwi", "mango", "plum"]
let options = try FuseOptions<String>(includeScore: true)
let fuse = try Fuse.Search<String>(fruits, options: options)
for r in fuse.search("ange", limit: 3) {
print(r.refIndex, r.item, r.score!)
}The documented output for that query puts orange first with a score of 0.02, then mango at 0.26, then banana at 0.51, which is the kind of ordering you would expect for a typo tolerant search.
The keyed version adds the model types and the weights:
struct Book: Codable, Sendable {
let title: String
let author: Author
}
struct Author: Codable, Sendable {
let firstName: String
let lastName: String
}let options = try FuseOptions<Book>(
includeMatches: true,
includeScore: true,
keys: [
try FuseKey<Book>("title", keyPath: \Book.title, weight: 0.7),
try FuseKey<Book>(path: "author.firstName", weight: 0.3),
]
)
let fuse = try Fuse.Search<Book>(books, options: options)
let results = fuse.search("stve")
print(results[0].item.title)Adding the package is a single dependency line in Package.swift:
// Package.swift
dependencies: [
.package(url: "https://github.com/krisk/fuse-swift.git", from: "2.0.0"),
]One detail to check before you wire this in: the documented version requirement is 2.0.0 while the only published release listed for the project is the 2.0.0-rc.1 candidate, and the documentation says promotion to 2.0.0 follows a short feedback window. Verify how your package manager resolves that range before you depend on it.
The release gate compares Swift output against fuse.js
The repository ships a Makefile that doubles as a release harness, and its structure explains how the project argues for byte-equivalent results.
The per-PR matrix lives in CI and runs the build and test commands directly, while the Makefile targets are convenience wrappers plus the cross-runtime parity check that gates a release. The three commands you will use most are short:
swift build
swift test
swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errorsThe parity target runs a script that checks the Swift results against a sibling fuse-js checkout, and the help text names the environment variable that points at it, FUSE_JS_PATH, overriding the default path to ../fuse-js/dist/fuse.mjs. The release target is strict-test plus parity, and the Makefile notes that the parity output is checked in alongside the tag.
The status section in the documentation gives the numbers behind this. The v1 scope is feature-complete and has 253 unit tests, plus a cross-runtime parity check against fuse-js 7.4.0-beta.5 covering 25 query cases and 138 result records, all matching within a score tolerance of 1e-9.
Two caveats. The parity check needs Node and a sibling checkout of the JavaScript library, so it is not something you can run from a lone clone of the Swift repository. And the tolerance is a score comparison, not a proof that every API matches.
On licensing and upkeep, the project is under Apache-2.0. The only listed release is 2.0.0-rc.1 dated 2026-05-17, and the last push was on 2026-05-22, so at the time of writing the 2.0 line has been at release candidate status since May and the repository has seen no commit since.
Editorial conclusion
Adopt fuse-swift when you want fuse.js ranking behaviour in a Swift app without reimplementing the bitap scoring, and stay on fuse.js in a web project since the Swift port earns its keep by keeping the same scores in a language that has no equivalent. Verify first that the version you depend on is one you can accept, since the only published tag is 2.0.0-rc.1 dated 2026-05-17 and the last push was on 2026-05-22, then decide how you will handle transform space offsets if you highlight matches in text that changes length under diacritic stripping.
Frequently asked questions
How do I add fuse-swift to a Swift project?
Add the package dependency in Package.swift with .package(url: "https://github.com/krisk/fuse-swift.git", from: "2.0.0"). The library requires Swift 6.0 or newer and supports iOS 15+, macOS 12+, tvOS 15+, watchOS 8+, visionOS 1+ and Linux, with no dependencies.
Does fuse-swift give the same results as fuse.js?
The project describes itself as the official Swift port of fuse.js with byte-equivalent results, and the release gate runs a cross-runtime parity check against fuse-js 7.4.0-beta.5 covering 25 query cases and 138 result records within a 1e-9 score tolerance. The parity check needs Node and a sibling checkout of the JavaScript library.
How do I search objects with weighted fields in fuse-swift?
Create FuseOptions with a keys array of FuseKey values, each carrying a weight above zero, then build a Fuse.Search over your collection. FuseKey has three accessor forms: a type-safe KeyPath, a runtime dotted path that requires Encodable, and a @Sendable closure for computed or non-Encodable fields.
Why are my highlighted match ranges offset in the wrong place?
The indices are UTF-16 offsets in the transformed text after case folding and diacritic stripping, so they only map one to one back to the original when the transforms preserve length. Text whose length changes, such as the Turkish dotted capital I or the German capital sharp S expanded to ss, needs a transform-aware helper.
Is fuse-swift ready for production use?
The v1 scope is described as feature-complete with 253 unit tests, and the project says 2.0.0-rc.1 is the first release candidate, with promotion to 2.0.0 following a short feedback window. The only published release is 2.0.0-rc.1 dated 2026-05-17.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/krisk-fuse-swift)Was this useful?