Auto-Suggest via ‘Trie’ (Pre-fix Tree)

Posted by Strenium on Geeks with Blogs See other posts from Geeks with Blogs or by Strenium
Published on Thu, 23 Aug 2012 22:19:25 GMT Indexed on 2012/08/27 21:40 UTC
Read the original article Hit count: 699

Filed under:

Auto-Suggest (Auto-Complete) “thing” has been around for a few years. Here’s my little snippet on the subject.

For one of my projects, I had to deal with a non-trivial set of items to be pulled via auto-suggest used by multiple concurrent users. Simple, dumb iteration through a list in local cache or back-end access didn’t quite cut it.

Enter a nifty little structure, perfectly suited for storing and matching verbal data: “Trie” (http://tinyurl.com/db56g) also known as a Pre-fix Tree:

“Unlike a binary search tree, no node in the tree stores the key associated with that node; instead, its position in the tree defines the key with which it is associated. All the descendants of a node have a common prefix of the string associated with that node, and the root is associated with the empty string. Values are normally not associated with every node, only with leaves and some inner nodes that correspond to keys of interest.”

image

This is a very scalable, performing structure. Though, as usual, something ‘fast’ comes at a cost of ‘size’; fortunately RAM is more plentiful today so I can live with that.

I won’t bore you with the detailed algorithmic performance here - Google can do a better job of such.

So, here’s C# implementation of all this. Let’s start with individual node:

Trie Node
  1. /// <summary>
  2. /// Contains datum of a single trie node.
  3. /// </summary>
  4. public class AutoSuggestTrieNode
  5. {
  6.     public char Value { get; set; }
  7.  
  8.     /// <summary>
  9.     /// Gets a value indicating whether this instance is leaf node.
  10.     /// </summary>
  11.     /// <value>
  12.     ///     <c>true</c> if this instance is leaf node; otherwise, a prefix node <c>false</c>.
  13.     /// </value>
  14.     public bool IsLeafNode { get; private set; }
  15.  
  16.     public List<AutoSuggestTrieNode> DescendantNodes { get; private set; }
  17.  
  18.  
  19.     /// <summary>
  20.     /// Initializes a new instance of the <see cref="AutoSuggestTrieNode"/> class.
  21.     /// </summary>
  22.     /// <param name="value">The phonetic value.</param>
  23.     /// <param name="isLeafNode">if set to <c>true</c> [is leaf node].</param>
  24.     public AutoSuggestTrieNode(char value = ' ', bool isLeafNode = false)
  25.     {
  26.         Value = value;
  27.         IsLeafNode = isLeafNode;
  28.  
  29.         DescendantNodes = new List<AutoSuggestTrieNode>();
  30.     }
  31.  
  32.     /// <summary>
  33.     /// Gets the descendants of the pre-fix node, if any.
  34.     /// </summary>
  35.     /// <param name="descendantValue">The descendant value.</param>
  36.     /// <returns></returns>
  37.     public AutoSuggestTrieNode GetDescendant(char descendantValue)
  38.     {
  39.         return DescendantNodes.FirstOrDefault(descendant => descendant.Value == descendantValue);
  40.     }
  41. }

 

Quite self-explanatory, imho. A node is either a “Pre-fix” or a “Leaf” node. “Leaf” contains the full “word”, while the “Pre-fix” nodes act as indices used for matching the results.

 

Ok, now the Trie:

Trie Structure
  1. /// <summary>
  2. /// Contains structure and functionality of an AutoSuggest Trie (Pre-fix Tree)
  3. /// </summary>
  4. public class AutoSuggestTrie
  5. {
  6.     private readonly AutoSuggestTrieNode _root = new AutoSuggestTrieNode();
  7.  
  8.     /// <summary>
  9.     /// Adds the word to the trie by breaking it up to pre-fix nodes + leaf node.
  10.     /// </summary>
  11.     /// <param name="word">Phonetic value.</param>
  12.     public void AddWord(string word)
  13.     {
  14.         var currentNode = _root;
  15.         word = word.Trim().ToLower();
  16.  
  17.         for (int i = 0; i < word.Length; i++)
  18.         {
  19.             var child = currentNode.GetDescendant(word[i]);
  20.  
  21.             if (child == null) /* this character hasn't yet been indexed in the trie */
  22.             {
  23.                 var newNode = new AutoSuggestTrieNode(word[i], word.Count() - 1 == i);
  24.  
  25.                 currentNode.DescendantNodes.Add(newNode);
  26.                 currentNode = newNode;
  27.             }
  28.             else
  29.                 currentNode = child; /* this character is already indexed, move down the trie */
  30.         }
  31.     }
  32.  
  33.  
  34.     /// <summary>
  35.     /// Gets the suggested matches.
  36.     /// </summary>
  37.     /// <param name="word">The phonetic search value.</param>
  38.     /// <returns></returns>
  39.     public List<string> GetSuggestedMatches(string word)
  40.     {
  41.         var currentNode = _root;
  42.         word = word.Trim().ToLower();
  43.  
  44.         var indexedNodesValues = new StringBuilder();
  45.         var resultBag = new ConcurrentBag<string>();
  46.  
  47.         for (int i = 0; i < word.Trim().Length; i++)  /* traverse the trie collecting closest indexed parent (parent can't be leaf, obviously) */
  48.         {
  49.             var child = currentNode.GetDescendant(word[i]);
  50.  
  51.             if (child == null || word.Count() - 1 == i)
  52.                 break; /* done looking, the rest of the characters aren't indexed in the trie */
  53.  
  54.             indexedNodesValues.Append(word[i]);
  55.             currentNode = child;
  56.         }
  57.  
  58.         Action<AutoSuggestTrieNode, string> collectAllMatches = null;
  59.         collectAllMatches = (node, aggregatedValue) => /* traverse the trie collecting matching leafNodes (i.e. "full words") */
  60.             {
  61.                 if (node.IsLeafNode) /* full word */
  62.                     resultBag.Add(aggregatedValue); /* thread-safe write */
  63.  
  64.                 Parallel.ForEach(node.DescendantNodes, descendandNode => /* asynchronous recursive traversal */
  65.                 {
  66.                     collectAllMatches(descendandNode, String.Format("{0}{1}", aggregatedValue, descendandNode.Value));
  67.                 });
  68.             };
  69.  
  70.         collectAllMatches(currentNode, indexedNodesValues.ToString());
  71.  
  72.         return resultBag.OrderBy(o => o).ToList();
  73.     }
  74.  
  75.  
  76.     /// <summary>
  77.     /// Gets the total words (leafs) in the trie. Recursive traversal.
  78.     /// </summary>
  79.     public int TotalWords
  80.     {
  81.         get
  82.         {
  83.             int runningCount = 0;
  84.  
  85.             Action<AutoSuggestTrieNode> traverseAllDecendants = null;
  86.             traverseAllDecendants = n => { runningCount += n.DescendantNodes.Count(o => o.IsLeafNode); n.DescendantNodes.ForEach(traverseAllDecendants); };
  87.             traverseAllDecendants(this._root);
  88.  
  89.             return runningCount;
  90.         }
  91.     }
  92. }

 

Matching operations and Inserts involve traversing the nodes before the right “spot” is found. Inserts need be synchronous since ordering of data matters here.

However, matching can be done in parallel traversal using recursion (line 64).

Here’s sample usage:

  1.  
  2. [TestMethod]
  3. public void AutoSuggestTest()
  4. {
  5.     var autoSuggestCache = new AutoSuggestTrie();
  6.  
  7.     var testInput = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero.
  8.                 Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris.
  9.                 Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad
  10.                 litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. Sed dignissim lacinia nunc.
  11.                 Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem.
  12.                 Proin ut ligula vel nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa. Fusce ac
  13.                 turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque
  14.                 volutpat condimentum velit. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam
  15.                 nec ante. Sed lacinia, urna non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis. Nulla
  16.                 facilisi. Ut fringilla. Suspendisse potenti. Nunc feugiat mi a tellus consequat imperdiet. Vestibulum sapien. Proin quam. Etiam
  17.                 ultrices. Suspendisse in justo eu magna luctus suscipit. Sed lectus. Integer euismod lacus luctus magna. Quisque cursus, metus
  18.                 vitae pharetra auctor, sem massa mattis sem, at interdum magna augue eget diam. Vestibulum ante ipsum primis in faucibus orci
  19.                 luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed non quam. In vel mi sit amet
  20.                 augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet. Donec lacus nunc, viverra nec.";
  21.  
  22.     testInput.Split(' ').ToList().ForEach(word => autoSuggestCache.AddWord(word));
  23.  
  24.     var testMatches = autoSuggestCache.GetSuggestedMatches("le");
  25. }

 

..and the result:

image

That’s it!

© Geeks with Blogs or respective owner