DEV Community

Nick Pezza
Nick Pezza

Posted on

RediSearch on Rails

If you are using Rails, chances are you are using Redis for something whether it is as a cache, ActionCable, or ActiveJob. So why not use it for one more thing?

Starting in v4 of Redis, Redis Modules were introduced which are add ons built to extend Redis' functionality. One of the first modules was RediSearch, a text search engine built on top of Redis. According to RediSearch.io:

Unlike other Redis search libraries, it does not use the internal data
structures of Redis like sorted sets. Using its own highly optimized data
structures and algorithms, it allows for advanced search features, high
performance, and low memory footprint. It can perform simple text searches, as
well as complex structured queries, filtering by numeric properties and
geographical distances. RediSearch supports continuous indexing with no
performance degradation, maintaining concurrent loads of querying and
indexing. This makes it ideal for searching frequently updated databases,
without the need for batch indexing and service interrupts.

Some of the headline features of RediSearch include:

  • Full-Text indexing of multiple fields in a document, including:
    • Exact phrase matching.
    • Stemming in many languages.
    • Prefix queries.
    • Optional, negative and union queries.
  • Distributed search on billions of documents.
  • Numeric property indexing.
  • Geographical indexing and radius filters.
  • Incremental indexing without performance loss.
  • A powerful auto-complete engine with fuzzy matching.
  • Concurrent low-latency insertion and updates of documents.
  • This benchmark against ElasticSearch!

Let's walk through how to get RediSearch integrated into your Rails app!

First, start out by installing Redis and RediSearch. Check out Redis.io for full installation instructions for Redis. If Homebrew is available you can brew install redis. As of v1.6, to build RediSearch do the following:

  1. git clone https://github.com/RediSearch/RediSearch.git
  2. cd RediSearch
  3. make

Once RediSearch is built you will need to tell Redis to load the module. The best way is to add loadmodule /path/to/redisearch.so to your redis.conf file to always load the module. (On macOS the redis.conf file can be found at /usr/local/etc/redis.conf). Once the conf file has been updated restart Redis. For full instructions on installing RediSearch visit RediSearch.io.

Alternatively, you can run Redis and RediSearch with Docker using docker run -p 6379:6379 redislabs/redisearch:latest.

Once Redis and RediSearch are installed add the redi_search gem to your Gemfile:

gem 'redi_search'
Enter fullscreen mode Exit fullscreen mode

and then run bundle install. Or you can install from the GitHub package registry.

Once installed we'll need to make an initializer to configure our Redis connection.

# config/initializers/redi_search.rb
RediSearch.configure do |config|
  config.redis_config = {
    host: "127.0.0.1",
    port: "6379"
  }
end
Enter fullscreen mode Exit fullscreen mode

Now that we have RediSearch available in our app, let's use it to index a model. We'll use a User model with first and last attributes as an example.

class User < ApplicationRecord
  redi_search schema: {
    first: { text: { phonetic: "dm:en" } },
    last: { text: { phonetic: "dm:en" } }
  }
end
Enter fullscreen mode Exit fullscreen mode

Calling the redi_search class method inside a model accepts one required named parameter called schema. This defines the fields inside an index and the attributes for those fields. RediSearch has text, numeric, geo, and tag fields. The phonetic option was passed above because we are indexing names and it makes it easier to search for names with similar sounds but spelled differently. The full list of available options for the different field types can be found here.

User.reindex
Enter fullscreen mode Exit fullscreen mode

Calling the redi_search class method inside a model adds a couple of useful methods, including reindex which does a couple of things:

  1. Creates the index if it doesn't exist
  2. Calls the search_import scope to fetch all the records from the database
  3. Converts those records to RediSearch Documents
  4. Indexes all those documents into Redis
User.search("jak")
Enter fullscreen mode Exit fullscreen mode

Now that we have all of our users indexed we can start searching for them. Querying is similar to the ActiveRecord interface where clauses and conditions can be chained together and the search is executed lazily​. Some simple queries are:

# simple phrase query - jak AND daxter
User.search("jak").and("daxter")

# exact phrase query - jak FOLLOWED BY daxter
User.search("jak daxter")

# union query - jak OR daxter
User.search("jak").or("daxter")

# negation query - jak AND NOT daxter
User.search("jak").and.not("daxter")
Enter fullscreen mode Exit fullscreen mode

Some more complex queries are:

