Noiseless
Async-first search abstraction for Rails with multi-backend support (OpenSearch, Elasticsearch, Typesense, PostgreSQL).
Features
- Chainable DSL — fluent query builder with runtime validation
- Multi-backend — OpenSearch, Elasticsearch, Typesense, PostgreSQL adapters
- Async-first — built on Ruby 3.4+ fiber scheduler with non-blocking I/O
-
HTTP/2 connection pooling — persistent connections via
Async::Pool - Rails integration — Railtie with log subscriber and controller runtime tracking
- Lazy loading — adapters loaded on-demand, test files excluded from production
Installation
gem "noiseless"noiseless is a Rails gem. It requires Ruby >= 3.4 and Rails >= 8.1.
Configuration
Create config/noiseless.yml:
development:
default: primary
connections:
primary:
adapter: elasticsearch
hosts:
- http://localhost:9201
opensearch:
adapter: open_search
hosts:
- http://localhost:9202
typesense:
adapter: typesense
hosts:
- http://localhost:8109
postgresql:
adapter: postgresql
production:
default: primary
connections:
primary:
adapter: opensearch
hosts:
- <%= ENV['OPENSEARCH_URL'] %>
typesense:
adapter: typesense
hosts:
- <%= ENV['TYPESENSE_URL'] %>
postgresql:
adapter: postgresqlUsage
Defining a Search
class Company::Search < Noiseless::Model
index_name 'companies'
def by_name(name)
multi_match(name, [:name, :name_aliases])
end
def suppliers_only
filter(:company_type, 'supplier')
end
endExecuting Searches
All .execute calls return Async::Task objects. Use Sync to wait for results, or use the _sync convenience methods:
# Convenience method (recommended for simple cases)
results = Company::Search.new.by_name('tech').execute_sync
# Class-level convenience
results = Company::Search.search_sync do |s|
s.match(:name, 'tech')
s.limit(10)
end
# Explicit Sync block
results = Sync do
Company::Search.new
.by_name('technology')
.suppliers_only
.limit(20)
.execute
.wait
endConcurrent Searches
Async do |task|
companies_task = Company::Search.new.match(:name, 'tech').execute
products_task = Product::Search.new.match(:name, 'tech').execute
companies = companies_task.wait
products = products_task.wait
endFor best performance, run independent searches concurrently within a single Async block rather than creating separate Sync blocks per search.
Advanced Queries
results = Company::Search.new
.match(:name, 'electronics')
.filter(:status, 'active')
.geo_distance(:location, lat: 40.7128, lon: -74.0060, distance: '50km')
.sort(:created_at, :desc)
.paginate(page: 1, per_page: 10)
.execute_syncRails Integration
class CompaniesController < ApplicationController
def search
@results = Company::Search.new
.by_name(params[:q])
.limit(20)
.execute_sync
render json: @results
end
endTesting
Add to test/test_helper.rb:
require 'noiseless/test_helper'
require 'noiseless/test_case'With Noiseless::TestCase (automatic VCR cassettes)
class CompanySearchTest < Noiseless::TestCase
def test_search_by_name
# Cassette auto-named: company_search/search_by_name
search = Company::Search.new.by_name('test')
assert_search_results(search)
end
endWith manual VCR control
class CompanySearchTest < ActiveSupport::TestCase
include Noiseless::TestHelper
def test_custom_search
noiseless_cassette(record: :new_episodes) do
results = Company::Search.new.by_name('test').execute_sync
assert results.any?
end
end
endRunning Tests Locally
docker compose up -d postgres elasticsearch opensearch typesense
bin/testbin/test expects all four local services from docker-compose.yml, including PostgreSQL on :5432.
Default ports match docker-compose.yml: PostgreSQL :5432, Elasticsearch :9201, OpenSearch :9202, Typesense :8109. Override via env vars:
ELASTICSEARCH_PORT=9200 OPENSEARCH_PORT=9201 TYPESENSE_PORT=8108 bin/testFor a release smoke test that does not require the dummy app or local services:
bundle exec rake release:checkDebug Mode
ENV['NOISELESS_VERBOSE'] = 'true'Contributing
- Follow Rails conventions for code organization
- Test helpers must remain separate from core functionality
- Add tests for new features using the provided test utilities
License
BSD 3-Clause License — See LICENSE.txt