Trie in JavaScript: The data structure behind autocomplete
stackfull.dev
stackfull.dev
However, for autocomplete you often want a weighted Trie because you have extra information you want to weight nodes by. An example with contacts is that you often want recent and frequent contacts.
My company has an open source trie implementation here for a client to do weighted contact auto complete: https://github.com/shortwave/trie
In Typesense[1], I've implemented fuzzy search based on levenshtein damerau distance and it's incredibly fast. I've found this approach to be a much better (faster + more flexible) alternative to Peter Norvig's brute-force based spell-checker that is quite a popular post [2].
[1]: https://github.com/typesense/typesense [2]: https://norvig.com/spell-correct.html
Indexes in mongodb use a similar concept (they call that prefix compression [1]), and that allows them to store indexes more efficiently.
We use them at Pyroscope and we wrote a blog post about our storage design [2] There are even some animations :)
[0] https://www.m4b.io/reverse/engineering/mach/binaries/2015/03...
[1] https://docs.mongodb.com/manual/reference/glossary/#std-term...
[2] https://github.com/pyroscope-io/pyroscope/blob/main/docs/sto...
from collections import defaultdict
END = object()
def make_trie():
return defaultdict(make_trie)
def insert(trie, word):
for c in word:
trie = trie[c]
trie[END] = True {'f': {'o': {'o': {END: True}}}}
I don't think this implementation is efficient enough to ever be worth using in a real program, though. function trieBuilder(word_list) {
const root = {};
for (const word of words_list) {
let node = root;
for (const char of word) {
let nextNode = node[char];
if (!nextNode) node[char] = nextNode = {};
node = nextNode;
}
node._ = 1; // mark the nodes that are endings of real words
}
function findChildren(node, prefix, list, maxLength) {
if (node._ === 1) list.push(prefix);
for (const char in node) {
findChildren(node[char], prefix + char, list, maxLength);
if (list.length >= maxLength) return list;
}
return list;
}
function findSuffixes(prefix, maxLength) {
prefix = prefix.toLowerCase();
let node = root;
for (const char of prefix) {
let nextNode = node[char];
if (!nextNode) return [""];
node = nextNode;
}
let words = findChildren(node, prefix, [], maxLength);
return words;
}
return {root, findSuffixes};
}
demo: https://observablehq.com/@jobleonard/autocompleteThere is an option to get all suffixes without traversing subtree, but it comes with extra O(N) memory where N is combined length of all stored words - depending on case might be acceptable since memory for storing words itself is O(N) anyway. https://stackoverflow.com/a/29966616/2104560 (update 1 and update 3)
And thanks for the link, that is an interesting optimization!
EDIT: one fun non-practical application (histogramming the letters in a word is simpler and faster) is an anagram finder using prime numbers:
https://observablehq.com/@jobleonard/finding-anagrams-using-...
UPD: Sorry, "up to 15" is a wrong phrasing. I checked once how "prime factorial" fits into primitive, and first 15 primes can fit into long. So it's possible to handle even more symbols if it's smth like "aaaaaaa"64 times because it would be just 2^64
Fredkin's contribution were to reversible computation and since Quantum computers are a kind of reversible computer, many ideas and gate notions carried over.
However, the idea of implementing in JS gives me the impression that the author is advocating implementing autocomplete on the client (browser) [1]. I would encourage developers not to do so. Depending on the data set to be rendered, that could be a big perf hit or not feasible in certain scenarios, but if the data set is not dynamic or has a small memory footprint (ie, states in the US), then it should be fine to have that data sent to the client to be processed on.
My general rule of thumb - limit business logic to the server and rendering logic to the browser.
You can implement server-side autocomplete - send the partial search string as a request to the server, which responds with the autocomplete list of strings to be rendered in the UI, debouncing [2] to limit the number of requests to the server (instead of requesting on every user change to the partial search string). Of course, this would have its trade offs (API reqs introduce their own perf concerns), but you want to go this route if your search data set is dynamic and sufficiently large.
[1] Not sure if that was the author's intention bc I cannot access the author's blog on my work machine.
As in a spreadsheet-based prefix tree?
Can I see it?
It's https://en.wikipedia.org/wiki/Hash_array_mapped_trie which used in Scala's immutableMap https://dotty.epfl.ch/api/scala/collection/immutable/HashMap...
https://github.com/mgraczyk/fast-trie-js/blob/master/index.j...
I used it for this little demo:
e.g. ABD matches A Brown Dog
^ ^ ^Yes, and it's crazy complicated. See Levenshtein Automata[1]. In reality, I would let the professionals handle this by either using Open/ElasticSearch or Apache Lucene.
[1]http://blog.notdot.net/2010/07/Damn-Cool-Algorithms-Levensht...
If this site doesn't contain "the professionals" then the phrase has zero meaning.
Same as you can design a cpu in HDL and flash it to an FPGA. (The "Nand to Tetris" course seems popular) Same as you can write an OS. Same as you can write a compiler and/or interpreter.
Believing these things (and they're actually true!) gets us all away from learned helplessness. There's enough of that when it comes to actual silicone...
Nobody understands it all. _You_ understand as much of it as you choose, in the direction that takes you, as deep as you want to go. It's just work, a lot of it, but no more than that.
First, you normalize your strings to index (convert to lower case, throw out stop words such as "a", "the", "is", etc.) then you generate prefixes (or n-grams, I suppose). Say you want to do musicians and you have "Bob Dylan" as an entry. That would become "bob dylan" which would generate the prefixes "bo", "bob", "bob d", etc. You include spaces but once you hit a space, you start generating new prefixes. So you would also start doing "dy", "dyl", "dyla", and "dylan". The max length of the prefixes depends on how much memory you're willing to use.
Lets say Bob Dylan is mapped to some musician table in your DB and he has id 1000. I've done this with Redis, but you can use a trie as well. You'd store every prefix in the trie and the data on the node would be an array of musician ids. Now the magic: if you search "bo dyl" you would look up the prefix "bo", grab all the ids and then look up the prefix "dyl" and grab the ids (you would also try the full "bo dyl" first, but let's just assume nothing is there). Once you have those two arrays, you would take the intersection of them. Which, in our case, would leave an array containing id = 1000 along with any others that matched both.
That's, in general, how a simple autocomplete works. But you're better off using Redis or Elasticsearch if your data set is large. And if it's not, well... a simple hashmap is probably competitive and easier than tries.
There are heuristics you can apply here rather than implementing full fuzziness or levenshtein comparators etc. Ultimately it depends on your use-case and what common variations users might try to use.
Please use const. Everything should be const until there is a proven need otherwise.
Take the code in this article as an example. What danger can there possibly be with using a let when iterating over the set of characters in a word? The inside of the for loop is two lines long. The extra information that using const provides to whoever maintains this code is negligible. Using a const doesn’t hurt, but that level of nitpicking it doesn’t add much value whether in this thread or in code reviews.