<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Why My Levenshtein Search Was Slow (and How BK-Trees Fixed It]]></title><description><![CDATA[Why My Levenshtein Search Was Slow (and How BK-Trees Fixed It]]></description><link>https://shramanbanerjee.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/67829f35b361b82f46d93e00/dea51668-1ca6-4aca-8af7-279d5ab21e26.png</url><title>Why My Levenshtein Search Was Slow (and How BK-Trees Fixed It</title><link>https://shramanbanerjee.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 00:30:56 GMT</lastBuildDate><atom:link href="https://shramanbanerjee.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[🚀 I Built Fuzzy Search Wrong — Until I Discovered BK-Trees]]></title><description><![CDATA[In my recent project ZENITH — a distributed search engine with semantic capabilities — I wanted to build a feature we all see in Google:

“Did you mean … ?”

It felt like magic.
I used Levenshtein dis]]></description><link>https://shramanbanerjee.hashnode.dev/i-built-fuzzy-search-wrong-until-i-discovered-bk-trees</link><guid isPermaLink="true">https://shramanbanerjee.hashnode.dev/i-built-fuzzy-search-wrong-until-i-discovered-bk-trees</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Databases]]></category><category><![CDATA[System Design]]></category><category><![CDATA[golang]]></category><category><![CDATA[data structures]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[S Banerjee]]></dc:creator><pubDate>Thu, 19 Mar 2026 23:27:33 GMT</pubDate><content:encoded><![CDATA[<p>In my recent project <strong>ZENITH</strong> — a distributed search engine with semantic capabilities — I wanted to build a feature we all see in Google:</p>
<blockquote>
<p><em>“Did you mean … ?”</em></p>
</blockquote>
<p>It felt like magic.</p>
<p>I used <strong>Levenshtein</strong> distance to calculate the number of edits—insertions, deletions, or substitutions—required to transform one word into another. On my laptop, with a small dataset, it was blazing fast.</p>
<p>I thought I was a genius.</p>
<hr />
<h2>😬 Then Reality Hit</h2>
<p>I pushed it to a real dataset: <strong>100,000+ words</strong>.</p>
<p>Suddenly, the magic disappeared.</p>
<p>Every time a user typed a query, the server had to compute the distance against <em>every single word</em>.</p>
<p>It was like trying to find one specific person in a massive cricket stadium by checking every seat one by one.</p>
<ul>
<li><p>CPU usage spiked</p>
</li>
<li><p>Latency increased</p>
</li>
<li><p>Users saw a loading spinner</p>
</li>
</ul>
<p>What worked in isolation completely broke at scale.</p>
<hr />
<h2>❌ The Naive Approach</h2>
<p>My approach was simple:</p>
<ul>
<li><p>Store all words in a list (or map)</p>
</li>
<li><p>For every query:</p>
<ul>
<li><p>Compute <strong>Levenshtein</strong> distance with each word</p>
</li>
<li><p>Return closest matches</p>
</li>
</ul>
</li>
</ul>
<p>Time complexity:</p>
<blockquote>
<p><strong>O(N × L²)</strong> Where:</p>
</blockquote>
<ul>
<li><p>N = number of words</p>
</li>
<li><p>L = average word length</p>
</li>
</ul>
<p>This is fine for 1,000 words.</p>
<p>Not for 100,000+.</p>
<hr />
<h2>🧠 Quick Refresher: What is Levenshtein Distance?</h2>
<p>If you’ve solved this LeetCode problem, you already know it:</p>
<p>👉 <a href="https://leetcode.com/problems/edit-distance/">https://leetcode.com/problems/edit-distance/</a></p>
<p>The idea is simple:</p>
<ul>
<li><p>Build a matrix of size <code>(M+1) × (N+1)</code></p>
</li>
<li><p>Each cell represents the minimum edits needed</p>
</li>
<li><p>At each step, choose the minimum of:</p>
<ul>
<li><p>Insertion</p>
</li>
<li><p>Deletion</p>
</li>
<li><p>Substitution</p>
</li>
</ul>
</li>
</ul>
<p>Example: Transform <code>"KITTEN"</code> → <code>"SITTING"</code></p>
<hr />
<h2>⚙️ Go Implementation (Optimized)</h2>
<pre><code class="language-go">package analysis

const MAX_DISTANCE = 3

func min(a, b, c int) int {
	if a &lt; b &amp;&amp; a &lt; c {
		return a
	}
	if b &lt; c {
		return b
	}
	return c
}

