C#
fuzzy matching
text processing
programming
sentence comparison

Fuzzy text sentences/titles matching in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Fuzzy text matching in C# finds strings that are similar but not identical — handling typos, abbreviations, and word reordering. The main approaches are edit-distance algorithms (Levenshtein), token-based comparison (splitting into words and comparing sets), and libraries like FuzzySharp and FuzzyString. For most use cases, FuzzySharp provides the best balance of accuracy and ease of use, while Levenshtein distance gives you full control when you need a custom implementation.

Levenshtein Distance

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

csharp
1public static int LevenshteinDistance(string source, string target)
2{
3    if (string.IsNullOrEmpty(source)) return target?.Length ?? 0;
4    if (string.IsNullOrEmpty(target)) return source.Length;
5
6    int[,] matrix = new int[source.Length + 1, target.Length + 1];
7
8    for (int i = 0; i <= source.Length; i++) matrix[i, 0] = i;
9    for (int j = 0; j <= target.Length; j++) matrix[0, j] = j;
10
11    for (int i = 1; i <= source.Length; i++)
12    {
13        for (int j = 1; j <= target.Length; j++)
14        {
15            int cost = source[i - 1] == target[j - 1] ? 0 : 1;
16            matrix[i, j] = Math.Min(
17                Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1),
18                matrix[i - 1, j - 1] + cost
19            );
20        }
21    }
22
23    return matrix[source.Length, target.Length];
24}
25
26// Convert to a 0-1 similarity ratio
27public static double Similarity(string a, string b)
28{
29    int distance = LevenshteinDistance(a, b);
30    int maxLen = Math.Max(a.Length, b.Length);
31    return maxLen == 0 ? 1.0 : 1.0 - (double)distance / maxLen;
32}
33
34// Usage
35Console.WriteLine(LevenshteinDistance("kitten", "sitting")); // 3
36Console.WriteLine(Similarity("kitten", "sitting"));          // 0.571
37Console.WriteLine(Similarity("hello world", "hello wrold")); // 0.909

FuzzySharp is a C# port of Python's fuzzywuzzy library and provides multiple matching strategies:

bash
dotnet add package FuzzySharp
csharp
1using FuzzySharp;
2
3// Simple ratio — character-by-character comparison
4int score = Fuzz.Ratio("Introduction to Algorithms", "Introduction to Algorithm");
5Console.WriteLine(score); // 96
6
7// Partial ratio — best substring match
8score = Fuzz.PartialRatio("Algorithms", "Introduction to Algorithms");
9Console.WriteLine(score); // 100
10
11// Token sort — ignores word order
12score = Fuzz.TokenSortRatio("World Hello", "Hello World");
13Console.WriteLine(score); // 100
14
15// Token set — handles extra/missing words
16score = Fuzz.TokenSetRatio(
17    "Introduction to Algorithms 4th Edition",
18    "Algorithms Introduction"
19);
20Console.WriteLine(score); // 100

Matching Against a List

csharp
1using FuzzySharp;
2using FuzzySharp.SimilarityRatio;
3using FuzzySharp.SimilarityRatio.Scorer.StrategySensitive;
4
5string[] titles = {
6    "Design Patterns",
7    "Clean Code",
8    "Introduction to Algorithms",
9    "The Pragmatic Programmer",
10    "Refactoring"
11};
12
13string query = "intro algorithms";
14
15// Find best match
16var bestMatch = Process.ExtractOne(query, titles);
17Console.WriteLine($"Best: {bestMatch.Value} (Score: {bestMatch.Score})");
18// Best: Introduction to Algorithms (Score: 90)
19
20// Find top N matches above a threshold
21var matches = Process.ExtractTop(query, titles, limit: 3, cutoff: 60);
22foreach (var match in matches)
23{
24    Console.WriteLine($"{match.Value}: {match.Score}");
25}

Jaro-Winkler Distance

Jaro-Winkler works well for short strings like names and titles. It gives higher scores to strings that share a common prefix:

