Levenshtein Distance
en.wikipedia.org
en.wikipedia.org
I believe that the first widely available approximate regex matcher was Tre (https://laurikari.net/tre/). However, while everybody wants to talk about approximate matching like they have something to say, most algorithmists act like they forgot about Tre.
https://en.wikipedia.org/wiki/Agrep says "A more recent agrep is the command-line tool provided with the TRE regular expression library. TRE agrep is more powerful than Wu-Manber agrep since it allows weights and total costs to be assigned separately to individual groups in the pattern."
If you need to run Levenshtein Distance over tens of thousands of strings, perhaps as part of a fuzzy search, and you only need to match on distances of 0 or 1, not any distance in general, then there's an optimization on Levenshtein Distance where you hard code the function just for distances of 0 or 1. It's not difficult to do and the payoff might enable new applications of what would otherwise be too costly.
So like, to index a string "hello", you'd add all of {"hello":"hello","ello":"hello","hllo":"hello","helo":"hello","hell":"hello"} into your table, and for the query "gello", you'd look up all of ["gello", "ello", "gllo", "gelo", "gell"], and get a match on "ello"->"hello", then do a Levenshtein calculation dist("gello","hello") to confirm it's within ED=1 (it is), and be done. (Bonus: the same method works with Damerau-Levenshtein distance as well.)
For example if the input query is "ello" and there is a dict.txt with millions of entries, it should output the words that are closest to the query: "hell", "hello", "fellowship", "mellow" and their respective matching scores (e.g 0.8, 0.9, 0.3, 0.7 respectively). The scores are just random numbers in this case.
(edited for clarity)
But your advice still applies! Run time scales with max distance.
[1] https://en.wikipedia.org/wiki/Bitap_algorithm [2] https://github.com/heyimalex/bitap
There is SymSpell, agrep, fuzzywuzzy, closestmatch, fuzzysearch, fuzzyset, SimString, ukkonen and others in Github.
The problem is I don't have a clue which one to use for each case, which are the cases and what are the differences between the libraries.
A writeup illustrating several cases with a recommendation of a library and a practical example for each one would be very helpful.
Btw, if anyone has experience with this type of stuff I would be interested in talking to you (mail is in profile).
For example, someone might have mistyped "berry" as "barry" but you don't want to match it against "terry" (both have a levenshtein distance of 1)
Also, my favorite library for approx matching, which is heavy duty in some cases, is https://github.com/Martinsos/edlib
Which has python bindings and Nim binding among others.
Most of these libraries focus on calculating distance metrics between pairs of strings, or finding the nearest match to a string among many candidates.
On the other hand, fuzzysearch is used for finding near matches to a string within much longer strings or texts. Common use cases are fuzzy searches in DNA sequences and long texts such as books and articles.
We tracked code "moves", whitespace changes, and "trivial" changes via Levenshtein distance--which isn't great for capturing most simple renamings--and then marked them up separately in an "augmented diff".
We briefly looked into doing AST edit-distance, but that turned out to be infeasible for many reasons.
Over-all it helped the prof grade faster, at least in the degenerate cases.
Sure, in the extremes it will give sensible results (someone wrote only 3 lines, or wrote 90% of the code base).
But minor, questionably motivated refactorings can create huge commits, while someone may come up with some great design while walking around for hours and sketching on paper, which all ultimately boils down to a short function.
Dumb metrics like this cannot replace actually talking with the students, asking questions about their choices and alternatives, having them do presentations or lab notebooks/logbooks etc.
As said in the parent:
>> Over-all it helped the prof grade faster, at least in the degenerate cases.
https://www.gnu.org/software/diffutils/manual/html_node/Whit...
So now I ask HN: is there any "good" way to do this short of some kind of answer like "find a database that natively offers levenshtein distance search"? Are there actually sorting algorithms for sorting by levenshtein distance (other than just using L as a sort function in a traditional sort function)? Are there search algorithms that work well for searching by L distance?
1) write a UDF—in C, or for SQL Server, in pretty much any managed language—that returned the Levenshtein distance between a passed param and a column name, with a “max distance” param to aid in optimizing (e.g., maybe it doesn’t bother calculating the distance if the length difference exceeds the max Levenshtein distance)
Then you could write a query that looks something like:
SELECT product, description WHERE levenshtein(`fobar`, product, 2) <= 2
-or-2) if you only care about a distance of 1 or 2, create a set of secondary tables where you’d pre-calculate the permutations of every word, with a FK pointing back to the PK of the actual row.
Alternately, a combination of a Levenshtein distance function and an RDBMS that already supports fuzzy text matching seems (to me) to be ideal.
I would argue your answer is the best as as far as doing an actual Levenshtein distance calculation.
That being said, sort orders are implemented algorithmically by the query engine. ORDER BY in SQL Server, as I recall implements up to 7 different algorithms built in, depending on the case, as far as I recall. But those algorithms are optimized for the engine and have access to the internals.
I can think of two interesting ways to look at this, one being how one could use an open source database to extend the built in sort algorithms in the query engine, perhaps controlled by collation settings, to get a native levenshtein sort implemented that has access to internals. Collations defining the rules for sort order, but this would probably be within a column, not against a value (although if you’re extending the query engine, why not). Example of collation settings [1] and how they control the rules of sort order - perhaps a SET command to define levenshtein order and then a native extension to the Postgres query engine, and now you can sort a column by Levenshtein distances.
The other thing I was thinking was word vectors (but this is similar to Levenshtein distance) and still in a relational database requires something special. Maybe some of the full text search implementations in database could do this out of the box.
[1] https://docs.microsoft.com/en-us/sql/relational-databases/co...
What I've seen databases that need to do fuzzy matching on strings do more often is index a list of substrings of keys and match on those. So hypothetically if I had a row identified with the string 'abc', I would have a separate table with the strings: 'a', 'b', 'c', 'ab', 'bc', 'abc' all mapped to the row in question and an index built on them.
If you had a cap on the edit distance you knew was acceptable for searches, you could do something similar with levenshtein distance, where you could save every string with an edit distance less than say, 3, to a separate table along with the edit distance. This would be expensive in terms of space though, if your keys were very large. I suppose you could optimize it by just pre-computing the distance on a prefix (say the first three or four characters) and using that to narrow the search space.
Instead enumerate all unique permutations of the search term, calculate it's levensten distance and then search through for a match...doing it one by one starting from distance 0 to max and returning the first match.
If the variable in complexity size is the number of rows then it's better to enumerate all permutations and do a single search over all rows, ordering results by distance and returning only the first match
I know of no real world implementation of this, however.
[1] https://en.wikipedia.org/wiki/Metric_(mathematics) [2] http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK...
Let A be the alphabet used. Precompute the "letter sum" of each string and store it. Then given two strings with letter sums c1 and c2, they cannot have Levenshtein distance k if |c1-c2| > k|A|. This could allow you to skip a lot of rows.
Think "atc" and "cat". Same sum.
The idea is that any edit operation on a string will at most change the letter sum by |A|.
Damerau-Levenshtein [1] is more practical then just Levenshtein as it includes transposition - a common cause of typos.
[1] https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_di...
Slightly off-topic, but one of the algorithms I'm most proud of coming up with as an engineer was an offshoot of Levenshtein distance where I had to find all pairs of strings in a set of ~1m strings which were fairly similar. 1m x 1m x O(n^2) is very slow. There turned out be a good way to figure out a lower bound on distances using some precomputation, and that eliminated almost all of the pairwise O(n^2) Levenshtein computations.
I'm anyone's interested, here the description: https://www.quora.com/What-is-the-best-programming-algorithm...
> "At Google, say we had 1 million plaintext passwords..."
EDIT: Found the reference I used to implement this.
https://books.google.com/books?id=_1rSBQAAQBAJ&pg=PA85&lpg=P...
https://github.com/cbartley/diffy/blob/master/src/diffy/diff...
I wrote a commented version years back that might be a handy reference; it's since been deprecated because we moved it out of StringUtils, but the original code is here https://github.com/apache/commons-lang/blob/master/src/main/...
That's pretty cool, especially the doubling scheme. I'm using a modified form of Levenshtein Distance for comparing lines when diffing files, and that's pretty expensive since code files that are thousands of lines long are not uncommon. Since you are usually comparing one file to another version of itself, the differences are often small though, so an incremental approach would really pay off.
https://en.wikipedia.org/wiki/Taxicab_geometry
https://en.wikipedia.org/wiki/Metric_(mathematics)
Also, this looks like could be related to the Hamming Code ? Error correcting codes.
1. Always >= 0 (what is a negative character edit?)
2. It satisfies identiy in that if no edits are required to make them equal, metric distance is 0 and they are equal (applies to either ordering of strings A->B and B->A)
3. It is symmetric (run the edits backwards to get B->A instead of A->B)
4. It satisfies triangle inequality. exercise left to reader, but intuitive since the edit distance is always the "shortest path" through character changes between two strings A and B, and deviating to visit an intermediate string C would not decrease number of edits from A->C->B vs original path A->B
I'm sleep deprived due to "offspring induced insomnia" so take this with a grain of salt.
EDIT: By 'seemingly trivial', I mean that if you explain it to a lay person it seems easy at first blush - not that it is actually trivial.
[0]: https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance
I'm that case, a long word like "helicopter" will be placed at a greater distance than "elipsis" despite being what I want "heli" to actually match, so I just added a "startsWith" check to override such issues.
https://en.wikipedia.org/wiki/N-gram#n-grams_for_approximate...
Did some more digging to see if there was a unique comparison word that would enable me to sort any group of strings but found that it was impossible. Still a fun afternoon of learning.
When I was trying to match batches of OCR'd strings to known strings, I knew some algorithm like this had to exist, but I could not find it. My searches kept turning up Jaccard similarity and Hamming distance again and again.
(Once I found it and began working on an implementation, I started noticing my compiler would find miss-typed variable names and suggest the correctly-spelled variable for me.)
Another good one is Manhattan distance (or better yet, city block distance) in place of Hamming distance. It gives a nice mental picture of the number of road segments you’d need to travel in a city to get from point A to point B.
Is it anything like this (typeahead example)? https://rawgit.com/jeancroy/FuzzySearch/master/demo/autocomp...
What is the name for this kind of approximate string matching?
I'm just spit-balling here... but when a natural metric induces the discrete topology then it's probably more useful to look at connectivity graphs induced by the metric. For instance the graph on all strings with two strings connected by an edge if their edit distance equals 1. Restrict to words in a dictionary and you have a basic spelling suggestion algorithm.
Edit: the analyst in me really wants a translation-invariant abelian group structure ... then we can do Fourier analysis...
Yep.
From the practical side, there are a lot of algorithms designed to work on vectors that can work on sets of strings. It depends on whether the algorithm really needs to use the underlying vector, or whether it just uses distances.
For example: Clustering! By using string metrics, you can group sets of strings into clusters using standard algorithms. Some algorithms like K-means won't work (in general, you can't find the string half-way in-between two strings), but some algorithms can apply, especially hierarchical clustering.
I think you could apply MDS to sets of strings the same way, but I'm a little less familiar with the guts of that algorithm.
So I had a routine that normalized the address, as best I could anyway, didn't help that it was all just one varchar field. Then implimented Levenshtein Distance, messed with the weighting a bit to fit our particular data and away it went. Saved a bunch of headaches. It wasn't perfect, but it was better than hand matching a couple thousand accounts.
Before a thing is called 'Levenshtein Distance' it will be used by someone saying 'calculating distance between these two strings the way Levenshtein did in [0]'. Eventually, when enough people do this, or someone gets particularly bold, the method will instead be given its name (occasionally, the original author will be so bold, but that seems rare). If I were looking to procrastinate more than I already am I would go find some examples of this, examples that unfortunately don't spring immediately to mind though I'm sure I remember their shadows.
As to why they are not named for what they represent - it's only after the thing has been invented, tested, and named that we realise that it is the correct or best way to do a thing. Often there are multiple ways of doing things (this is Levenshteins edit distance) and so they are named to distinguish them from each other. The survivor is not renamed just because the others are forgotten.
[0] not a real reference
This distance metric is exceptionally useful when picking the states to map to in line codes with error correction/detection.
> Given two strings a and b on an alphabet Σ (e.g. the set of ASCII characters, the set of bytes [0..255], etc.), the edit distance d(a, b) is the minimum-weight series of edit operations that transforms a into b. One of the simplest sets of edit operations is that defined by Levenshtein in 1966 Insertion of a single symbol. If a = uv, then inserting the symbol x produces uxv. This can also be denoted ε→x, using ε to denote the empty string. Deletion of a single symbol changes uxv to uv (x→ε). Substitution of a single symbol x for a symbol y ≠ x changes uxv to uyv (x→y).
...
> Other variants of edit distance are obtained by restricting the set of operations. Longest common subsequence (LCS) distance is edit distance with insertion and deletion as the only two edit operations, both at unit cost. Similarly, by only allowing substitutions (again at unit cost), Hamming distance is obtained; this must be restricted to equal-length strings.
https://github.com/BurntSushi/ripgrep/issues/1053The boundary file address records include, among other things:
address range start
address range end
odd/even/both flag
directional prefix
street name
street suffix
directional postfix
zip code
The address range specifies the range of numerical addresses that the record applies to, and the odd/even/both flag indicates if it is for just the odd address, just the even addresses, or both.The direction prefix and suffix are supposed to be one of {blank, N, S, E, W, NE, NW, SE, SW}, but some states botch that and I've seen EA, EB, NO, SO, and WE end up in there.
The suffix field is for things like LANE, AVE, ST, and the like. It is a 4 character field, so a street that residents thing of as ELM AVENUE would be name=ELM suffix=AVE in the boundary file. Or it would be name="ELM AVENUE" with no suffix. (There are even some with AVENUE in the name and something else in the suffix, such as ROOSEVELT AVENUE ALY in Albany GA. The ALY is the suffix). (There are a 318 distinct suffixes across all the SST states, for the curious [3]).
Anyway, my point is that the data is kind of loose.
I want to be able to take what a user puts on an address form:
address
city
state
zip code
and find a good match in the boundary files. Most people manage to put something reasonable in the city, state, and zip code fields, and manage to put their street address at the start of the address field, so I can pretty reliably get the zip code and the street number. It's the rest of the address line that is problematic.At first I was trying to figure out what in there might be prefix, postfix, and suffix to isolate just the street name. But because of both the aforementioned looseness of the boundary data, and differences between how the user might think of their address and how their state thinks of it, this didn't work too well.
I was considering keeping an index that maps words that occur in street names to those street names, and then looking for the longest word in the user supplied address and looking up all streets in the user's zip code that contain that word and that include the user's street number. If that returned more than one match, the idea was then to look at the prefix, postfix, and suffix and see how well those matched with things in the user address. Looking at a few addresses that had given me trouble, though, it looked like this plan to resolve them would still be tricky and unreliable.
So then I decided to try a Levenshtein distance approach. Given zip, street number, street, where street is whatever the user included in the "address" field after the street number, I pull up all records in my boundary DB that match on zip code and street number. I don't even include street name in the query.
Then for each result, I take the prefix, name, suffix, and postfix and join them separated by space. I then simply take the one with the smallest Levenshtein distance from what the user supplied. With this, I no longer care if something is name=ELM, suffix=AVE or name="ELM AVE", suffix="".
This has worked remarkably well. Even if the user throws on stuff that doesn't belong (some people manage to put the city, state, and/or zip in the address box) it usually doesn't throw it off. It raises the Levenshtein distance of the correct match by sometimes a lot...but it also raises the incorrect match distances too, so the correct one still wins.
The boundary files also have secondary address fields, for things like apartments, unit, suites, and the like. These are an abbreviation (APT, e.g.) and low and high ends of a range, and an odd/even/both flag. Unfortunately the ranges for these aren't always numeric. E.g., there is 695 E MORRILL AVE in Columbus, OH, which has secondary fields of "AP", "A", "M", and the odd/even/both flag set to both. I assume that means Apartments A-M.
So far, I've ignored the secondary address fields. Every time I've seen more than one result for the lowest Levenshtein distance, the matches have been close enough to each other to be in the same taxing districts, so would have the same rate and the tax would be allocated to the same entities, so further resolution is not necessary.
At some point I'll take my scripts that clean up the raw rate and boundary files and transform them into a form more suitable for DB use, and that make sqlite and mysql databases from those, and that do the address matching and also sales tax lookups by address, and put 'em on Github, but that will probably not be very soon.
[1] https://www.streamlinedsalestax.org/Shared-Pages/rate-and-bo...