Show HN: uFuzzy.js – A tiny, efficient fuzzy search that doesn't suck (github.com)
I became frustrated with the unpredictible/poor match quality and opaqueness of "relevance scores" in existing fuzzy and fulltext search libs, so I tried something different and this is the result. The main selling point is the result quality / ordering, with best-in-class memory overhead and excellent performance being bonuses. The API is pretty stable at this point, but looking for feedback before committing to 1.0.
TL;DR
The test corpus is a 4MB json file with 162k words/phrases, so give it a second for initial download. You can also drag/drop your own text/json corpus into the UI to try it against your own dataset.
Live demo/compare with a few other libs (there are many more in the codebase, in various states of completion, WIP):
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
In isolation for perf assessment:
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
To increase fuzziness and get broader results, try setting intraMax=1 (core) and enable outOfOrder (userland):
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
Also play with the sortPreset selector to swap out the default Array.sort() for one in userland that prioritizes typehead-ness (the resultset remains identical).
Still TODO:
- Example of stripping diacritics
- Example of using non-latin charsets
- Example of prefix-caching to improve typeahead perf even further
- Example of poor man's document search (matching multiple object properties)
That's all, thanks!
85 comments
[ 2.9 ms ] story [ 127 ms ] threadfor a language that evolved to manipulate text documents it is odd that it has no features of this kind. StartsWith endsWith and indexOf seems an amazingly unsophisticated set of tools.
autocomplete ui is also terrible compared to phones?
why?
Tell me you know nothing about JavaScript's history, without telling me you know nothing about JavaScript's history.
We got arrow functions use strict template strings and even asm.js, countless truly insane things were added to the eco system like unicode symbols and css animations (madness!) all of a quality as~if a late Sunday night project - per www tradition.
Im simply suggesting the next weird thing should be fuzzy text matching. Even if the implementation is complete garbage, like a permanently frozen turd, say, string compare returning a value between 0 and 1 I could see myself use it often enough. If needed one can always wrap some enormous enterprise natural language processing api to keylog the user and beam their delicious data back to the mothership for advertisement and creditscores etc
I know we cant have nice things but I can dream and it doesn't have to be nice?
How difficult - or not - would it be to use it with https://bootstrap-table.com?
filtering one one column? easy.
filtering multiple columns via AND? also easy.
filtering multiple columns plus highlighting matched parts in each? will take more work, but shouldnt be daunting.
Truth be told I'm working on a side project POC / MVP, found bootstrap table, and just dove in. I know 3 to 6 hrs more than you :)
https://www.forrestthewoods.com/blog/reverse_engineering_sub...
you can make uFuzzy behave similarly by setting intraMax to Infinity (just remove 0 from the field). but the results are usually too fuzzy in this config, though it depends on the corpus and application (auto-complete vs search)
Amusingly you do have Hearthstone cards and UE4 filenames whose data definitely comes from my post.
My code has been used in more than a few fuzzy matchers. Anytime I see one posted I always check to see if it’s related or not. =D
Mine was definitely tuned for filenames and matching CamelCaseWords.
> Mine was definitely tuned for filenames and matching CamelCaseWords.
mhm, mine considers case-change and alpha-num boundaries same as whitespace and punct boundaries (boostable), so will also float those results up when appropriate.
I am annoyed Blizzard never implemented fuzzy matching for their cards. At least not the last time I played Hearthstone…
I am also quite frustrated with the current state of full text search in the javascript world. All libs I've tried miss the most basic examples and their community seems to ignore it. Will give yours a try but it already looks much better from the comparison page.
Edit: Nope, your lib doesn't seem to handle substitution well (THE most common type of typo), so yep, we are back to square one ...
the intro does caveat that it would make for a poor spellcheck :)
FlexSearch actually does pretty well and can work for you, though can get quite memory hungry depending on your tokenization settings. try other libs in my compare demo, too. there are a lot of options!
i'm an OSS dev; how thin do you think my skin is? :D
But thats a good general sentiment yeah
e.g.
/^cat$/ will match cat
/^ca[tr]$/ will match cat and car
/^ca\w$/ will match any 3 letter word with 'ca' prefix. (any substitution for third letter)
/^\w\w\w$/ will allow alterations for any letter, making it useless.
you can make a more complex regex that allows a single substitution in any char position
/^(?:ca\w|c\wt|\wat)$/
but this gets out of hand very quickly, especially with possible insertions, deletions, transpositions, etc.
Anyway, not being a hater (as that other commenter suggested), if you could add support for this (even if it's limited, one or two typos at most) you will blow all other libs out of the way since what you have now is already quite good :D
of course if you have specific known/common mistakes, it could be useful to only consider those. for example, spelling errors are less common at the start of words. and keyboard letter proximity limits which mistakes are likely.
At least on mobile, I find I make just as many first letter mistakes by hitting the wrong "key" on the on-screen keyboard, as I do in any other position of the word. Very annoyingly the predictive text engine assumes like you mention and it takes a lot for it to consider the first letter being wrong.
This would help a lot I think.
When I'm on mobile, the most common error is "keyboard offset error", where I hit a "key" next to the one I intended. So it's not completely arbitrary, and it only happens once or twice in 99% of the cases.
On a physical keyboard this also happens, but the more likely error is synchronization error, where I hit a left-hand key before a right-hand key or vice versa, the classic teh vs the. Again usually only a single such error per word and not arbitrary.
Finally there's also the common case of simply missing a letter. Again, limited and not arbitrary.
So at least for my sake, anything that can handle the above errors would go a long way.
The regex library builds the NFA, so it probably bakes the fuzzy stuff into the NFA itself rather than changing the pattern.
Another option is something like word2vec where you cluster words of similar meaning together, as a bonus this usually handles typos as well. Not really in scope for your library, but I find it cool!
I settled on MiniSearch. [0] It is fast & small enough and fairly feature complete.
Afterwards I made a few contributions to improve performance and implement a better scoring algorithm. So I'm probably a bit biased now. Take my recommendation with a grain of salt.
Personally I think that OP's library does not perform searches, fuzzy or otherwise. It's much more similar to 'grep'. Try searching for "mario adventures". It won't actually find the most obvious results, because the order of the keywords in the search string must match the order of the keywords in the indexed text.
[0]: https://github.com/lucaong/minisearch
[1]: https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
i'm really confused why people dont bother actually reading anything i spent so much time distilling (both in the readme and the short instructions in this submission itself), which explicitly spell out how to adjust the necessary options precisely for what you're asking.
simply toggling outOfOrder returns perfectly good results:
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
Sorry for missing that feature. I stand by my overall point though. There is more to search than finding matches. Handling real world typing errors is one (try searching for "mario avdentures" or "mario adventutes").
Ordering results by relevance is not trivial either. And it does not appear this library intends to tackle that problem completely. Relevance scoring should take into account the term frequency and the document/field length, as well as average length.
I don't mean to discount your project though. There are many distinct use cases related to searching and filtering, and power to you for solving it in a way that works for you (and I'm sure for others as well). I just wanted to share my experience in exploring the space of full text search libraries that handle long form text and typos gracefully.
s/everything/anything
this is by no means meant to replace fulltext search, with term omission tolerance, typos, stemming, etc.
fwiw, i wrote this to replace a frontend search strategy that was originally based on MiniSearch and [apparently] gave underwhelming results and/or performance.
since you seem to be familiar with MiniSearch, perhaps you can help improve the result ordering for this:
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
this is my fundamental issue with fulltext searches. even if the result order could be made sane, how do you find the "junk cutoff" (there clearly is one). at least it does seem to place the best results on top, though ordered haphazardly, which is also true of FlexSearch.
boiling things down to a single relevance score, and making the user tweak various boosting knobs to nudge things mostly [but not perfectly] into place seems like a dogma that most of these engines suffer from.
In the case of MiniSearch you can change this default and configure it to only return hits if all keywords match [0].
Searching for "super ma" seems like an autocomplete query, for which most users have subtly different expectations than regular search. MiniSearch has a separate method for that, essentially baking in different default settings. [1]
[0] https://lucaong.github.io/minisearch/classes/_minisearch_.mi...
[1] https://lucaong.github.io/minisearch/classes/_minisearch_.mi...
Edit:
> s/everything/anything
No need for the sneer.
https://news.ycombinator.com/item?id=33053180
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
perf:
https://github.com/leeoniya/uFuzzy#benchmark
and also please submit any improvements to the codebase to get the results better/closer. i'm only an expert in one lib (mine).
[0]: https://github.com/cloudcannon/pagefind
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
intraChars: [a-z\d ]
(probably not ideal for perf)
you can leave intraMax at 1 in this case
you dont need either of these if you have camelcase LlanowarElves but will for lowercase elves: Llanowarelves
Was using indexeddb considered? I've yet to see an easy to use library that allows you to store simple but large json in indexeddb, and then query against that. Useful for something as simple as an emoji picker which needs to store keywords or aliases.
i mean, you can store it in localstorage or wherever to save net transfer for repeated use, but i dont think it would be faster to use indexdb directly at runtime.
The memory efficiency you might get would be that you don't need to hold the whole dataset in memory while running the filter step though at the moment it looks like it assumes you're working with an array in memory (https://github.com/leeoniya/uFuzzy/blob/main/src/uFuzzy.js#L...). That said I suspect there distance between this and something that could search against a stream of data is pretty short.
I see that your heap is incredibly small, how do you keep such a small working set of memory while keeping the library so fast?
other than that, nothing special. the stats-gathering pass works with large columnar arrays rather than thousands of objects. and it only does this when the pre-filtered resultset is small enough (default threshold is 1000). there's no string distance checks done, so there's no need to heap-allocate thousands of M*N matricies.
Another new better package in the JavaScript world just for the sake of pride.
in case you're still confused:
https://web.archive.org/web/20070310183121/http://www.lejeun...
PS: leeoniya, your link makes no sense in this context.
my link explains where that phrase comes from. it's a play on words that has nothing to do with pride or why i made "another one" (which is explained in great detail in many other places).
i made uFuzzy because i couldnt get the other 20 existing libraries to return only the results that i expected.
Also as this is a new library I highly recommend either fully dropping commonjs support or at least creating a ES module version and showing how to use that instead.
> at least creating a ES module version
that exists
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
https://github.com/leeoniya/uFuzzy/issues/2
however, it's probably infeasible to accomodate more than single-char-per-term substitution tolerance. thankfully, both your examples have 1-char substitutions :)
[1] https://news.ycombinator.com/item?id=33042665
uFuzzy can be made tolerant to extra insertions in the matches between/around the specified needle chars, and can also handle out of order terms. those together cover a surprising amount of common cases.
but it's not fuzzy in unlimited ways, such as letter omissions in the match (a superset of substitutions) like a spellcheck or levenshtein distance would be...but extreme tolerance often produces garbage results, too.
actually, might be able to handle single-char-per-term omissions as well:
https://github.com/leeoniya/uFuzzy/issues/2#issuecomment-126...
It's more like you can search using word parts. Similar to how your IDE's search work when jumping to files. E.g. you can type 'smb' to find 'Super Meat Boy'. Or type something like 'sup mea acc' to find 'Super Meat Boy Accessed Content'
So it requires you to know exactly what you're looking for, but you can find it quickly without having to type a lot.
Generally for full text search like you're describing you need to do that on the server side. It would be too heavy to have something full featured like that on the client side.
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...
commit:
https://github.com/leeoniya/uFuzzy/commit/63dc67b8bdb7577f85...
Some weeks ago (86 days ago) you commented on a post with this: <<
”Dammit, Jack, we got another one.” ”Another awakening?”
”Yeah.”
”Just do what we always do, make their argument look nihilist or absurdist. Remember to avoid mentioning capitalism and neoconservatism!”
>>
and then said that you plan on becoming a professional author. I remembered of your existence and it crossed my mind that I'm curious about how that is going for you.
what results are you expecting but not getting back? or what results are you getting back that you are not expecting?
xemaple has two transposition errors, one in the critical (and unusual) prefix position. if we allowed this level of fuzz in "spac ca" it should presumably match "psac ca", "spca ac", "sapc ac". it seems like a good idea at first, but with a big enough corpus you start to realize that these mangled variations actually appear as substrings in very unrealated matches, (they're all two swap errors away)
i dont think you can have what you're asking for without destroying match quality and making things a lot slower in the process - the very qualities that distinguish uFuzzy from what already exists.
i did add support for single transpositions, substitutions, and deletions (in non prefix or suffix positions) yesterday. so your version with one swap ("exmaple") should work.
https://leeoniya.github.io/uFuzzy/demos/compare.html?libs=uF...