const CONSECUTIVE_BONUS: f64 = 8.0;
const WORD_START_BONUS: f64 = 10.0;
const MATCH_BASE: f64 = 4.0;
const LEADING_PENALTY: f64 = 0.5;
#[derive(Debug, Clone, Copy)]
pub struct Match {
pub index: usize,
pub score: f64,
}
fn is_boundary(prev: Option<char>) -> bool {
match prev {
None => true,
Some(c) => c == ' ' || c == '_' || c == '-' || c == '/' || c == '.',
}
}
pub fn fuzzy_score(candidate: &str, query: &str) -> Option<f64> {
if query.is_empty() {
return Some(0.0);
}
let q: Vec<char> = query.to_ascii_lowercase().chars().collect();
let mut qi = 0usize;
let mut score = 0.0f64;
let mut prev_matched = false;
let mut prev_char: Option<char> = None;
let mut matched_yet = false;
for ch in candidate.chars() {
let lower = ch.to_ascii_lowercase();
if qi < q.len() && lower == q[qi] {
score += MATCH_BASE;
if prev_matched {
score += CONSECUTIVE_BONUS;
}
if is_boundary(prev_char) {
score += WORD_START_BONUS;
}
qi += 1;
prev_matched = true;
matched_yet = true;
} else {
if !matched_yet {
score -= LEADING_PENALTY;
}
prev_matched = false;
}
prev_char = Some(ch);
}
if qi == q.len() {
Some(score)
} else {
None
}
}
use crate::matcher::{fuzzy_score, Match};
use std::cmp::Ordering;
pub fn rank(candidates: &[&str], query: &str) -> Vec<Match> {
let mut matches: Vec<Match> = candidates
.iter()
.enumerate()
.filter_map(|(index, candidate)| {
fuzzy_score(candidate, query).map(|score| Match { index, score })
})
.collect();
matches.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(Ordering::Equal)
.then_with(|| a.index.cmp(&b.index))
});
matches
}
pub fn top_n<'a>(candidates: &[&'a str], query: &str, n: usize) -> Vec<(&'a str, f64)> {
rank(candidates, query)
.into_iter()
.take(n)
.map(|m| (candidates[m.index], m.score))
.collect()
}
mod matcher;
mod ranker;
fn main() {
let candidates = [
"src/main.rs",
"src/matcher.rs",
"src/ranker.rs",
"tests/matcher_tests.rs",
"Cargo.toml",
"README.md",
"src/util/string_helpers.rs",
];
let query = "mtch";
println!("query: {:?}\n", query);
for (candidate, score) in ranker::top_n(&candidates, query, 5) {
println!("{:>6.1} {}", score, candidate);
}
}
This snippet implements a small fuzzy search ranker of the kind used to power command palettes, file finders, and autocomplete boxes. The core idea is subsequence matching: a query matches a candidate if every character of the query appears in the candidate in order, not necessarily contiguously. Beyond a boolean match, the ranker computes a numeric score so results can be sorted by relevance, which is what makes fuzzy finders feel responsive.
In matcher.rs, fuzzy_score walks the candidate once, advancing a cursor through the query on each matching character. Matching is case-insensitive via to_ascii_lowercase, but the original casing is preserved for display. The scoring model rewards the qualities users intuitively expect: a CONSECUTIVE_BONUS for adjacent matches so contiguous runs beat scattered ones, a WORD_START_BONUS for characters that begin a word (start of string or after a separator, tracked by is_boundary), and a LEADING_PENALTY that gently discounts unmatched characters before the first hit so prefix matches float to the top. If the query is exhausted the function returns Some(score); if the candidate ends first it returns None, cleanly separating the match/no-match decision from ranking.
The Match struct in matcher.rs bundles the candidate index with its score, keeping the ranker independent of how candidates are stored. The empty-query case returns a neutral score so an unfiltered list still renders.
In ranker.rs, rank maps candidates through fuzzy_score, keeps only the Some results with filter_map, then sorts. The sort uses partial_cmp because scores are f64, and deliberately sorts by descending score first, breaking ties by ascending index so equally-scored items keep their original stable order. This tie-break matters: without it, results shuffle unpredictably between keystrokes, which feels broken to users. partial_cmp returns Option because floats can be NaN; falling back to Ordering::Equal avoids a panic even though this scoring never produces NaN.
The main.rs tab shows the pieces working together against a fixed candidate list, printing scored, sorted results for a sample query. The design keeps the hot path allocation-light and the scoring weights are plain constants, so tuning ranking behavior is a matter of adjusting a few numbers rather than restructuring the algorithm.
Related snips
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
use anyhow::{Context, Result};
use std::fs;
fn load_config(path: &str) -> Result<String> {
fs::read_to_string(path)
.with_context(|| format!("failed to read config from {}", path))
anyhow::Context for adding error context without custom types
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
Debounced search input (React)
-- Create table with tsvector column
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
content TEXT,
author VARCHAR(100),
Full-text search with PostgreSQL and tsvector
use std::process::Command;
fn main() -> std::io::Result<()> {
let output = Command::new("ls")
.arg("-la")
.output()?;
std::process::Command for spawning external processes
Share this code
Here's the card — post it anywhere.