func Levenshtein(s1, s2 string) (int, bool) {
	if len(s1) &gt; len(s2) {
		s1, s2 = s2, s1
	}

	n, m := len(s1), len(s2)

	prevRow := make([]int, n+1)
	currRow := make([]int, n+1)

	for i := 0; i &lt;= n; i++ {
		prevRow[i] = i
	}

	for j := 1; j &lt;= m; j++ {
		currRow[0] = j

		minInRow := currRow[0]

		for i := 1; i &lt;= n; i++ {
			cost := 1
			if s1[i-1] == s2[j-1] {
				cost = 0
			}

			currRow[i] = min(
				prevRow[i]+1,     // deletion
				currRow[i-1]+1,   // insertion
				prevRow[i-1]+cost, // substitution
			)

			if currRow[i] &lt; minInRow {
				minInRow = currRow[i]
			}
		}

		// Early exit optimization
		if minInRow &gt; MAX_DISTANCE {
			return 0, false
		}

		copy(prevRow, currRow)
	}

	return prevRow[n], true
}
</code></pre>
<hr />
<h2>⚠️ Why This Still Fails</h2>
<p>Even with optimizations:</p>
<ul>
<li><p>You still compute distance for <strong>every word</strong></p>
</li>
<li><p>The system is fundamentally <strong>O(N)</strong> per query</p>
</li>
</ul>
<p>That’s the real bottleneck.</p>
<hr />
<h2>💡 The Key Insight: Distance is a Metric</h2>
<p>Here’s the breakthrough moment.</p>
<p><strong>Levenshtein</strong> distance follows the <strong>triangle inequality</strong>:</p>
<blockquote>
<p>If distance(A, B) = X and distance(B, C) = Y then distance(A, C) ≤ X + Y</p>
</blockquote>
<p>This means:</p>
<p>👉 You can <strong>avoid computing distances</strong> for many words 👉 If you structure your data correctly</p>
<hr />
<h2>🌳 Enter BK-Trees (Burkhard-Keller Trees)</h2>
<p>A BK-tree organizes words based on <strong>distance</strong>, not order.</p>
<h3>Structure:</h3>
<ul>
<li><p>Each node = a word</p>
</li>
<li><p>Each edge = a distance</p>
</li>
<li><p>Children are grouped by distance from parent</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/67829f35b361b82f46d93e00/167c0c7c-5238-4956-987e-15f89c8623ba.png" alt="" style="display:block;margin:0 auto" />

<p>Example:</p>
<ul>
<li><p>Root = <code>"HELP"</code></p>
</li>
<li><p><code>"HELL"</code> → distance 1 → child at edge 1</p>
</li>
<li><p><code>"HELLO"</code> → distance 2 → child at edge 2</p>
</li>
</ul>
<hr />
<h2>🔍 How Search Works</h2>
<p>Let:</p>
<ul>
<li><p>Query = <code>q</code></p>
</li>
<li><p>Max allowed distance = <code>k</code></p>
</li>
</ul>
<h3>Step 1: Compare with current node</h3>
<p>Compute:</p>
<pre><code class="language-plaintext">d = distance(q, node.word)
</code></pre>
<ul>
<li>If <code>d ≤ k</code> → add to results</li>
</ul>
<hr />
<h3>Step 2: Prune aggressively</h3>
<p>Instead of exploring all children:</p>
<p>👉 Only visit children with edge values in:</p>
<pre><code class="language-plaintext">[d - k, d + k]
</code></pre>
<hr />
<h3>🔥 Why This Works</h3>
<p>Triangle inequality guarantees:</p>
<blockquote>
<p>Any node outside this range <strong>cannot</strong> be within distance <code>k</code></p>
</blockquote>
<p>So entire subtrees are skipped.</p>
<hr />
<h2>📈 Real Impact</h2>
<p>Example (approximate):</p>
<pre><code class="language-plaintext">Dataset: 100,000 words

Naive scan: ~120 ms/query  
BK-tree:    ~5–10 ms/query
</code></pre>
<p>Instead of checking 100,000 words, you might only check:</p>
<p>👉 <strong>50–200 nodes</strong></p>
<p>That’s the difference between:</p>
<ul>
<li><p>❌ unusable system</p>
</li>
<li><p>✅ production-ready feature</p>
</li>
</ul>
<hr />
<h2>🧠 When Should You Use BK-Trees?</h2>
<ul>
<li><p>Spell checkers</p>
</li>
<li><p>Search autocomplete</p>
</li>
<li><p>Fuzzy matching systems</p>
</li>
<li><p>Query correction engines</p>
</li>
</ul>
<hr />
<h2>🚧 Limitations</h2>
<p>BK-trees are powerful, but not perfect:</p>
<ul>
<li><p>Performance depends on data distribution</p>
</li>
<li><p>Worst-case can still degrade to O(N)</p>
</li>
<li><p>Not ideal for high-dimensional data</p>
</li>
</ul>
<hr />
<h2>🎯 Final Thoughts</h2>
<p>The biggest lesson wasn’t BK-trees.</p>
<p>It was this:</p>
<blockquote>
<p><strong>A good algorithm is not enough. You need the right data structure to scale it.</strong></p>
</blockquote>
<p>That shift—from solving problems to designing systems—completely changed how I think about engineering. For the code implementation of this check out this project <a href="https://www.github.com/shramanb13/ZENITH"><strong>ZENITH — A Distributed Search Engine with Semantic Capabilities</strong></a></p>
<hr />
<h2>🚀 What’s Next</h2>
<p>This is part of my larger project:</p>
<p>👉 <a href="https://www.github.com/shramanb13/ZENITH"><strong>ZENITH — A Distributed Search Engine with Semantic Capabilities</strong></a></p>
<p>I’ll be sharing more system design deep dives soon.</p>
<p>If you found this useful:</p>
<ul>
<li><p>⭐ Star the project</p>
</li>
<li><p>Follow for more system design content</p>
</li>
</ul>
<hr />
<h2>🔗 Connect with Me</h2>
<ul>
<li><p><a href="https://x.com/SB434223">X (Twitter)</a></p>
</li>
<li><p><a href="https://www.linkedin.com/in/shraman-banerjee-385200303/">LinkedIn</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>