BKTree for similar string search
recursion, complexity, probabilitistic argument, algorithmic algorithm, experimentation on million level dataset

Search for a command to run...
recursion, complexity, probabilitistic argument, algorithmic algorithm, experimentation on million level dataset

Hello everyone,
If you're having issues withdrawing, swapping, or selling ETHM (EthereumMeta), LUSD, Thoreum V3, 4WMM, BTCBR, or any other tokens, we can help.
Many projects experience migration errors, liquidity loss, or contract issues that lock funds. Whether it's meme coins, DeFi tokens, or abandoned projects, our team specializes in recovering inaccessible assets.
For secure assistance, contact us at: email📩rexasfinance99@gmail.com
Vibe Video, thoughts of video production in agentic world

demo with sample codes

Inspirations from old days

GCP Conversational Agents and CloudRun

B-K Tree is a data structure for efficient similarity search. It provides logarithmic insertion and search operations, if the distance metric follows triangle inequality, e.g. hamming (for bit arrays), Levenshtein (edit distance for strings), Euclidean distances (say between word embeddigns).
The idea is a very neat recursive process.
build([nodes...]). Given a list of nodes, choose any one to be the root. It then buckets the remaining nodes based on their distances to the root. For each distance, and its corresponding nodes, choose any one to be the child of root and apply the same build() process recursively.insert(root, node). It starts from the root, obtain the distance between root and node, and locates the substree, whose root is child. If the subtree (child) is empty, node is inserted as child of root. Otherwise, recursively apply insert(child, node).search(root, target, tolerance). tolerance is the max distance difference accepted between target and any node. When tolerance=0 , the search is an exact match. The key idea is a constraint scope tranversal. The process maintains an open set of candidates (usually implemented as queue or stack). It starts from the root, obtain the distance d between root and node, and add all children whose distances are in the range [d - tolerance, d + tolerance] to the candidates (important!). It pops the candidates and recursively add their children in the same way. During the tree tranversal, we note the nodes whose distance to target is within tolerance.We use hamming distance as an example, to articulate the complexity is logarithmic in a probabilistic way.
Hamming distance essentially measures the number of different bits between two bit arrays.
Suppose a given target is at a very deep leaf node. The path from root to this target has a distance sequence of (d1, d2, d3, ... dL), where L is the depth (aka "level") of final hop.
No matter what is the dx, there is 1/ 2^m probabiliy that distance between our target and the particular child has a distance of dx, where m is size of each element (aka number of bits). The joint probability that a path tranverses distances in exact order of (d1, d2, d3, ... dL), is simply (1/ 2^m) ^ (L-1). This is the probability that one random target follows a path of depth-L. For n nodes in the tree, the expectation of # of nodes that follow a depth-L or more path is (1/ 2^m) ^ (L-1) * n.
We solve (1/ 2^m) ^ (L-1) * n >= 1 for L, and gets L <= 1/m log(n) + 1. That means, in order to have at least one node that has depth L, L needs to be smaller than a logarithmic function of n, which is the problem size.
The proof is not rigorous but to give some intuition of the BK tree's core design. The key observation is that, when L increases, the chance that we can see a leaf node decays exponentially.
Once we show that the tree is of logarithmic depth, and the search range at each layer is a bounded constant irrelevant of n, we can conclude that both the insert() and search() are of logarithmic complexity.
The key for BK tree's efficiency roots from the constraint scope search by selection children whose distances are in the range [d - tolerance, d + tolerance] .
Let's see why the nodes outside this range can be safely excluded. Consider a node c of current root p, which has distance greater than d+tolerance. We denote target node as t. Then we have below relations:
distance(c, p) > d+tolerance (via search constraints)distance(t, p) = d (via BK Tree definition, aka build() algorithm)distance(c, t) + distance(t, p) >= distance(c, p) (triangle inequality of distance)Rearranging the last one, we can get distance(c, t) >= distance(c, p) - distance(t, p) = distance(c, p) - d > d + tolerance - d = tolerance.
Since distance(c, t) > tolerance, we can safely exclude the node and the subtree (all subtree nodes have the same distance as c, by the definition).
We use pybktree in this experiment.
The dataset is a search query and keyword matching dataset.
The distance is given by dist_levenshtein, which is a python only DP-implementation of Levenshtein distances.
We set tolerance to 0 for exact match.
By varying the number of query and keyword, we have the below results:
The implementation works Ok on a single Macbook, at the size of 100K. When the problem size approaches millions level, it takes two hours to execute the matching phase.
Comments:
{}, which incurrs large overhead. We will show some statistics later that the majority nodes has a breadth of 0, aka no children. We can change the implementation to save memory.We fix query=10000 and keyword=10000 in this experiment, and change the tolerance.
When the tolerance increases, the constraint search scope increases by 2x the tolerance. This is exponential in depth L. BK Tree only works well when the tolerance is small. In our experiment, even with 3-character edit distane tolerance, matching 10,000 keywords already takes minutes.
The last number in above list shows the average candidates for one search. The denominator is 10,000. We can see that less than 5% of the queries were searched with a tolerance=3. The search space pruning is still effective.
In order to better understand the tree's shape, we take the statistics for each node:
We define two types of order of insert() , random and sequential. Sequential is a string sort of the input texts.

We can see that:
We can further comprehend the experiment results in the last chapter using the statistics here. Suppose a depth of 8, and a tolerance tot. The number of all candidates for a given search() task is (2 * tot) ^ 8. This number is irrelevant of problem size, but increases dramatically with respect to the tolerance level.