#[1]gnikyt feed [2]gnikyt Code ramblings. Building a simple Redis autosuggest with Ruby /* Mar 02, 2016 — 5.6KB */ So you have a search box on your website.. an article search, a product search.. whatever it may be, you may find yourself the need to display suggested results to your user based on what they type. Redis is the perfect solution. Planning Let’s say we have a bunch of products: * Tommy’s Ray Gun - ID: 1 * 1990 Blaster Ray!! - ID: 2 * (Nuke) Bomb Gun #8 - ID: 3 Looking at our products, we can see there is a different and unpredictable title. We have punctuation, special characters, numbers, and letters. If someone types Gun we’d like to see our search suggest (Nuke) Bomb Gun #8 and Tommy's Ray Gun. Redis does not offer a full-text search solution like ElasticSearch, so we simply can’t drop Tommy's Ray Gun string into Redis and expect to search it. We need to come up with a clever way. My solution was to split each letter up of each word in each product. For each letter set, we store the products which contain those letters. So common sets of letters in titles will be stored together. As a basic example, let’s look at a singular word. RUBY we can split this up into R then RU then RUB and finally RUBY. Lets store these product titles in this manner with the values being the title and ID for the product into a [3]Redis sorted set. Now, when someone types gun in your search box we should be able to call p:gun key on Redis and get: * Tommy's Ray Gun//1 * (Nuke) Bomb Gun #8//3 Processing the Objects So now we have a plan in place, let’s write a quick script to import the objects you wish to autosuggest, into your Redis database. As before, we need to split each word up of each object. # autosuggest.rb require "redis" redis = Redis.new redis.flushdb # Resets to a clean database def clean_title(title) # Change to your needs title.downcase.gsub(/-/, " ").gsub(/[^0-9a-z ]/, "") end # Our list of products products = [ {id: 1, title: "Tommy's Ray Gun"}, {id: 2, title: "1990 Blaster Ray!!"}, {id: 3, title: "(Nuke) Bomb Gun #8"} ] products.each do |product| puts "Processing #{product[:title]}..." # Clean the title, split up into parts clean_title(product[:title]).split(" ").each do |part| 1.upto(part.length) do |len| next if len == 1 # So we do not have a key of 1 length # Output a piece of each part puts part[0...len] end end end If you run this into your terminal it should output: > ruby autosuggest.rb Processing Tommy\'s Ray Gun... "to" "tom" "tomm" "tommy" "tommys" "ra" "ray" "gu" "gun" Processing 1990 Blaster Ray!!... "19" "199" "1990" "bl" "bla" "blas" "blast" "blaste" "blaster" "ra" "ray" Processing (Nuke) Bomb Gun #8... "nu" "nuk" "nuke" "bo" "bom" "bomb" "gu" "gun" Here we can see how the script cleans the titles, then breaks them down to produce key names for Redis to use as we had hoped for in the planning section. Now, let’s import this into Redis. Simply change line 27: # BEFORE puts part[0...len] # AFTER redis.zadd "p:#{part[0...len]}", 0, "#{product[:title]}//#{product[:id]}" This will now store the titles for the objects as planned into a sorted list on Redis, where common sets of parts will group objects together. Go ahead and run your script again. > ruby autosuggest.rb Processing Tommy's Ray Gun... Processing 1990 Blaster Ray!!... Processing (Nuke) Bomb Gun #8... Login to Redis and let’s check if it works as planned. Since this is a sorted set we need to use [4]ZRANGE. > redis-cli 127.0.0.1:6379> ZRANGE p:gun 0 -1 1) "(Nuke) Bomb Gun #8//3" 2) "Tommy's Ray Gun//1" 127.0.0.1:6379> ZRANGE p:nuke 0 -1 1) "(Nuke) Bomb Gun #8//3" Awesome, it works! We now have sorted sets with groups of products based on parts of the words in the object titles. Frontend Now that we have a script (that you should expand on into a proper lib), we need to now show results to the user for when they’re searching. Here’s a quick Sinatra example (of course you can use more advanced techniques as well) require "redis" require "json" require "securerandom" require "sinatra/base" require "sinatra/jsonp" module YourApp class AutoComplete < Sinatra::Base helpers Sinatra::Jsonp configure {set :redis, Redis.new} get "/" do # Clean the query and get each word sets = [] clean_query(params["q"]).split(" ").each {|word| sets << "p:#{word}"} # Get the common results in a temporary key tmp_key = "tmp_#{SecureRandom.uuid[0...8]}" settings.redis.zinterstore tmp_key, sets results = settings.redis.zrange tmp_key, 0, -1 settings.redis.del tmp_key # Output results as JSON to browser jsonp results.to_json end private def clean_query(query) # Remove all special characters and adjusts naming query.downcase.gsub(/-/, " ").gsub(/[^0-9a-z ]/, "") end end end By calling /?q=some+text, we create a key for each word passed. So some+text goes into the sets variable and becomes ["p:some", "p:text"]. Next, we create a temporary key to use with [5]zinterstore which computes intersection between keys (our p:some and p:text). This finds products that have both the words some and text in their title. We then use zrange to get the result of the intersection and delete the temporary key. Finally, send the results as JSON. You can use AJAX to actively call the Sinatra app when the user is typing. [6]MD | [7]TXT | [8]CC-4.0 This post is 9 years old and may contain outdated information. __________________________________________________________________ [9]Ty King Ty King A self-taught, seasoned, and versatile developer from Newfoundland. Crafting innovative solutions with care and expertise. See more [10]about me. [11]Github [12]LinkedIn [13]CV [14]RSS * * * * * * * * * * References Visible links: 1. /rss.xml 2. / 3. http://redis.io/commands/ZADD 4. http://redis.io/commands/ZRANGE 5. http://redis.io/commands/ZINTERSTORE 6. /building-a-simple-redis-autosuggest-with-ruby/index.md 7. /building-a-simple-redis-autosuggest-with-ruby/index.txt 8. https://creativecommons.org/licenses/by/4.0/ 9. /about 10. /about 11. https://github.com/gnikyt 12. https://linkedin.com/in/gnikyt 13. /assets/files/cv.pdf 14. /rss.xml Hidden links: 16. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-1 17. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-2 18. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-3 19. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-4 20. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-5 21. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-6 22. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-7 23. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-8 24. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-9 25. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-10 26. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-11 27. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-12 28. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-13 29. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-14 30. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-15 31. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-16 32. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-17 33. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-18 34. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-19 35. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-20 36. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-21 37. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-22 38. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-23 39. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-24 40. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-25 41. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-26 42. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-27 43. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-28 44. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-29 45. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-30 46. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-31 47. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb1-32 48. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb3-1 49. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb3-2 50. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb3-3 51. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb3-4 52. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb3-5 53. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-1 54. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-2 55. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-3 56. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-4 57. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-5 58. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-6 59. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-7 60. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-8 61. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-9 62. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-10 63. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-11 64. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-12 65. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-13 66. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-14 67. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-15 68. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-16 69. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-17 70. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-18 71. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-19 72. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-20 73. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-21 74. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-22 75. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-23 76. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-24 77. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-25 78. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-26 79. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-27 80. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-28 81. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-29 82. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-30 83. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-31 84. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-32 85. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-33 86. localhost/tmp/lynxXXXXDr0YOm/L768794-8343TMP.html#cb6-34