Searchkick Documentation

repository·master·Indexed 27 days ago

https://github.com/ankane/searchkick

An intelligent search engine for Ruby applications that provides a developer-friendly interface for Elasticsearch and OpenSearch. It integrates with Active Record and Mongoid to simplify complex search features including stemming, misspellings, synonyms, and boosting. Supports Elasticsearch 8/9 and OpenSearch 2/3 (version 5.5.2 for older versions).

Tokens
11.5K
Snippets
40
Records
73
Agent score
92%

What's inside Searchkick

  1. Configure indexing sync strategies

    master

    Searchkick provides four strategies for keeping your index in sync with your database:

    1. Inline (default): Updates are performed immediately during record insertion, update, or deletion.
    2. Asynchronous: Uses background jobs for better performance. Add searchkick callbacks: :async to your model. Jobs are added to a queue named searchkick.
    3. Queuing: Pushes record IDs to a queue to be reindexed in batches in the background. This is more performant than the asynchronous method.
    4. Manual: Disables automatic syncing with searchkick callbacks: false. You must then call .reindex on records or relations manually.

    You can also override a specific instance's strategy using reindex(mode: :async) or reindex(mode: :queue).

    # Asynchronous strategy
    class Product < ApplicationRecord
      searchkick callbacks: :async
    end
    
    # Manual strategy
    class Product < ApplicationRecord
      searchkick callbacks: false
    end
    
    # Manual reindexing
    product.reindex
    # or
    store.products.reindex(mode: :async)
    
    # Bulk updates
    Searchkick.callbacks(:bulk) do
      Product.find_each(&:update_fields)
    end
    
    # Temporarily skip updates
    Searchkick.callbacks(false) do
      Product.find_each(&:update_fields)
    end
  2. Sync associations with callbacks

    master

    Data in associations is not automatically synced when the association is updated. To ensure changes in an associated model trigger a reindex of the parent, add an after_commit callback to the child model.

    class Image < ApplicationRecord
      belongs_to :product
    
      after_commit :reindex_product
    
      def reindex_product
        product.reindex
      end
    end
  3. Support Single Table Inheritance (STI)

    master

    Searchkick supports STI by setting inheritance: true on the parent model. This allows you to search across all types or filter by specific subclasses using the .type() method.

    Note: This relies on an automatic type field. Avoid defining your own type field in search_data to prevent conflicts.

    class Animal < ApplicationRecord
      searchkick inheritance: true
    end
    
    class Dog < Animal
    end
    
    # Searching
    Animal.search("*")                # all animals
    Dog.search("*")                   # just dogs
    Animal.search("*").type(Cat, Dog) # just cats and dogs
  4. Control which records are indexed with `should_index?`

    master

    By default, all records are indexed. To restrict indexing to specific records (e.g., only active records), implement the should_index? method. This is also useful for excluding records that are filtered out by a default_scope.

    class Product < ApplicationRecord
      # Only index active records
      def should_index?
        active
      end
    end
    
    # If you have a default_scope that filters records, use should_index? to exclude them:
    class Product < ApplicationRecord
      default_scope { where(deleted_at: nil) }
    
      def should_index?
        deleted_at.nil?
      end
    end
  5. Implement Semantic and Hybrid Search

    master

    Semantic search uses kNN to find results based on vector embeddings. Hybrid search combines keyword search and semantic search in parallel.

    • Hybrid Search (RRF): Use Searchkick::Reranking.rrf(keyword_search, semantic_search) to combine results using Reciprocal Rank Fusion.
    • Hybrid Search (Reranking Model): Manually combine results and pass them through a reranking model (e.g., using Informers).
  6. Queue updates for bulk reindexing

    master

    To improve performance, push record IDs to a queue for bulk reindexing.

    1. Set up Redis in an initializer (using connection_pool is recommended): Searchkick.redis = ConnectionPool.new { Redis.new }.
    2. Enable queuing in your model: searchkick callbacks: :queue.
    3. Run the background job: Searchkick::ProcessQueueJob.perform_later(class_name: "Product").
    4. Check queue length: Product.search_index.reindex_queue.length.
    # Initializer
    Searchkick.redis = ConnectionPool.new { Redis.new }
    
    # Model
    class Product < ApplicationRecord
      searchkick callbacks: :queue
    end
    
    # Background Job
    Searchkick::ProcessQueueJob.perform_later(class_name: "Product")
  7. Debug search queries and analyzers

    master

    Use the following methods to debug how Searchkick processes queries:

    • .debug: Prints query information to stdout.
    • .explain.response: Shows how the search server scores queries.
    • .search_index.tokens(text, analyzer: "..."): Shows how a specific string is tokenized by a specific analyzer (e.g., searchkick_index, searchkick_search, searchkick_word_start_index).
    # Debugging
    Product.search("soap").debug
    Product.search("soap").explain.response
    
    # Tokenization inspection
    Product.search_index.tokens("San Diego", analyzer: "searchkick_word_start_index")
  8. Configure and use text highlighting

    master

    To enable highlighting, specify the fields to index in your model using the highlight option. You can then trigger highlighting in your search queries and retrieve the highlighted snippets using with_highlights.

    Key features:

    • Custom Tags: Use highlight(tag: "<strong|") to change the HTML tag.
    • Field Selection: Use .fields(:name) to search specific fields and .highlight(fields: [:description]) to highlight others.
    • Snippets: Use fragment_size to get small snippets instead of the full field.
    • Multiple Highlights: Use with_highlights(multiple: true) to handle multiple fragments per field.
    # Model configuration
    class Band < ApplicationRecord
      searchkick highlight: [:name]
    end
    
    # Searching and retrieving highlights
    bands = Band.search("cinema").highlight
    bands.with_highlights.each do |band, highlights|
      highlights[:name] # "Two Door <em>Cinema</em> Club"
    end
    
    # Customizing highlights
    Band.search("cinema").highlight(tag: "<strong>")
    Band.search("cinema").fields(:name).highlight(fields: {name: {fragment_size: 200}})
  9. Upgrade to Searchkick 6.0

    master

    Searchkick 6 introduces a new query builder API and improved conversion performance. You can use the new method-based syntax or continue using the old API. To upgrade conversions without downtime, follow the dual-field pattern.

    # New Query Builder API
    Product.search("apples").where(in_stock: true).limit(10).offset(50)
    
    # Zero-downtime conversion upgrade pattern
    class Product < ApplicationRecord
      searchkick conversions: [:conversions], conversions_v2: [:conversions_v2]
    
      def search_data
        conversions = searches.group(:query).distinct.count(:user_id)
        {
          conversions: conversions,
          conversions_v2: conversions
        }
      end
    end
    
    # After reindexing, remove the old 'conversions' key
  10. Configure indexing with `search_data`

    master

    Use the search_data method in your model to define which attributes and associations should be indexed. After modifying this method, you must call ModelName.reindex to update the index.

    To optimize the import process and eager load associations, define a search_import scope.

    class Product < ApplicationRecord
      belongs_to :department
    
      def search_data
        {
          name: name,
          department_name: department.name,
          on_sale: sale_price.present?
        }
      end
    end
    
    # To eager load associations during reindexing:
    class Product < ApplicationRecord
      scope :search_import, -> { includes(:department) }
    end
  11. Configure Pagination

    master

    Searchkick integrates with kaminari and will_paginate. In your controller, use .page() and .per_page() to handle pagination.

    # Controller
    @products = Product.search("milk").page(params[:page]).per_page(20)

    In your views:

    <%# Using kaminari %>
    <%= paginate @products %>
    
    <%# Using will_paginate %>
    <%= will_paginate @products %>
    @products = Product.search("milk").page(params[:page]).per_page(20)