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:
- git clone https://github.com/RediSearch/RediSearch.git
- cd RediSearch
- 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'
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
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
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
Calling the redi_search
class method inside a model adds a couple of useful methods, including reindex
which does a couple of things:
- Creates the index if it doesn't exist
- Calls the
search_import
scope to fetch all the records from the database - Converts those records to RediSearch
Document
s - Indexes all those documents into Redis
User.search("jak")
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")
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"))
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)
Optional terms: documents containing the optional terms will rank higher than those without
User.search("foo").and("bar", optional: true).and("baz", optional: true)
Fuzzy terms: matches are performed based on Levenshtein distance. The maximum Levenshtein distance supported is 3.
User.search("zuchini", fuzziness: 1)
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"))
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)
When searching, by default a collection of Document
s 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">]
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:
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'
And then:
❯ bundle
Or install it yourself:
❯ gem install redi_search
and require it:
require 'redi_search'
Once the gem is installed and required you'll need to configure it with your Redis configuration. If you're on Rails, this should…
Top comments (1)
Great article, Nick. Will definitely have to check this out 😉