Warning
⚠️ This project is under maintenance – work in progress. APIs and docs may change. ⚠️
Mountless Rails::Engine for Typesense. Expressive Relation/DSL with JOINs, grouping, presets/curation — with strong DX and observability.
Note
This project is not affiliated with Typesense and is a wrapper for the typesense gem.
Versioning
The gem version mirrors the Typesense server major/minor it targets. Patch releases are reserved for gem-only fixes and enhancements.
Example: 30.1.x targets Typesense 30.1.
Quickstart
# Gemfile
gem "search-engine-for-typesense"# config/initializers/search_engine_for_typesense.rb
SearchEngine.configure do |c|
c.host = ENV.fetch("TYPESENSE_HOST", "localhost")
c.port = 8108
c.protocol = "http"
c.api_key = ENV.fetch("TYPESENSE_API_KEY")
endclass SearchEngine::Product < SearchEngine::Base
collection :products
attribute :id, :integer
attribute :name, :string
query_by %i[name brand description]
end
SearchEngine::Product.where(name: "milk").select(:id, :name).limit(5).to_aSee Quickstart.
Host app SearchEngine models
By default, the gem manages a dedicated Zeitwerk loader for your SearchEngine models under app/search_engine/. The loader is initialized after Rails so that application models/constants are available, auto-reloads in development, and is eager-loaded in production/test.
Customize or disable via configuration:
# config/initializers/search_engine.rb
SearchEngine.configure do |c|
# Relative to Rails.root or absolute; set to nil/false to disable
c.search_engine_models = 'app/search_engine'
endUsage examples
# Model
class SearchEngine::Product < SearchEngine::Base
collection "products"
attribute :id, :integer
attribute :name, :string
end
# Basic query
SearchEngine::Product
.where(name: "milk")
# Explicit query_by always wins over model/global defaults
.options(query_by: 'name,brand')
.select(:id, :name)
.order(price_cents: :asc)
.limit(5)
.to_a
# JOIN + nested selection
SearchEngine::Product
.joins(:brands)
.select(:id, :name, brands: %i[id name])
.where(brands: { name: "Acme" })
.per(10)
.to_a
# Faceting + grouping
rel = SearchEngine::Product
.facet_by(:brand_id, max_values: 5)
.facet_by(:category)
.group_by(:brand_id, limit: 3)
params = rel.to_h # compiled Typesense params
# Multi-search
result_set = SearchEngine.multi_search(common: { query_by: SearchEngine.config.default_query_by }) do |m|
m.add :products, SearchEngine::Product.where("name:~rud").per(10)
m.add :brands, SearchEngine::Brand.all.per(5)
end
result_set[:products].found
# Upserting documents
product_record = Product.first
mapped = SearchEngine::Product.mapped_data_for(product_record)
# Map + upsert a single record
SearchEngine::Product.upsert(record: product_record)
# Upsert already-mapped data
SearchEngine::Product.upsert(data: mapped)
# Bulk upsert records (mapper runs internally)
SearchEngine::Product.upsert_bulk(records: Product.limit(2))
# Bulk upsert mapped payloads
SearchEngine::Product.upsert_bulk(data: [mapped])Documentation
See the Docs
Test/offline mode
In test environments (Rails.env.test? or RACK_ENV=test), SearchEngine defaults to an offline client
(SearchEngine::Test::OfflineClient) so no Typesense HTTP calls are made.
You can control this explicitly with:
-
SEARCH_ENGINE_TEST_MODE=1to force offline mode -
SEARCH_ENGINE_TEST_MODE=0to disable offline mode -
SEARCH_ENGINE_OFFLINE=1(legacy alias)
If you set SearchEngine.configure { |c| c.client = ... }, the custom client is always used.
Example app
See examples/demo_shop — demonstrates single/multi search, JOINs, grouping, presets/curation, and DX/observability. Supports offline mode via the stub client (see Testing).
Contributing
See Docs Style Guide. Follow YARDoc for public APIs, add backlinks on docs landing pages, and redact secrets in examples.