0.0
No release in over 3 years
Simple, Elastic-quality search for Postgres via ParadeDB and ActiveRecord.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 1.5
~> 8.1
 Project Readme

rails-paradedb

Gem Version CI License

ActiveRecord integration for ParadeDB: BM25 full-text search, scoring, snippets, facets, and aggregations in PostgreSQL.

ParadeDB docs: https://docs.paradedb.com

Requirements

  • Ruby 3.2+
  • Rails 7.2+
  • PostgreSQL 17+ with pg_search (ParadeDB)

Installation

gem "rails-paradedb"
bundle install

Quick Start

class Product < ApplicationRecord
  include ParadeDB::Model
end
Product.search(:description).matching_all("running shoes")
Product.search(:description).matching_any("wireless", "bluetooth")
Product.search(:description).term("electronics")

Index Definition

class ProductIndex < ParadeDB::Index
  self.table_name = :products
  self.key_field = :id
  self.fields = {
    id: {},
    description: {
      tokenizers: [
        { tokenizer: :literal },
        { tokenizer: :simple, alias: "description_simple", filters: [:lowercase] }
      ]
    },
    category: { tokenizer: :literal },
    rating: {}
  }
end

Create in migration:

class AddProductBm25Index < ActiveRecord::Migration[8.1]
  def up
    create_paradedb_index(ProductIndex, if_not_exists: true)
  end

  def down
    remove_bm25_index :products, name: :products_bm25_idx, if_exists: true
  end
end

Query API

# Full-text
Product.search(:description).matching_all("running shoes")
Product.search(:description).matching_any("wireless bluetooth")

# Query-time tokenizer override
Product.search(:description).matching_any("running shoes", tokenizer: "whitespace")
Product.search(:description).matching_any("running shoes", tokenizer: "whitespace('lowercase=false')")

# Fuzzy options on match/term
Product.search(:description).matching_any("runing shose", distance: 1)
Product.search(:description).matching_all("runing", distance: 1, prefix: true)
Product.search(:description).term("shose", distance: 1, transposition_cost_one: true)

# Other query types
Product.search(:description).phrase("running shoes", slop: 2)
Product.search(:description).regex("run.*")
Product.search(:description).near("running", "shoes", distance: 3)
Product.search(:description).phrase_prefix("run", "sh")
Product.search(:description).phrase_prefix("run", "sh", max_expansion: 100)
Product.search(:description).parse("running AND shoes", lenient: true)
Product.search(:description).parse("running shoes", conjunction_mode: true)

Product.search(:id).match_all
Product.search(:id).exists
Product.search(:rating).range(gte: 3, lt: 5)

Product.more_like_this(42, fields: [:description])

Scoring and Highlighting

results = Product.search(:description)
                 .matching_all("shoes")
                 .with_score
                 .order(search_score: :desc)

Product.search(:description)
       .matching_all("shoes")
       .with_snippet(:description, start_tag: "<b>", end_tag: "</b>", max_chars: 80)

Product.search(:description)
       .matching_all("running")
       .with_snippets(:description, max_chars: 15, limit: 2, offset: 0, sort_by: :position)

Product.search(:description)
       .matching_all("running")
       .with_snippet_positions(:description)

Facets and Aggregations

# Rows + facets (requires order + limit)
relation = Product.search(:description)
                  .matching_all("shoes")
                  .with_facets(:category, size: 10)
                  .order(:id)
                  .limit(10)
rows = relation.to_a
facets = relation.facets

# Non-exact window facets
relation = Product.search(:description)
                  .matching_all("shoes")
                  .with_facets(:category, size: 10, exact: false)
                  .order(:id)
                  .limit(10)

# Facets-only aggregate
Product.search(:description).matching_all("shoes").facets(:category)

# Named aggregations
Product.search(:description).matching_all("shoes").facets_agg(
  docs: ParadeDB::Aggregations.value_count(:id),
  avg_rating: ParadeDB::Aggregations.avg(:rating)
)

# Non-exact window named aggregations
Product.search(:description).matching_all("shoes").with_agg(
  exact: false,
  docs: ParadeDB::Aggregations.value_count(:id)
).order(:id).limit(10)

Diagnostics Helpers

Ruby helpers:

ParadeDB.paradedb_indexes
ParadeDB.paradedb_index_segments("products_bm25_idx")
ParadeDB.paradedb_verify_index("products_bm25_idx", sample_rate: 0.1)
ParadeDB.paradedb_verify_all_indexes(index_pattern: "products_bm25_idx")

Rake tasks:

rake paradedb:diagnostics:indexes
rake "paradedb:diagnostics:index_segments[products_bm25_idx]"
rake "paradedb:diagnostics:verify_index[products_bm25_idx]" SAMPLE_RATE=0.1
rake paradedb:diagnostics:verify_all_indexes INDEX_PATTERN=products_bm25_idx

Note: availability depends on your installed pg_search version.

Examples

Contributing

See CONTRIBUTING.md.

License

MIT