csharp
1public static double JaroWinkler(string s1, string s2, double prefixScale = 0.1)
2{
3    if (s1 == s2) return 1.0;
4
5    int maxDist = Math.Max(s1.Length, s2.Length) / 2 - 1;
6    if (maxDist < 0) maxDist = 0;
7
8    bool[] s1Matches = new bool[s1.Length];
9    bool[] s2Matches = new bool[s2.Length];
10    int matches = 0, transpositions = 0;
11
12    for (int i = 0; i < s1.Length; i++)
13    {
14        int start = Math.Max(0, i - maxDist);
15        int end = Math.Min(i + maxDist + 1, s2.Length);
16        for (int j = start; j < end; j++)
17        {
18            if (s2Matches[j] || s1[i] != s2[j]) continue;
19            s1Matches[i] = s2Matches[j] = true;
20            matches++;
21            break;
22        }
23    }
24
25    if (matches == 0) return 0.0;
26
27    int k = 0;
28    for (int i = 0; i < s1.Length; i++)
29    {
30        if (!s1Matches[i]) continue;
31        while (!s2Matches[k]) k++;
32        if (s1[i] != s2[k]) transpositions++;
33        k++;
34    }
35
36    double jaro = ((double)matches / s1.Length + (double)matches / s2.Length +
37        (matches - transpositions / 2.0) / matches) / 3.0;
38
39    // Winkler prefix bonus
40    int prefix = 0;
41    for (int i = 0; i < Math.Min(4, Math.Min(s1.Length, s2.Length)); i++)
42    {
43        if (s1[i] == s2[i]) prefix++;
44        else break;
45    }
46
47    return jaro + prefix * prefixScale * (1 - jaro);
48}
49
50Console.WriteLine(JaroWinkler("MARTHA", "MARHTA")); // 0.961
51Console.WriteLine(JaroWinkler("John Smith", "Jon Smith")); // 0.963

Token-Based Matching

For sentences and titles, splitting into tokens and comparing word sets handles word reordering and extra words:

csharp
1public static double TokenOverlap(string a, string b)
2{
3    var tokensA = a.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries).ToHashSet();
4    var tokensB = b.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries).ToHashSet();
5
6    int intersection = tokensA.Intersect(tokensB).Count();
7    int union = tokensA.Union(tokensB).Count();
8
9    return union == 0 ? 0 : (double)intersection / union;  // Jaccard similarity
10}
11
12Console.WriteLine(TokenOverlap("Clean Code by Robert Martin", "Robert Martin Clean Code"));
13// 0.8 (4 shared words / 5 unique words)

Preprocessing for Better Results

Normalize text before comparing to reduce noise:

csharp
1public static string Normalize(string input)
2{
3    if (string.IsNullOrWhiteSpace(input)) return "";
4
5    // Lowercase, remove punctuation, collapse whitespace
6    var cleaned = new string(input.ToLower()
7        .Where(c => char.IsLetterOrDigit(c) || c == ' ')
8        .ToArray());
9
10    return string.Join(" ", cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries));
11}
12
13string a = Normalize("Introduction to Algorithms, 4th Ed.");  // "introduction to algorithms 4th ed"
14string b = Normalize("INTRODUCTION TO ALGORITHMS (4TH ED)");  // "introduction to algorithms 4th ed"
15Console.WriteLine(Fuzz.Ratio(a, b)); // 100

Common Pitfalls

  • Not normalizing case and punctuation before comparison: "Hello World" vs "hello world" gives a low Levenshtein similarity even though the words are identical. Always normalize to lowercase and strip punctuation before fuzzy matching.
  • Using simple ratio for sentences with different word order: Fuzz.Ratio("World Hello", "Hello World") returns a low score because it compares character by character. Use Fuzz.TokenSortRatio or Fuzz.TokenSetRatio for sentences where word order may differ.
  • O(n*m) complexity for large-scale matching: Levenshtein distance is O(n*m) per pair. Comparing every string against every other in a large dataset is O(k^2 * n * m). Use indexing strategies (n-gram indexes, locality-sensitive hashing) to prefilter candidates before running expensive comparisons.
  • Choosing the wrong threshold: A threshold of 80 works well for short titles but may be too strict for longer sentences. Test with your specific dataset and adjust. FuzzySharp's Process.ExtractTop with a cutoff parameter helps filter low-quality matches.
  • Ignoring Unicode and diacritics: "cafe" vs "café" may score poorly. Use string.Normalize(NormalizationForm.FormD) to decompose accented characters before comparison, or use a library that handles Unicode normalization.

Summary

  • Levenshtein distance measures character-level edit distance — good for short strings and typo detection
  • FuzzySharp provides Ratio, PartialRatio, TokenSortRatio, and TokenSetRatio for different matching scenarios
  • Jaro-Winkler works well for names and titles (prefix-weighted)
  • Token overlap (Jaccard similarity) handles word reordering in sentences
  • Always normalize text (lowercase, strip punctuation) before comparing
  • Use Process.ExtractTop from FuzzySharp to find the best matches from a collection

Course illustration
Course illustration

All Rights Reserved.