ruby
51 lines · 9 steps
Building a ranked fuzzy search in Ruby
A small class that scores candidate strings against a query and returns the best matches in ranked order.
Explained by
highlit
1require "set"
2
3class FuzzySearch
4 def initialize(candidates)
5 @candidates = candidates
6 end
7
8 def search(query, limit: 10)
9 normalized = query.downcase.strip
10 return [] if normalized.empty?
11
12 @candidates
13 .map { |item| [item, score(normalized, item.downcase)] }
14 .reject { |_, s| s.nil? }
15 .sort_by { |item, s| [-s, item.length, item.downcase] }
16 .first(limit)
17 .map(&:first)
18 end
19
20 private
21
22 def score(query, target)
23 return 100 if target == query
24 return 80 if target.start_with?(query)
25 return 60 if target.include?(query)
26 return nil unless subsequence?(query, target)
27
28 distance = levenshtein(query, target)
29 max_len = [query.length, target.length].max
30 similarity = 1.0 - (distance.to_f / max_len)
31 (similarity * 50).round
32 end
33
34 def subsequence?(query, target)
35 chars = target.each_char
36 query.each_char.all? { |c| chars.any? { |t| t == c } }
37 end
38
39 def levenshtein(a, b)
40 rows = (0..a.length).to_a
41 b.each_char.with_index(1) do |bc, j|
42 prev = rows[0]
43 rows[0] = j
44 a.each_char.with_index(1) do |ac, i|
45 cost = ac == bc ? 0 : 1
46 prev, rows[i] = rows[i], [rows[i] + 1, rows[i - 1] + 1, prev + cost].min
47 end
48 end
49 rows.last
50 end
51end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Tiered scoring lets exact, prefix, and substring matches outrank looser fuzzy ones.
- 2Sorting by a tuple key breaks ties deterministically instead of leaving ordering to chance.
- 3A rolling single-row Levenshtein keeps edit-distance memory linear in string length.
Related explainers
ruby
module RequestTagging class Middleware def initialize(app) @app = app
Per-request context with CurrentAttributes in Rails
middleware
thread-safety
logging
Intermediate
7 steps
ruby
class Spellchecker ALPHABET = ('a'..'z').to_a.freeze def initialize(dictionary)
Building a Norvig-style spellchecker in Ruby
edit-distance
levenshtein
dynamic-programming
Advanced
10 steps
ruby
class Order < ApplicationRecord belongs_to :customer has_many :line_items has_many :products, through: :line_items
How scopes compose in a Rails model
scopes
activerecord
subqueries
Intermediate
7 steps
ruby
require "charlock_holmes" class TextFileNormalizer DEFAULT_CONFIDENCE = 60
Normalizing text files to clean UTF-8 in Ruby
encoding
text-processing
file-io
Intermediate
8 steps
ruby
require 'date' require 'set' class BusinessDayCalculator
Counting business days in Ruby
dates
sets
ranges
Intermediate
8 steps
ruby
class Document < ApplicationRecord class StaleObjectError < StandardError def initialize(id) super("Document ##{id} was modified by another process")
Optimistic locking with retries in Rails
optimistic-locking
concurrency
transactions
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-ranked-fuzzy-search-in-ruby-explained-ruby-1e74/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.