I have a long text (about 6,000,000 chars) and a short text (about 6,000 chars). The long text contains the short text, but not exactly - a small number of words are missing, abbreviated or just erroneous. So I want to do an approximate search.
There are several Python libraries that do fuzzy string matching. For example, FuzzySet accepts a list of candidates and a string and returns the candidate that is most similar to the string. However, here I do not have a list of candidates - I have just a very long text.
A naive solution is to take, as the list of candidates, all substrings of the long text. However, the number of candidates will be huge. So, now I use make several heuristic assumptions:
- I assume that the first several characters at the start and end of the short text are not erroneous in the long text.
- I assume that the length of the best match of the short text inside the long text is at least half and at most twice the length of the short text.
These assumptions allow me to reduce the number of candidates dramatically, so all of them can be checked in a reasonable time. Here is the current solution:
import re
from fuzzyset import FuzzySet
def fuzzyFind(needle:str, haystack:str, needleStart:str, needleEnd:str)->str:
"""
needle: short text to look for.
haystack: long text to look in.
needleStart: first few chars in needle,
needleEnd : last few chars in needle - assumed to exist verbatim in haystack.
returns a subset of haystack that is a best match for needle.
"""
minPossibleLength = len(needle)//2
maxPossibleLength = len(needle)*2
possibleStarts = [m.start() for m in re.finditer(needleStart, haystack)]
possibleEnds = [m.end() for m in re.finditer(needleEnd, haystack)]
possibleMatches = FuzzySet()
for iStart in possibleStarts:
for iEnd in possibleEnds:
possibleLength = iEnd-iStart
if minPossibleLength <= possibleLength and possibleLength <= maxPossibleLength:
possibleMatch = haystack[iStart:iEnd]
print("possible match from {} to {}".format(iStart,iEnd))
possibleMatches.add(possibleMatch)
matches = possibleMatches.get(needle)
bestMatch = matches[0]
bestMatchText = bestMatch[1]
return bestMatchText
This heuristic solution is quick and quite accurate in practice, but of course it is not guaranteed to work. I would like to get rid of the assumptions, especically assumption #1. Is there a quick solution that does not need to assume anything about the first and last characters of the needle?
Note: before writing this function I tried to use the fuzzysearch library. However, this function requires an upper bound on the Levenshtein distance between the short text and its variant inside long text. Even with modest upper bounds (e.g, 60-70) it becomes quite slow.
haystack
with the same length asneedle
? Additionally, for example, ifhaystack = 'nee_dle'
andneedle = 'needle'
, would you expect'nee_dle'
to be the result of the matching function? \$\endgroup\$