Fuzzy Matching: opt.fuzzy and opt.fuzzy_item¶
afwf ships two optional modules for narrowing a list of items by fuzzy
string similarity. They are layered: opt.fuzzy provides a generic
matcher for any Python type; opt.fuzzy_item specialises it for
Item.
Both require the afwf[fuzzy] extra (rapidfuzz >= 3.0.0).
opt.fuzzy — Generic Fuzzy Matching¶
FuzzyMatcher is a generic dataclass over an
arbitrary item type T. It builds an internal name→items map at
construction time and exposes a single match()
method.
Subclassing
To use it you must subclass and implement get_name():
import dataclasses
from afwf.opt.fuzzy.api import FuzzyMatcher
@dataclasses.dataclass
class Bookmark:
title: str
url: str
class BookmarkMatcher(FuzzyMatcher[Bookmark]):
def get_name(self, item: Bookmark) -> str | None:
return item.title # the string that gets fuzzy-matched
bookmarks = [
Bookmark("Alfred App", "https://www.alfredapp.com/"),
Bookmark("Python Docs", "https://docs.python.org/"),
]
matcher = BookmarkMatcher.from_items(bookmarks)
results = matcher.match("alfred", threshold=0)
# → [Bookmark("Alfred App", ...)]
If get_name() returns None for an item, that item is silently excluded
from the match index — useful for conditionally hiding items.
Factory methods
Factory |
When to use |
|---|---|
|
You have a flat list; |
|
You already have a |
match() parameters
Parameter |
Default |
Meaning |
|---|---|---|
|
— |
The search string to match against |
|
|
Minimum similarity score (0–100). Results below this are discarded.
Use |
|
|
Maximum number of results to return |
|
|
Extra callable applied after score filtering. Receives a
|
Results are sorted by score, highest first. If the best match falls below
threshold, an empty list is returned immediately.
Duplicate names
Multiple items can share the same name. They are stored together under one key in the internal mapper and all returned when that name matches.
opt.fuzzy_item — Fuzzy Matching for Alfred Items¶
FuzzyItemMatcher is a ready-made subclass
of FuzzyMatcher for Item objects. It does not require
you to subclass anything — the match name is stored directly on the item.
Item.set_fuzzy_match_name()
The companion Item (a thin subclass of the
core Item) stores the match name in item.variables["fuzzy_match_name"].
Storing it in variables means the name travels with the item through
Alfred’s variable inheritance mechanism:
import afwf.opt.fuzzy_item.api as fuzzy_item
item = fuzzy_item.Item(title="Alfred App", subtitle="https://www.alfredapp.com/")
item.set_fuzzy_match_name("Alfred App")
# item.variables == {"fuzzy_match_name": "Alfred App"}
item.fuzzy_match_name # → "Alfred App" (read-back property)
FuzzyItemMatcher
matcher = fuzzy_item.FuzzyItemMatcher.from_items(items)
matched = matcher.match("alfred", threshold=0)
FuzzyItemMatcher.get_name() simply reads
item.variables.get("fuzzy_match_name"), so items that have not called
set_fuzzy_match_name() are silently excluded from matching.
The Standard Script Filter Pattern¶
Almost every fuzzy Script Filter in afwf.examples follows this pattern:
Build the full item list unconditionally.
If the query is non-empty, run the matcher.
Fall back to the full list when there are no matches — the user always sees something.
@afwf.log_error(
log_file=afwf.path_enum.dir_home.joinpath(
".alfred-afwf/search_bookmarks.log"
), # or just @log_error()
)
def main(query: str) -> afwf.ScriptFilter:
if query.strip() == "error":
raise ValueError("This is a simulated Python error triggered by query='error'")
items = []
for title, url in BOOKMARKS:
item = fuzzy_item.Item(title=title, subtitle=url, arg=url)
item.set_fuzzy_match_name(title)
item.open_url(url)
items.append(item)
if query.strip():
matcher = fuzzy_item.FuzzyItemMatcher.from_items(items)
matched = matcher.match(query, threshold=0)
result_items = matched if matched else items
else:
result_items = items
sf = afwf.ScriptFilter()
sf.items.extend(result_items)
return sf
The fall-back to items on no match (matched if matched else items) is
intentional: an empty result pane is confusing. Showing the full list lets
the user see what is available even when their query does not hit anything.
When to Use opt.fuzzy vs opt.fuzzy_item¶
Use opt.fuzzy_item when your items are already Item
objects destined for Alfred — it requires the least boilerplate.
Use opt.fuzzy (base class) when you are matching over a domain type that
is not an Item — for example, a list of database records or file metadata
objects — and you want to keep the matching logic decoupled from the Alfred
presentation layer.
Installation¶
pip install "afwf[fuzzy]"
# or with uv:
uv add "afwf[fuzzy]"