Searchkick
We use Searchkick for full text search.
Searchkick is a library that integrates ElasticSearch or OpenSearch (ES/OS)
with ActiveRecord, enhancing it with many features. You may want to read
its docs before keeping on with
the reading.
A former knowledge of ElasticSearch or OpenSearch is a great help when dealing with them and with Searchkick. For any questions, don't hesitate to ask around.
Next index management
The Problem
While developing Warehouse products full text search, when we deployed changes
to the Searchkick index, we got the products index page, which is the main
entrypoint of the Warehouse section, returning 500s because of the newly
deployed application trying to use the previous application's Searchkick index
version, which didn't contain the settings required by the new application.
Searchkick indices of some models (mainly Product) can take quite some time to
perform a .reindex (even more than 30 minutes), so it wasn't acceptable having
the main entrypoint down during all that time. We needed a way to create the new
index (referred as the next index from now on) before actually deploying it.
We tried to implement a solution that creates the next index before the deployment at platform level, but at the time of writing we couldn't figure out an easy way, given the infrastructure setup, to deploy the Docker image of the yet-to-be-deployed application just to create the next index.
The Solution
So we came up with an implementation residing in the SearchkickUtils
namespace, whose main character is the SearchkickUtils::WithNextIndexSettings
model's concern.
Suppose we have a model calling the .searchkick method to define a Searchkick
index:
model Product
searchkick settings: { ... }, mappings: { ... }
end
Now we want update the settings, but preventing the old application instances using the new index, or the new ones using the old one. We can make a preparatory release with the following code:
model Product
include SearchkickUtils::WithNextIndexSettings
current_settings = { ... }
next_settings = { index_prefix: 'v1', ... }
searchkick_with_next_index_settings(current_settings, next_settings)
end
The index_prefix value points to the index version. It must comply with the
versioning format, that is:
nilfor the first version"v1"for the second version"v#{i}"for the next versions, whereiis an integer number greater by one than the previous version
nil was chosen for the first version to support brand new Searchkick indices,
that are usually created without :index_prefix.
You'll get an error if the actual next settings version doesn't match with the expected one.
While working to a pull request containing index changes, you might want to
export SEARCHKICK_NEXT=true. This way you can develop against the updated
settings without compromising the index used by main.
After its deployment, we can run the maintenance task
MaintenanceTask::SearchkickUtils::CreateNextIndex passing the
model_name_as_string argument (in this case Product), either by console or
UI.
If the task terminated successfully, ES/OS should have one or more indices
matching the products_ pattern, that are the current indices, and one index
with the v1_products_ pattern, which is the next one. Additionally, if the
next index version reached "v3" or more, it cleans up the old indices using
the service SearchkickUtils::CleanUpOldIndicesService. This service deletes
the indices of two versions ago starting from the current one, preventing the
old indices from piling up.
Now we can deploy the codebase pointing to the next index, which is going to be the current one:
model Product
current_settings = { index_prefix: 'v1', ... }
searchkick(**current_settings)
end
We should now run a reindex to sync the current index with the changes that
happened between the two releases. We can achieve that using the maintenance
task MaintenanceTask::SearchkickUtils::Reindex passing the
model_name_as_string argument. Optionally, the updated_after argument may be
provided to perform a partial reindex that involves only records updated after
the updated_after date. This is handy for Searchkick models for which a whole
model reindex is too expensive in terms of time and possibly resources, such as
Product. The updated_after argument is set by default to prevent accidental
reindexing of the whole model. If you need to reindex the whole model, you can
set updated_after to nil.
Reverting a release pointing to the previous index
Since the indices pointing to the previous version are always preserved, we can
seamlessly deploy a release reverting a pull request containing an index version
bump, caring of running the maintenance task
MaintenanceTask::SearchkickUtils::Reindex passing the model name argument to
sync the index document changes.
F.A.Q.
When does a model need a reindex?
- when the
searchkickgem gets updated - when the
.search_datamethod gets updated
See https://github.com/ankane/searchkick#to-reindex-or-not-to-reindex for further information.
When to bump an index version?
Safe answer: every time a model's .searchkick method arugments get updated.
Here in TWPro there isn't much knowledge about ES/OS, so this rule prevents
any possible issues due to unexpected breaking changes.
How to deploy Searchkick changes?
Deploy Searchkick index changes
How to perform a reindex?
Deploy Searchkick index changes -> Reindex
How to bump an index version?
See SearchkickUtils::WithNextIndexSettings model's concern to learn how to
create the next version of a model's Searchkick index.