string comparison
similar string search
string matching
text similarity
algorithm

string comparison with the most similar string

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Finding the most similar string from a set of candidates is a common task in spell checking, search suggestions, deduplication, and fuzzy matching. The standard approach uses string distance metrics — Levenshtein distance, Jaro-Winkler similarity, or cosine similarity on n-grams — to rank candidates by similarity. Python's difflib and the fuzzywuzzy/rapidfuzz libraries make this straightforward.

Method 1: difflib.get_close_matches (Built-in)

Python's standard library includes difflib for sequence matching:

python
1from difflib import get_close_matches, SequenceMatcher
2
3# Find closest matches from a list
4words = ['apple', 'application', 'apply', 'banana', 'grape', 'pineapple']
5
6matches = get_close_matches('aple', words, n=3, cutoff=0.6)
7print(matches)  # ['apple', 'apply', 'maple'] — ranked by similarity
8
9# Get similarity ratio between two strings
10ratio = SequenceMatcher(None, 'apple', 'aple').ratio()
11print(f"Similarity: {ratio:.3f}")  # 0.889

SequenceMatcher uses the Ratcliff/Obershelp algorithm, which finds the longest common subsequence.

Method 2: Levenshtein Distance

Levenshtein distance counts the minimum number of single-character edits (insertions, deletions, substitutions) to transform one string into another:

python
1# Using python-Levenshtein or rapidfuzz
2from rapidfuzz.distance import Levenshtein
3
4# Distance (lower = more similar)
5dist = Levenshtein.distance('kitten', 'sitting')
6print(f"Distance: {dist}")  # 3
7
8# Normalized similarity (0 to 1)
9sim = Levenshtein.normalized_similarity('kitten', 'sitting')
10print(f"Similarity: {sim:.3f}")  # 0.571

Manual Implementation

python
1def levenshtein_distance(s1, s2):
2    m, n = len(s1), len(s2)
3    dp = [[0] * (n + 1) for _ in range(m + 1)]
4
5    for i in range(m + 1):
6        dp[i][0] = i
7    for j in range(n + 1):
8        dp[0][j] = j
9
10    for i in range(1, m + 1):
11        for j in range(1, n + 1):
12            if s1[i-1] == s2[j-1]:
13                dp[i][j] = dp[i-1][j-1]
14            else:
15                dp[i][j] = 1 + min(dp[i-1][j],      # deletion
16                                    dp[i][j-1],      # insertion
17                                    dp[i-1][j-1])    # substitution
18    return dp[m][n]
19
20print(levenshtein_distance('kitten', 'sitting'))  # 3

Method 3: FuzzyWuzzy / RapidFuzz

rapidfuzz (a faster C-based replacement for fuzzywuzzy) provides several fuzzy matching strategies:

python
1from rapidfuzz import fuzz, process
2
3# Simple ratio
4print(fuzz.ratio('New York', 'new york'))           # 82
5print(fuzz.ratio('New York', 'New York City'))       # 82
6
7# Partial ratio (best substring match)
8print(fuzz.partial_ratio('New York', 'New York City'))  # 100
9
10# Token sort (order-independent)
11print(fuzz.token_sort_ratio('John Smith', 'Smith John'))  # 100
12
13# Token set (handles duplicates and subsets)
14print(fuzz.token_set_ratio('Los Angeles CA', 'CA Los Angeles California'))  # 100

Find Best Match from a List

python
1choices = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix']
2
3# Best single match
4best = process.extractOne('new yrok', choices)
5print(best)  # ('New York', 90, 0)  — (match, score, index)
6
7# Top N matches
8top3 = process.extract('chcago', choices, limit=3)
9print(top3)
10# [('Chicago', 91, 2), ('New York', 18, 0), ('Phoenix', 14, 4)]

Method 4: Jaro-Winkler Similarity

Optimized for short strings like names — gives bonus weight to matching prefixes:

