Async-first search abstraction for Ruby/Rails with multi-backend support (OpenSearch, Elasticsearch, Typesense, PostgreSQL).
- 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
gem 'noiseless'Requires Ruby >= 3.4 and Rails >= 8.1.
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: postgresqlclass 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
endAll .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
endAsync 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.
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_syncclass CompaniesController < ApplicationController
def search
@results = Company::Search.new
.by_name(params[:q])
.limit(20)
.execute_sync
render json: @results
end
endAdd to test/test_helper.rb:
require 'noiseless/test_helper'
require 'noiseless/test_case'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
endclass 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
enddocker compose up -d
bin/testDefault ports match docker-compose.yml: Elasticsearch :9201, OpenSearch :9202, Typesense :8109. Override via env vars:
ELASTICSEARCH_PORT=9200 OPENSEARCH_PORT=9201 TYPESENSE_PORT=8108 bin/testENV['NOISELESS_VERBOSE'] = 'true'- Follow Rails conventions for code organization
- Test helpers must remain separate from core functionality
- Add tests for new features using the provided test utilities
BSD 3-Clause License — See LICENSE.txt