Ruby on Rails
The framework eliminates search backend lock-in by returning hydrated Active Record models across databases and Elasticsearch, while intentionally eschewing automated migrations to avoid cross-engine schema complexity.
AI-assisted summary of Active Search - Donal McBreen — original source. Check the original for full context.
Source published:
Key ideas
- 37signals has introduced Active Search, an alpha-stage full-text search framework for Ruby on Rails that provides a single unified API across relational databases (SQLite, PostgreSQL, MySQL) and dedicated search engines like Elasticsearch. (0:00)
- Already running in production at 37signals, Active Search powers Basecamp on Elasticsearch and allows the open-source Kanban app Fizzy to dynamically toggle between MySQL and SQLite depending on the deployment environment. (0:00)
- Unlike raw search clients that return JSON hashes, Active Search queries hydrate matching document IDs directly into native Active Record model instances while preserving engine-level metadata like relevance scores and highlighted snippets via a `.hit` method. (6:40)
- When using relational database adapters, Active Search generates dedicated, denormalized Active Record document models and migrations—such as SQLite FTS virtual tables or PostgreSQL `tsvector` columns—that support standard multi-database routing and read replicas. (22:45)
Detailed notes
## Background Published on YouTube by the Ruby on Rails channel on October 1, 2026, this presentation features Donal McBreen, a principal programmer at 37signals, introducing Active Search. According to the video description, the talk covers a new full-text search framework for Rails featuring a unified API with pluggable adapters for SQLite, Postgres, MySQL, and Elasticsearch, demonstrating its production usage in Basecamp and Fizzy alongside an end-to-end demo.
## TL;DR In this presentation, Donal McBreen introduces Active Search, a unified full-text search framework for Ruby on Rails that mirrors Active Record by decoupling a single search API from pluggable backend adapters like Elasticsearch, SQLite, PostgreSQL, and MySQL. He walks through index configuration in `config/search.rb`, model integration via the `has_search` macro, and demonstrating how search queries automatically hydrate back into native Active Record instances. The talk also explores polymorphic indexing across multiple models, denormalized SQLite FTS document models, and chainable query filtering. Finally, McBreen details low-level escape hatches like `to_native_query` and backend capability flags, inviting community experimentation with the alpha-stage gem currently running in production at 37signals.
## Atmosphere & style The presentation carries a grounded, pragmatic, and unpretentious tone, opening with a lighthearted remark about delivering an 'AI-free talk' palate cleanser before settling into a developer-focused architectural walkthrough. McBreen maintains a calm, transparent delivery, openly discussing intentional trade-offs—such as skipping automatic cross-engine schema migrations—and acknowledging rough edges in what he candidly frames as alpha software. Watching the talk feels like an insider look into the design of a core Rails primitive, driven by real-world production pragmatism and clean developer ergonomics.
## Bulletin - [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) 37signals has introduced Active Search, an alpha-stage full-text search framework for Ruby on Rails that provides a single unified API across relational databases (SQLite, PostgreSQL, MySQL) and dedicated search engines like Elasticsearch. - [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) Already running in production at 37signals, Active Search powers Basecamp on Elasticsearch and allows the open-source Kanban app Fizzy to dynamically toggle between MySQL and SQLite depending on the deployment environment. - [06:40](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=400s) Unlike raw search clients that return JSON hashes, Active Search queries hydrate matching document IDs directly into native Active Record model instances while preserving engine-level metadata like relevance scores and highlighted snippets via a `.hit` method. - [22:45](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1365s) When using relational database adapters, Active Search generates dedicated, denormalized Active Record document models and migrations—such as SQLite FTS virtual tables or PostgreSQL `tsvector` columns—that support standard multi-database routing and read replicas.
## Key takeaways - [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) Active Search provides a unified Rails search API with pluggable store adapters, sparing developers from writing backend-specific search logic across SQLite, PostgreSQL, MySQL, and Elasticsearch. - [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) 37signals uses Active Search in production to power Basecamp's Elasticsearch infrastructure and to allow their open-source Kanban app, Fizzy, to switch seamlessly between MySQL and SQLite. - [02:37](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=157s) Framework configuration is divided between `config/search.yml` for environment-specific adapter connection settings and `config/search.rb` for defining index schemas with full-text (`text`) and filterable (`string`) fields. - [04:47](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=287s) Active Search deliberately avoids an automated schema migration system for standalone search engines due to cross-engine complexity, meaning schema modifications must be managed manually or by rebuilding the index. - [06:40](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=400s) Querying via `.search` on a model equipped with `has_search` fetches matching document IDs from the search backend and automatically hydrates them into native Active Record model instances rather than raw document hashes. - [08:35](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=515s) Active Search operates through a three-tier pipeline—the Index layer, the Store Adapter, and the underlying Search Engine or Database—while also allowing developers to bypass Active Record to index arbitrary Ruby hashes directly. - [10:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=600s) By default, model lifecycle callbacks dispatch asynchronous Active Job tasks (`ActiveSearch::ReindexJob` and `ActiveSearch::RemoveJob`) to maintain the index, though passing `has_search async: false` runs indexing inline. - [10:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=600s) Custom serializers via methods or inline procs allow developers to transform fields (such as truncating body text), synthesize dynamic attributes, or extract data from associations before indexing. - [15:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=900s) The query API supports chaining `.filter`, `.reject`, `.filter_any`, and custom sort orders, explicitly deferring execution until `.results` is called so hits and metadata are retrieved in a single round-trip. - [15:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=900s) Hydrated model instances returned from a search are equipped with a `.hit` method that exposes engine-level details including relevance scores, match diagnostics, and highlighted text snippets. - [18:22](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1102s) Polymorphic indices add `record_type` and `record_id` fields to aggregate multiple models into a shared index, returning mixed model results when queried via `ActiveSearch.index` or implicitly filtering to a single class when called on an Active Record model. - [22:45](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1365s) Database adapters generate a denormalized Active Record document model (such as `ArticleDocument`) and migration to store serialized filter attributes alongside backend-specific structures like SQLite `_fts` virtual tables, PostgreSQL `tsvector` columns, or MySQL `FULLTEXT` indexes. - [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) For low-level customization, `.native` injects engine-specific ranking or scoring parameters into a query, while `to_native_query` exposes the underlying Elasticsearch hash or joinable `ActiveRecord::Relation`. - [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) Because features vary across engines—for example, native result highlighting is supported in Elasticsearch, PostgreSQL, and SQLite, but unsupported in MySQL—adapters expose a `capabilities` system to signal when applications must handle features manually.
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) Introducing Active Search: A Unified Search Framework for Rails
Donal McBreen, a principal programmer at 37signals, opens with a lighthearted observation about the tech climate: with how "exhausting" it has become to talk to AI tools all day and talk about AI whenever one isn't using them, this presentation serves as a welcome, "AI free talk" palate cleanser. The focus is entirely on Active Search, a new full-text search framework built for Ruby on Rails. Much like core Rails components such as Active Record or Active Job, Active Search provides developers with a single, uniform API backed by pluggable adapters, sparing developers from writing backend-specific search logic across their applications.
The framework supports two distinct categories of storage backends: 1. **Relational databases with built-in full-text search capabilities:** SQLite, PostgreSQL, and MySQL. 2. **Dedicated standalone search engines:** Modern engines such as Elasticsearch and others.
Within 37signals, Active Search is already battle-tested in production. Basecamp relies on it paired with Elasticsearch to power its full-text search infrastructure. Additionally, 37signals uses Active Search inside Fizzy, their free and open-source Kanban application. Because Fizzy ships both as a hosted SaaS product and as a self-hostable standalone application, Active Search allows the app to dynamically toggle between MySQL and SQLite depending on the environment. Developers interested in viewing a real-world integration can inspect Active Search directly within the open-source Fizzy codebase.
```mermaid flowchart TD accTitle: Active Search Architecture Overview accDescr: Active Search sits between Rails models and pluggable storage backends divided into databases and dedicated search engines.
subgraph RailsApp["Rails Application Layer"] Model["Active Record Model\n(e.g., Article)"] ASAPI["Active Search API\n(has_search, search)"] end
subgraph Adapters["Active Search Store Adapters"] DBAdapters["Database Adapters\n(SQLite, Postgres, MySQL)"] EngineAdapters["Engine Adapters\n(Elasticsearch, etc.)"] end
Model --> ASAPI ASAPI --> DBAdapters ASAPI --> EngineAdapters ```
[02:37](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=157s) Installation and Configuring the Search Engine
To illustrate how Active Search operates, McBreen walks through a demo scenario built around a classic schema: an `articles` table associated with a `comments` table. In typical search implementations, attributes fall into two buckets: full-text searchable fields where users search for arbitrary words (here, `title` and `body`), and structured metadata fields used to narrow down matches (such as publication dates or authorship).
Getting started requires installing the Active Search gem and running its installation generator:
```bash bin/rails active_search:install ```
For the initial walkthrough, McBreen configures the Elasticsearch adapter (noting that database adapters function similarly with minor differences explained later). The install generator creates two core configuration files: - `config/search.yml`: Defines environment-specific settings (development, test, production), specifying the target adapter along with connection parameters. While production deployments specify hostnames and custom ports, it defaults out of the box to `localhost:9200` for Elasticsearch. - `config/search.rb`: The central DSL file where search index schemas are defined. It starts essentially empty, containing only commented boilerplate illustrating expected declarations.
[04:47](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=287s) Defining Index Schemas and Model Integration
To index the `Article` model, Active Search provides a dedicated generator that accepts model attributes and their intended search roles. Fields passed without an explicit type (such as `title` and `body`) default to full-text search attributes, whereas filterable attributes are explicitly designated (for instance, allowing users to restrict queries to articles published within the last month).
Running the generator modifies both the configuration and the model: - In `config/search.rb`, the generator appends an index definition. The schema differentiates between Active Search types: `text` fields are indexed for full-text token matching, while `string` fields are preserved as exact values for filtering. - In `app/models/article.rb`, the generator injects the `has_search` macro.
The `has_search` declaration equips the Active Record model with essential search machinery: it adds class-level querying methods, instance helper methods, and model lifecycle callbacks (`after_save`, `after_destroy`) to automatically push record changes to the search index.
Once declared, the physical index must be provisioned on the backend using a rake task:
```bash bin/rails active_search:index:create index=articles ```
The output confirms the schema fields and the active adapter. Querying the Elasticsearch HTTP mapping API directly reveals the generated Elasticsearch schema matching the Rails declaration. McBreen highlights an intentional design tradeoff: Active Search deliberately avoids providing an automated schema migration system analogous to Active Record migrations. Supporting cross-engine schema modifications across diverse search backends would introduce immense complexity. If an index definition changes later, altering existing fields must currently be managed manually or by rebuilding the index.
[06:40](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=400s) Indexing Records, Querying, and Result Hydration
With the index created, creating sample records in Rails automatically triggers Active Search callbacks, formatting the records and transmitting them to the configured backend.
Searching the index is invoked directly on the model via the class method injected by `has_search`:
```ruby results = Article.search("rails") ```
The returned `results` object is an enumerable collection with a crucial architectural property: **it yields native Active Record model instances, not raw search engine JSON or document hashes**. Under the hood, Active Search executes the query against Elasticsearch to identify matching document IDs, fetches the corresponding rows from the database, and hydrates them into standard `Article` model objects. Iterating over the collection (e.g., mapping article titles) yields standard Rails models containing all database attributes, retaining typical Rails behavior throughout application views and controllers.
[08:35](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=515s) Architecture: Indexes, Store Adapters, and Engine Communication
The operational architecture of Active Search is organized around a clean three-tier pipeline:
```mermaid flowchart LR accTitle: Active Search Data Flow accDescr: Demonstrates index writing and querying flows between the model, Active Search index, store adapter, and search engine.
subgraph Rails["Rails Application"] Model["Article Record"] end
subgraph ActiveSearchLayer["Active Search Framework"] Idx["Index\n(:articles)"] Store["Store Adapter\n(Elasticsearch Adapter)"] end
subgraph Backend["External Store"] Engine["Search Engine\n(Elasticsearch 9200)"] end
Model -->|"1. Save Callback / Query"| Idx Idx -->|"2. Transform"| Store Store -->|"3. HTTP / SQL"| Engine Engine -->|"4. Raw Results"| Store Store -->|"5. IDs / Hits"| Idx Idx -->|"6. Hydrated Models"| Model ```
1. **The Index Layer:** By default, named after the model's underlying database table (e.g., `:articles`). It defines what fields exist and acts as the entry point. 2. **The Store Adapter:** A dedicated bridge adapter tailored to the specific backend (such as Elasticsearch, PostgreSQL, or SQLite). 3. **The Search Engine / Database:** The actual underlying storage handling the inverted index or database full-text catalog.
When an article record is saved, the model delegates to the index, the index hands structured data to the adapter, and the adapter issues the appropriate driver command or HTTP payload to the search engine. When querying, the process reverses: a search term travels down through the adapter to the search engine, raw hits return up the pipeline, and Active Search queries the primary database to hydrate those IDs into full Active Record instances.
Developers can also inspect the index directly in the Rails console via `ActiveSearch.index(:articles)`, exposing methods to interrogate the index name, underlying search engine naming conventions, and the active store adapter instance.
[10:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=600s) Direct Index Access, Async Callbacks, and Document Serialization
Active Search models expose direct index access, meaning developers can bypass ActiveRecord integration entirely if needed. One can talk directly to an index object to add articles, remove articles, or call `remove_by_id` (especially useful when a model instance has already been destroyed in the database and is no longer available in memory). Raw searches can also be executed directly against the index. While the standard Rails pattern relies on model integration handling communication behind the scenes, using Active Search standalone allows developers to index arbitrary data structures—such as standalone Ruby hashes—without backing ActiveRecord tables.
When models are integrated via the `has_search` macro, lifecycle callbacks are hooked into model creation, updates, and destruction. These callbacks dispatch background jobs (such as `ActiveSearch::ReindexJob` and `ActiveSearch::RemoveJob`) to perform asynchronous indexing. For workflows requiring immediate consistency, passing `has_search async: false` bypasses the background queues and runs the indexing inline within the application process.
```mermaid flowchart TD accTitle: Active Search Indexing Pipeline accDescr: Demonstrates how an article moves from ActiveRecord save to jobs, index conversion, and final storage engine. subgraph App["Application Layer"] A["Save Article"] --> B["ReindexJob"] end subgraph IndexLayer["Active Search"] B --> C["Index"] C --> D["Document (Hash: id + data)"] end subgraph Storage["Search Engines"] D --> E["Elasticsearch (JSON blob)"] D --> F["SQLite / MySQL (SQL query)"] end ```
Under the hood, the index serializes an ActiveRecord model into an internal `Document` format by calling the `document_for` method. This simple data payload contains an identifier alongside a hash of mapped fields matching the index definition. By default, Active Search expects model methods corresponding to field names (e.g., calling `article.title` and `article.body`). If field names diverge, or if data must be pulled from associations, developers can define a custom serializer via a dedicated method or an inline proc:
- Custom serializers allow transforming fields before indexing, such as truncating long body text to prevent index bloat. - Serializers can synthesize fields dynamically (e.g., inferring a `status` field from a `published_at` timestamp). - The generated document hash is forwarded directly to the backend store, transforming into an Elasticsearch JSON blob or an insert/upsert query in SQLite or MySQL.
---
[15:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=900s) Chainable Search API, the `results` Method, and Hit Metadata
Searching Active Search indices mimics the chainable design of ActiveRecord relations. Starting a query with `Article.search("rails")` returns a composable query object. Additional constraints like `.filter(...)` or custom sort orders return new query objects containing all cumulative criteria, defaulting to relevance ordering when unspecified:
- Queries remain chainable across multiple criteria and return query objects at each step until evaluated. - Filtering supports multi-field criteria, multiple values, and both open-ended and closed ranges (ideal for filtering by publication windows like recently published articles).
``` # Example of the chainable search API flow Article.search("rails") .filter(status::published, published_at: 1.month.ago..) .results ```
Execution is triggered explicitly by calling `.results`. Rather than immediately iterating via `article.search.each`, Active Search separates query construction from execution to prevent accidental repeated searches. Evaluating the search only via `.results` ensures that metadata—such as total result counts, partial execution warnings, and execution duration—is fetched in a single round-trip without running redundant performance-degrading queries. The returned results object is fully `Enumerable`, converting smoothly to an array of model instances.
Calling `has_search` on a model also equips instances with a `hit` method to store search-specific context. When results return from the underlying search engine, the associated `hit` object exposes engine-level details that do not exist on the database record:
- Underlying relevance scores calculated by the engine. - Raw engine fields and match diagnostics. - Highlighted snippets indicating exact word matches within the document text.
---
[18:22](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1102s) Polymorphic Searching Across Multiple Models
When an application contains diverse searchable content—such as searching both articles and comments simultaneously—Active Search provides polymorphic indices. Rather than constraining an index to a single record type, a generic index (such as `records`) can be defined with universal search fields like `title` and `body`.
Declaring an index as polymorphic automatically adds `record_type` and `record_id` fields to the search schema. These composite fields serve two primary functions: 1. They map search hits back to their original ActiveRecord models across different tables. 2. They allow queries to be scoped or filtered by model type directly within the search backend.
Models are not restricted to a single search destination. An article model can declare multiple `has_search` definitions simultaneously. By default, an index name is inferred from the model name (e.g., routing an article to an `articles` index), but additional declarations can explicitly target shared indexes like `records`, enabling records to exist in both specialized single-table indices and aggregated multi-model search pools.
[20:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1200s) Polymorphic Indexing and Searching Across Models
When configuring callbacks across multiple indices, saving or updating a model can fan out across different search pipelines. In the polymorphic setup Donal demonstrates, creating or updating an article triggers callbacks that generate two separate indexing jobs: one to populate the dedicated `articles` index and another to push the record into the broader `records` index. Comments follow a similar pattern, but with a twist: because comments do not have their own standalone title attribute, they utilize a custom serializer that extracts and synthesizes a title directly from the parent article. Donal notes a practical edge case in production: developers must ensure that updating an article also triggers a re-index of all its associated comments, though he skips the exact re-indexing implementation details for the demo.
Once a polymorphic index exists, there are two distinct ways to query it:
```mermaid flowchart TD accTitle: Polymorphic Search Query Paths accDescr: Diagram showing how a polymorphic index returns mixed records via ActiveSearch directly versus single-model records via the model search method.
subgraph Query["Query Methods"] Q1["ActiveSearch.index(:records).search('...')"] Q2["Article.search('...', index::records)"] end
subgraph Index["Polymorphic Index (:records)"] R1["Articles"] R2["Comments"] end
subgraph Results["Search Results"] Res1["Mixed Results: Articles & Comments"] Res2["Filtered Results: Articles Only<br/>(Hidden filter: record_type = 'Article')"] end
Q1 --> Index Index --> Res1
Q2 --> Index Index -->|"implicit filter"| Res2 ```
Querying the polymorphic index directly via `ActiveSearch.index(:records).search(...)` reaches into the shared store and returns a heterogeneous collection mixing both articles and comments. Alternatively, calling `.search` directly on an Active Record model—such as `Article.search("...", index::records)`—defaults to scoping results to that specific class. Under the hood, Active Search injects an invisible filter equivalent to `.filter(record_type: "Article")`. This allows developers to query against a massive multi-model index while letting the caller "basically pretend there's just articles in our index."
- **Dual-job callbacks**: Saving a record can dispatch distinct Active Job tasks for each target index simultaneously. - **Custom serializers**: Useful for indexing calculated attributes or values belonging to associations, such as pulling an article's title into a comment search record. - **Direct index access**: `ActiveSearch.index(:records)` returns the underlying search index handle. - **Implicit model scoping**: Searching through a model wrapper transparently narrows the query so caller code only ever receives instances of that model back.
---
[22:45](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1365s) Database Adapters: SQLite Full-Text Search and Denormalized Document Models
Switching from dedicated search engines like Elasticsearch to database-backed adapters involves a different architectural approach. When running the installer configured for SQLite, Active Search creates a minimal `search.yml` declaring `adapter: sqlite`, which references standard connection definitions from `database.yml`. However, generating an index for SQLite does something extra beyond updating `config/search.rb` and injecting `has_search` into the model: it generates a dedicated Active Record document model (such as `ArticleDocument`) and a companion database migration.
```mermaid flowchart LR accTitle: SQLite Active Search Document Model Architecture accDescr: Diagram showing how Article maps to an ArticleDocument backed by a regular table and an FTS virtual table.
A["Article Model"] -->|"has_search"| AD["ArticleDocument Model<br/>(Subclasses ApplicationRecord)"]
subgraph Storage["SQLite Database"] T1["article_documents Table<br/>(article_id, status, views, published_at)"] T2["article_documents_fts<br/>(Virtual Table: title, body)"] end
AD --> T1 AD --> T2 ```
In the database adapter paradigm, the `ArticleDocument` model represents the search index itself. Because it inherits directly from `ApplicationRecord`, developers can leverage all standard Active Record functionality, including custom connection routing, read replicas, or moving search indexes to dedicated secondary databases. In SQLite, the migration creates an `article_documents` table to store metadata and filtering attributes (`article_id`, `status`, `views`, `published_at`), alongside a companion `article_documents_fts` virtual table specifically for full-text search on `title` and `body`. Donal explains that this requires denormalizing data: because Active Search allows arbitrary transformations and custom serializers, the framework cannot work backwards to inspect original source tables when filtering, making a dedicated denormalized document table necessary.
- **SQLite configuration**: Minimal declaration in `search.yml` automatically ties into Rails' existing `database.yml`. - **Generated document model**: Named by convention with a `Document` suffix (e.g., `ArticleDocument`) and acts as the interface to the underlying database index. - **Full Active Record power**: Document models can utilize replicas and multi-database configurations out of the box. - **SQLite virtual tables**: Uses SQLite's FTS engine (`_fts` virtual table) for high-performance text matching on unstructured text fields like titles and bodies. - **Necessity of denormalization**: Since indexed data may be calculated, formatted, or derived from arbitrary serializers, the search table must duplicate those values to allow indexing and filtering.
---
[25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) Engine Differences, Escape Hatches, Capabilities, and Open-Source Roadmap
Database search implementations vary significantly across backends. While SQLite relies on dedicated FTS virtual tables, PostgreSQL retains search columns in the main table and supplements them with `tsvector` columns and specialized indexes. MySQL represents the simplest pattern, maintaining standard columns with native `FULLTEXT` indexing.
Beyond core text search, Active Search provides rich querying primitives such as `.reject` (negated filtering) and `.filter_any` (OR-style conjunctions). For power users needing access to raw features, the framework includes two critical escape hatches:
```mermaid flowchart TD accTitle: Active Search Escape Hatches accDescr: Diagram illustrating the escape hatches to_native_query and native for low-level search engine integration.
Query["Active Search Query"]
Query -->|"to_native_query"| RawQuery["Inspect Raw Query"] Query -->|"native"| ModifyEngine["Low-Level Engine Modification"]
RawQuery --> ES_Q["Elasticsearch: Raw Query Hash / JSON"] RawQuery --> SQL_Q["SQLite: ActiveRecord Relation (joinable!)"]
ModifyEngine --> LowLevel["Custom scoring, boosts, custom ranking logic"] ```
Calling `to_native_query` exposes the internal query payload—returning a JSON-ready Ruby hash when running on Elasticsearch, or an actual `ActiveRecord::Relation` when backed by SQLite, enabling developers to "do crazy things like just join to that or just plug it into Active Record as you want." Meanwhile, the `.native` method allows passing low-level engine parameters directly into the query, such as platform-specific custom ranking or relevance scoring.
To handle disparities across engines without breaking compatibility, Active Search introduces a `capabilities` system. Unlike traditional relational databases where Active Record features are relatively uniform across engines, search backends differ wildly. A prime example is result highlighting: marking matching search keywords directly within returned text is supported out of the box in Elasticsearch, PostgreSQL, and SQLite, but is unsupported in MySQL. Active Search adapters use capabilities flags to declare whether a given feature is supported natively, signaling to developers when they need to handle tasks (such as string highlighting) manually in application code.
Donal wraps up by reiterating how simple the core API remains: declare `has_search` in the model and call `.search` to fetch results. He notes that the gem is currently hosted at `github.com/basecamp/rails/active_search` and can be seen operating in production within the open-source Fizzy Kanban application (`Basecamp/fizzy`). He warns the audience that the library is still "sort of alpha-ish software" whose API is not yet set in stone, and invites the Rails community to try it out and share their feedback.
- **PostgreSQL implementation**: Generates `tsvector` columns paired with GIN/GiST indexes. - **MySQL implementation**: Uses native `FULLTEXT` indexes directly over existing text columns. - **Escape hatch `to_native_query`**: - Elasticsearch returns the raw hash used to build search JSON. - SQLite returns an `ActiveRecord::Relation`, allowing direct SQL joins and scopes. - **Escape hatch `native`**: Injects custom engine-specific configuration, scoring algorithms, and sorting logic into the query. - **Capabilities matrix**: Declares backend feature availability; highlighting is natively supported on SQLite, PostgreSQL, and Elasticsearch, but unsupported on MySQL. - **Project status**: Alpha-stage gem under active development in the Basecamp GitHub organization, running in production on Fizzy.
## Notable quotes > [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) exhausting — Donal McBreen
> [00:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=0s) AI free talk — Donal McBreen
> [04:47](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=287s) has_search — Donal McBreen
> [06:40](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=400s) results — Donal McBreen
> [10:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=600s) has_search async: false — Donal McBreen
> [15:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=900s) hit — Donal McBreen
> [20:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1200s) basically pretend there's just articles in our index. — Donal McBreen
> [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) do crazy things like just join to that or just plug it into Active Record as you want. — Donal McBreen
> [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) to_native_query — Donal McBreen
> [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) native — Donal McBreen
> [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) capabilities — Donal McBreen
> [25:00](https://www.youtube.com/watch?v=OlzIyVzmMgI&t=1500s) sort of alpha-ish software — Donal McBreen
## Mentioned Donal McBreen, 37signals, Active Search, Ruby on Rails, Active Record, Active Job, SQLite, Postgres, MySQL, Elasticsearch, Basecamp, Fizzy, Shopify, GitHub
## Video description Donal McBreen, principal programmer at 37signals, introduces Active Search, a new full-text search framework for Rails. Like Active Record or Active Job, it gives you a single API with pluggable adapters. It supports databases with full-text features (SQLite, Postgres, and MySQL) as well as dedicated search engines such as Elasticsearch.
Donal shows how 37signals uses Active Search in production. Basecamp runs it on Elasticsearch, while Fizzy, the free and open-source Kanban app, uses it to switch between MySQL and SQLite. He then walks through a demo app to show how Active Search works, from installation and configuration to searching full-text fields and filtering results.
#RailsWorld #RubyOnRails #Rails #ActiveSearch #FullTextSearch #37signals
Thank you Shopify for sponsoring the editing and post-production of these videos. Check out insights from the Engineering team at: https://shopify.engineering/
[Source](https://www.youtube.com/watch?v=OlzIyVzmMgI)