#[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