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])
# Geo search
class SearchEngine::Venue < SearchEngine::Base
collection :venues
identify_by :id
attribute :name, :string
attribute :location, :geopoint
end
# Filter by radius
SearchEngine::Venue
.where_geo(:location, within_radius: { lat: 54.69, lng: 25.28, radius: "10 km" })
.order_geo(:location, from: { lat: 54.69, lng: 25.28 })
.to_a
# Filter by polygon (viewport)
SearchEngine::Venue
.where_geo(:location, within_polygon: [[54.72, 25.35], [54.72, 25.22], [54.67, 25.22], [54.67, 25.35]])
.to_a
# Viewport boost with _eval() + distance tiebreaker
SearchEngine::Venue
.order_eval("location:(54.72,25.35, 54.72,25.22, 54.67,25.22, 54.67,25.35)", direction: :desc)
.order_geo(:location, from: { lat: 54.69, lng: 25.28 })
.to_a
# Access geo distance on results (present when order_geo is used)
result = SearchEngine::Venue.all.order_geo(:location, from: { lat: 54.69, lng: 25.28 }).execute
result.hits.first.geo_distance_meters # => { "location" => 1234 }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.