Esse is intentionally minimal. Framework and ORM integration, pagination, async indexing, and template engines are delivered as separate gems. Each extension is a plugin (see Plugins) and has its own docs/ directory.
ORM integrations
esse-active_record
gem 'esse-active_record' — ActiveRecord support. Adds collection Model DSL, scope, batch_context, automatic after-commit callbacks, and hook-based disabling.
class UsersIndex < Esse::Index plugin :active_record
repository :user do collection ::User, batch_size: 500 do scope :active, -> { where(active: true) } end document { |u, **| { _id: u.id, name: u.name } } endend
class User < ApplicationRecord include Esse::ActiveRecord::Model index_callback 'users_index:user'endesse-sequel
gem 'esse-sequel' — Sequel ORM support with an identical DSL to esse-active_record.
Framework integration
esse-rails
gem 'esse-rails' — Rails-specific integration. Subscribes to all elasticsearch.* events and surfaces aggregate search latency in controller logs (and Lograge).
Completed 200 OK in 125.3ms (Views: 45.2ms | Search: 78.1ms)Also auto-loads the Rails environment when esse CLI runs in a Rails project.
Async indexing
esse-async_indexing
gem 'esse-async_indexing' — Offload indexing to Sidekiq or Faktory. Adds async_indexing_job DSL, CLI commands (esse index async_import), and ActiveRecord callbacks that enqueue jobs instead of indexing synchronously.
class City < ApplicationRecord include Esse::AsyncIndexing::ActiveRecord::Model async_index_callback('geos_index:city', service_name: :sidekiq) { id }endHook management
esse-hooks
gem 'esse-hooks' — The callback/state layer used by esse-active_record and esse-sequel to enable/disable indexing globally, per-repository, or per-model. Not used directly by end users most of the time; included here for completeness.
Esse::ActiveRecord::Hooks.without_indexing { 10.times { User.create! } }Search query templates
esse-jbuilder
gem 'esse-jbuilder' — Build search bodies with Jbuilder templates.
UsersIndex.search do |json| json.query do json.match { json.set! 'name', params[:q] } endendOr from a .json.jbuilder file:
body = Esse::Jbuilder::ViewTemplate.call('users/search', q: params[:q])UsersIndex.search(body: body)Pagination
esse-kaminari
gem 'esse-kaminari' — Kaminari integration. Adds .page(n).per(x) chainable on search queries.
@search = UsersIndex.search(params[:q]).page(params[:page]).per(10)# View<%= paginate @search.paginated_results %>esse-pagy
gem 'esse-pagy' — Pagy integration with controller helpers.
@pagy, @response = pagy_esse(UsersIndex.pagy_search(params[:q]), items: 10)esse-will_paginate
gem 'esse-will_paginate' — WillPaginate integration. Adds .paginate(page:, per_page:) on search queries.
@search = UsersIndex.search(params[:q]).paginate(page: params[:page], per_page: 10)# View<%= will_paginate @search.paginated_results %>Testing
esse-rspec
gem 'esse-rspec' — RSpec matchers and index stubs for testing Esse-backed code without a live Elasticsearch/OpenSearch cluster. Intercepts any Esse::Transport method and returns canned responses or realistic Esse::Transport::* errors. Auto-included on require 'esse/rspec'.
Stub a search and assert on the request body:
expect(ProductsIndex).to esse_receive_request(:search) .with(body: { query: { match_all: {} }, size: 10 }) .and_return('hits' => { 'total' => 0, 'hits' => [] })
ProductsIndex.search(query: { match_all: {} }, size: 10).response.total # => 0.with(...) also accepts any RSpec argument matcher for loose assertions on a subset of the request:
expect(PostsIndex).to esse_receive_request(:search) .with( hash_including( _source: false, body: hash_including('aggregations' => anything), ), ) .and_return('aggregations' => { 'tags' => { 'buckets' => [] } })Simulate an HTTP error — the status code maps to the right Esse::Transport::* subclass:
expect(ProductsIndex).to esse_receive_request(:search) .and_raise_http_status(500, { 'error' => 'Something went wrong' })# raises Esse::Transport::InternalServerError when calledScaffold a disposable Esse::Index scoped to a single example:
before do stub_esse_index('products') do repository :product, const: true do document { |record| { id: record[:id], name: record[:name] } } end endendOther extensions
These extensions are not part of this workspace but are mentioned in the main README:
- esse-redis_storage — Redis-backed storage for long-running state.
Visit the main project for the complete list.
Writing your own
Any extension is just a plugin module (see Plugins). Package it as a gem if you want to share it. The contract is:
- Define a module under
Esse::Plugins::YourName. - Optionally add
apply(index, **opts, &block),configure(index, ...). - Optionally add
IndexClassMethodsandRepositoryClassMethodssubmodules. - Publish with a dependency on
esse>= 0.3.0 (or your required minimum).