TypesenseModel
A Ruby gem that provides seamless Typesense integration for ActiveRecord models with automatic syncing and search capabilities.
Installation
Add to your Gemfile:
gem 'typesense_model'
Or install directly:
$ gem install typesense_model
Configuration
Initialize Typesense connection in your Rails application:
# config/initializers/typesense.rb
TypesenseModel.configure do |config|
config.api_key = 'your_api_key'
config.host = 'localhost'
config.port = 8108
config.protocol = 'http'
end
ActiveRecord Integration
The gem automatically extends ActiveRecord models with Typesense capabilities. Simply add uses_typesense to any model:
class Product < ApplicationRecord
uses_typesense collection: 'products' do
field :id, :string
field :name, :string
field :description, :string, optional: true, index: false
field :price, :float, sort: true
field :categories, "string[]", optional: true, facet: true
field :tags, "string[]", optional: true, facet: true
field :brand, :string, facet: true
field :in_stock, :bool, facet: true
field :created_at, :int64, sort: true
field :updated_at, :int64, sort: true
end
# Optional: Custom JSON serialization for Typesense
def as_json_typesense
{
id: id,
name: name,
description: description,
price: price,
categories: categories,
tags: ,
brand: brand,
in_stock: in_stock,
created_at: created_at.to_i,
updated_at: updated_at.to_i
}
end
end
Features
Automatic Syncing
When you save or destroy ActiveRecord records, they automatically sync to Typesense:
# Create a product - automatically synced to Typesense
product = Product.create!(
name: "iPhone 15",
price: 999.99,
categories: ["Electronics", "Phones"],
brand: "Apple",
in_stock: true
)
# Update a product - automatically synced to Typesense
product.update!(price: 899.99)
# Destroy a product - automatically removed from Typesense
product.destroy!
Search Capabilities
Search your ActiveRecord models using Typesense:
# Basic search
results = Product.search("iPhone")
# Advanced search with filters and sorting
results = Product.search("smartphone",
filter_by: "brand:Apple && price:< 1000",
sort_by: "price:asc",
per_page: 20,
page: 1
)
# Search with facets
results = Product.search("phone")
results.facet_values("brand") # Get brand facet counts
results.facet_values("categories") # Get category facet counts
Access Typesense Documents
Get the Typesense document for any ActiveRecord instance:
product = Product.find(123)
typesense_doc = product.typesense_model
# Returns a TypesenseModel::Base instance with the Typesense document data
Schema Options
Field options available in the schema definition:
optional: true- Field is optionalindex: false- Field is not indexed (not searchable)facet: true- Field can be used for faceted searchsort: true- Field can be used for sortingdefault_sort: true- Field is the default sorting field
Custom JSON Serialization
You can customize how your model data is serialized for Typesense:
class Product < ApplicationRecord
uses_typesense collection: 'products', model_json: :to_typesense_hash do
# schema definition
end
def to_typesense_hash
{
id: id,
name: name,
price: price,
# Add computed fields
searchable_text: "#{name} #{description} #{brand}".downcase,
price_range: case price
when 0..100 then "budget"
when 101..500 then "mid-range"
else "premium"
end
}
end
end
Or use a Proc for dynamic serialization:
class Product < ApplicationRecord
uses_typesense collection: 'products',
model_json: ->(record) { record.as_json.merge(computed_field: record.compute_something) } do
# schema definition
end
end
Collection Management
Create and manage Typesense collections:
# Create the collection in Typesense
Product.search("") # This will create the collection if it doesn't exist
# Or explicitly create/update
proxy = TypesenseModel::ActiveRecordExtension::TypesenseProxy.for(Product)
proxy.create_collection
proxy.update_collection
proxy.delete_collection
Import Existing Data
Import existing ActiveRecord records to Typesense without N+1 queries:
# Basic import (default batch_size: 1000)
results = Product.import_all_to_typesense
# With preloads to avoid N+1
results = Product.import_all_to_typesense(preloads: [:brand, :images])
# With custom transformer and options
results = Product.import_all_to_typesense(
preloads: { variants: [:prices, :stock_items] },
transform: :to_typesense_hash,
batch_size: 2000,
import_options: { action: 'upsert' }
)
# results => { success: 500, failed: 0, errors: [...] }
Advanced search
Highlights, scores and grouped results
search returns a SearchResults that, beyond enumerating records, exposes the
search metadata Typesense returns:
results = Product.search("running shoe")
# Each record paired with its highlights and relevance score
results..each do |hit|
hit[:record] # => Product-like document
hit[:highlights] # => [{ "field" => "title", ... }]
hit[:text_match] # => relevance score
end
# When searching with group_by:
grouped = Product.search("shoe", group_by: "brand")
grouped.grouped_hits.each do |group|
group[:group_key] # => ["Nike"]
group[:hits] # => [records...]
end
Geo and vector search parameters are passed straight through as options, e.g.
Product.search("*", filter_by: "location:(48.8,2.3,5 km)").
Multi-search
Issue several queries in a single request:
results = Product.multi_search([
{ q: "shoe" },
{ q: "boot", filter_by: "price:>100" }
])
results.first.total_hits
Synonyms and overrides (curation)
Product.upsert_synonym("coat-synonyms", synonyms: %w[coat jacket parka])
Product.upsert_override("promote-nike", rule: { query: "shoe", match: "exact" },
includes: [{ id: "1", position: 1 }])
Async syncing
Pass async: true to perform the Typesense write in an ActiveJob instead of
inline in the after_save/after_destroy callbacks:
class Product < ApplicationRecord
uses_typesense collection: "products", async: true do |s|
s.field :id, :string
s.field :title, :string
end
end
Requires ActiveJob; the job (TypesenseModel::SyncJob) reloads the record and
syncs it on whatever queue adapter your app is configured with.
Configuration
Failures in the background sync callbacks are logged rather than raised. By
default they go to Rails.logger (or $stderr outside Rails); override with:
TypesenseModel.logger = MyLogger.new
Development
After checking out the repo, install dependencies and run the test suite:
bundle install
bundle exec rake # runs the unit specs (Typesense client is mocked)
Integration specs talk to a real Typesense server and are skipped by default.
To run them, start a local Typesense instance and set TYPESENSE_INTEGRATION:
TYPESENSE_INTEGRATION=1 TYPESENSE_API_KEY=test-key bundle exec rspec --tag integration
To build the gem locally:
gem build typesense_model.gemspec
License
Available as open source under the MIT License. Copyright (c) 2025 Ruby Dev SRL