python
1from rapidfuzz.distance import JaroWinkler
2
3sim = JaroWinkler.similarity('MARTHA', 'MARHTA')
4print(f"Similarity: {sim:.3f}")  # 0.961
5
6sim2 = JaroWinkler.similarity('DIXON', 'DICKSONX')
7print(f"Similarity: {sim2:.3f}")  # 0.813

Method 5: Cosine Similarity on N-grams

For longer texts, convert strings to n-gram vectors and compute cosine similarity:

python
1from sklearn.feature_extraction.text import TfidfVectorizer
2from sklearn.metrics.pairwise import cosine_similarity
3
4documents = [
5    'machine learning algorithms',
6    'deep learning neural networks',
7    'statistical machine learning',
8    'cooking Italian pasta recipes',
9]
10
11query = 'machine learning techniques'
12
13vectorizer = TfidfVectorizer(analyzer='char_wb', ngram_range=(2, 3))
14tfidf = vectorizer.fit_transform(documents + [query])
15
16# Similarity of query against all documents
17similarities = cosine_similarity(tfidf[-1:], tfidf[:-1]).flatten()
18
19for doc, score in sorted(zip(documents, similarities), key=lambda x: -x[1]):
20    print(f"{score:.3f}: {doc}")
21# 0.812: machine learning algorithms
22# 0.645: statistical machine learning
23# 0.234: deep learning neural networks
24# 0.000: cooking Italian pasta recipes

Comparison of Methods

MethodBest ForCase SensitiveSpeed
SequenceMatcherGeneral purposeYesModerate
LevenshteinShort strings, spell checkYesFast
Jaro-WinklerNames, short stringsYesFast
fuzz.token_sort_ratioReordered wordsNoFast
fuzz.partial_ratioSubstring matchingNoFast
Cosine (TF-IDF)Long texts, documentsConfigurableModerate
python
1from rapidfuzz import process, fuzz
2
3# Product catalog
4products = [
5    'iPhone 15 Pro Max',
6    'Samsung Galaxy S24 Ultra',
7    'Google Pixel 8 Pro',
8    'OnePlus 12',
9    'Sony Xperia 1 V',
10]
11
12def fuzzy_search(query, choices, threshold=60):
13    results = process.extract(query, choices, scorer=fuzz.token_set_ratio, limit=5)
14    return [(match, score) for match, score, _ in results if score >= threshold]
15
16print(fuzzy_search('iphone pro'))
17# [('iPhone 15 Pro Max', 90)]
18
19print(fuzzy_search('galaxy'))
20# [('Samsung Galaxy S24 Ultra', 90)]
21
22print(fuzzy_search('pixel'))
23# [('Google Pixel 8 Pro', 90)]

Common Pitfalls

  • Case sensitivity: Most distance metrics are case-sensitive. 'Apple' and 'apple' have distance 1. Normalize to lowercase before comparing unless case matters.
  • Whitespace and punctuation: 'New York' and 'NewYork' have distance 1, but 'New York' (double space) adds another. Strip and normalize whitespace before comparing.
  • Performance at scale: Computing distances against millions of candidates is O(n * m) per pair. Use indexing techniques (BK-trees, locality-sensitive hashing) or rapidfuzz.process.cdist for bulk comparisons.
  • Choosing the right metric: Levenshtein works for typos; Jaro-Winkler works for names; token-based ratios work for reordered words; cosine similarity works for documents. No single metric is best for all cases.
  • Threshold tuning: A cutoff of 80 works for many applications, but the optimal threshold depends on your data. Too low gives false positives; too high misses valid matches. Evaluate on labeled examples.

Summary

  • Use difflib.get_close_matches for simple built-in fuzzy matching
  • Use rapidfuzz (or fuzzywuzzy) for production fuzzy matching with multiple scoring strategies
  • Use Levenshtein distance for edit-based similarity (typos, misspellings)
  • Use Jaro-Winkler for name matching and short strings
  • Use TF-IDF cosine similarity for document-level text similarity
  • Always normalize case and whitespace before comparing

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.