# intersection of unions - (hello OR halo) AND (world OR werld)
User.search(User.search("hello").or("halo")).and(User.search("world").or("werld"))
# negation of union - hello AND NOT (world or werld)
User.search("hello").and.not(User.search("world").or("werld"))
# union inside phrase - hello AND (world OR werld)
User.search("hello").and(User.search("world").or("werld"))
Enter fullscreen mode Exit fullscreen mode

All terms support a few options that can be applied.

Prefix terms: match all terms starting with a prefix. (Akin to like term% in SQL)

User.search("hel", prefix: true)
User.search("hello worl", prefix: true)
User.search("hel", prefix: true).and("worl", prefix: true)
User.search("hello").and.not("worl", prefix: true)
Enter fullscreen mode Exit fullscreen mode

Optional terms: documents containing the optional terms will rank higher than those without

User.search("foo").and("bar", optional: true).and("baz", optional: true)
Enter fullscreen mode Exit fullscreen mode

Fuzzy terms: matches are performed based on Levenshtein distance. The maximum Levenshtein distance supported is 3.

User.search("zuchini", fuzziness: 1)
Enter fullscreen mode Exit fullscreen mode

Search terms can also be scoped to specific fields using a where clause:

# Simple field specific query
User.search.where(name: "john")
# Using where with options
User.search.where(first: "jon", fuzziness: 1)
# Using where with more complex query
User.search.where(first: User.search("bill").or("bob"))
Enter fullscreen mode Exit fullscreen mode

Searching for numeric fields accepts a range:

User.search.where(number: 0..100)
# Searching to infinity
User.search.where(number: 0..Float::INFINITY)
User.search.where(number: -Float::INFINITY..0)
Enter fullscreen mode Exit fullscreen mode

When searching, by default a collection of Documents is returned. Calling #results on the search query will execute the search, and then look up all the found records in the database and return an ActiveRecord relation.

Another useful method redi_search adds is spellcheck and responds with suggestions for misspelled search terms.

User.spellcheck("jimy")
  RediSearch (1.1ms)  FT.SPELLCHECK user_idx jimy DISTANCE 1
=> [#<RediSearch::Spellcheck::Result:0x00007f805591c670
    term: "jimy",
    suggestions:
     [#<struct RediSearch::Spellcheck::Suggestion score=0.0006849315068493151, suggestion="jimmy">,
      #<struct RediSearch::Spellcheck::Suggestion score=0.00019569471624266145, suggestion="jim">]>]
User.spellcheck("jimy", distance: 2).first.suggestions
  RediSearch (0.5ms)  FT.SPELLCHECK user_idx jimy DISTANCE 2
=> [#<struct RediSearch::Spellcheck::Suggestion score=0.0006849315068493151, suggestion="jimmy">,
 #<struct RediSearch::Spellcheck::Suggestion score=0.00019569471624266145, suggestion="jim">]
Enter fullscreen mode Exit fullscreen mode

Next time you are looking for a search engine give RediSearch a try! You can read about more options and see more examples on the README:

GitHub logo npezza93 / redi_search

Ruby wrapper around RediSearch that can integrate with Rails

RediSearch

A simple, but powerful, Ruby wrapper around RediSearch, a search engine on top of Redis.

Installation

Firstly, Redis and RediSearch need to be installed.

You can download Redis from https://redis.io/download, and check out installation instructions here. Alternatively, on macOS or Linux you can install via Homebrew.

To install RediSearch check out https://oss.redislabs.com/redisearch/Quick_Start.html Once you have RediSearch built, if you are not using Docker, you can update your redis.conf file to always load the RediSearch module with loadmodule /path/to/redisearch.so. (On macOS the redis.conf file can be found at /usr/local/etc/redis.conf)

After Redis and RediSearch are up and running, add the following line to your Gemfile:

gem 'redi_search'
Enter fullscreen mode Exit fullscreen mode

And then:

❯ bundle
Enter fullscreen mode Exit fullscreen mode

Or install it yourself:

❯ gem install redi_search
Enter fullscreen mode Exit fullscreen mode

and require it:

require 'redi_search'
Enter fullscreen mode Exit fullscreen mode

Once the gem is installed and required you'll need to configure it with your Redis configuration. If you're on Rails, this should…

Latest comments (1)

Collapse
 
andrewmcodes profile image
Andrew Mason

Great article, Nick. Will definitely have to check this out 😉