• Stars
    star
    533
  • Rank 83,238 (Top 2 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 11 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Searchlight helps you build searches from options via Ruby methods that you write.

Searchlight

Status

I consider searchlight "done". It has no production dependencies, so there's no reason it shouldn't work indefinitely. I've also moved on to other things.

If you find a bug, feel free to open an issue so others can find it and discuss, but I'm unlikely to respond personally. If Searchlight doesn't meet your needs anymore, fork away! :)

Description

Searchlight is a low-magic way to build database searches using an ORM.

Searchlight can work with any ORM or object that can build a query using chained method calls (eg, ActiveRecord's .where(...).where(...).limit(...), or similar chains with Sequel, Mongoid, etc).

Gem Version Code Climate Build Status

Getting Started

A demo app and the code for that app are available to help you get started.

Overview

Searchlight's main use is to support search forms in web applications.

Searchlight doesn't write queries for you. What it does do is:

  • Give you an object with which you can build a search form (eg, using form_for in Rails)
  • Give you a sensible place to put your query logic
  • Decide which parts of the search to run based on what the user submitted (eg, if they didn't fill in a "first name", don't do the WHERE first_name = part)

For example, if you have a Searchlight search class called YetiSearch, and you instantiate it like this:

  search = YetiSearch.new(
    # or params[:yeti_search]
    "active" => true, "name" => "Jimmy", "location_in" => %w[NY LA]
  )

... calling search.results will build a search by calling the methods search_active, search_name, and search_location_in on your YetiSearch, assuming that you've defined them. (If you do it again but omit "name", it won't call search_name.)

The results method will then return the return value of the last search method. If you're using ActiveRecord, this would be an ActiveRecord::Relation, and you can then call each to loop through the results, to_sql to get the generated query, etc.

Usage

Search class

A search class has two main parts: a base_query and some search_ methods. For example:

class PersonSearch < Searchlight::Search

  # This is the starting point for any chaining we do, and it's what
  # will be returned if no search options are passed.
  # In this case, it's an ActiveRecord model.
  def base_query
    Person.all # or `.scoped` for ActiveRecord 3
  end

  # A search method.
  def search_first_name
    # If `"first_name"` was the first key in the options_hash,
    # `query` here will be the base query, namely, `Person.all`.
    query.where(first_name: options[:first_name])
  end

  # Another search method.
  def search_last_name
    # If `"last_name"` was the second key in the options_hash,
    # `query` here will be whatever `search_first_name` returned.
    query.where(last_name: last_name)
  end
end

Calling PersonSearch.new("first_name" => "Gregor", "last_name" => "Mendel").results would run Person.all.where(first_name: "Gregor").where(last_name: "Mendel") and return the resulting ActiveRecord::Relation. If you omitted the last_name option, or provided "last_name" => "", the second where would not be added.

Here's a fuller example search class. Note that because Searchlight doesn't write queries for you, you're free to do anything your ORM supports. (See spec/support/book_search.rb for even more fanciness.)

# app/searches/city_search.rb
class CitySearch < Searchlight::Search

  # `City` here is an ActiveRecord model
  def base_query
    City.includes(:country)
  end

  # Reach into other tables
  def search_continent
    query.where('`countries`.`continent` = ?', continent)
  end

  # Other kinds of queries
  def search_country_name_like
    query.where("`countries`.`name` LIKE ?", "%#{country_name_like}%")
  end

  # .checked? considers "false", 0 and "0" to be false
  def search_is_megacity
    query.where("`cities`.`population` #{checked?(is_megacity) ? '>=' : '<'} ?", 10_000_000)
  end

end

Here are some example searches.

CitySearch.new.results.to_sql
  # => "SELECT `cities`.* FROM `cities` "
CitySearch.new("name" => "Nairobi").results.to_sql
  # => "SELECT `cities`.* FROM `cities`  WHERE `cities`.`name` = 'Nairobi'"

CitySearch.new("country_name_like" =>  "aust", "continent" => "Europe").results.count # => 6

non_megas = CitySearch.new("is_megacity" => "false")
non_megas.results.to_sql 
  # => "SELECT `cities`.* FROM `cities`  WHERE (`cities`.`population` < 10000000"
non_megas.results.each do |city|
  # ...
end

Option Readers

For each search method you define, Searchlight will define a corresponding option reader method. Eg, if you add def search_first_name, your search class will get a .first_name method that returns options["first_name"] or, if that key doesn't exist, options[:first_name]. This is useful mainly when building forms.

Since it considers the keys "first_name" and :first_name to be interchangeable, Searchlight will raise an error if you supply both.

Examining Options

Searchlight provides some methods for examining the options provided to your search.

  • raw_options contains exactly what it was instantiated with
  • options contains all raw_options that weren't empty?. Eg, if raw_options is categories: nil, tags: ["a", ""], options will be tags: ["a"].
  • empty?(value) returns true for nil, whitespace-only strings, or anything else that returns true from value.empty? (eg, empty arrays)
  • checked?(value) returns a boolean, which mostly works like !!value but considers 0, "0", and "false" to be false

Finally, explain will tell you how Searchlight interpreted your options. Eg, book_search.explain might output:

Initialized with `raw_options`: ["title_like", "author_name_like", "category_in",
"tags", "book_thickness", "parts_about_lolcats"]

Of those, the non-blank ones are available as `options`: ["title_like",
"author_name_like", "tags", "book_thickness", "in_print"]

Of those, the following have corresponding `search_` methods: ["title_like",
"author_name_like", "in_print"]. These would be used to build the query.

Blank options are: ["category_in", "parts_about_lolcats"]

Non-blank options with no corresponding `search_` method are: ["tags",
"book_thickness"]

Defining Defaults

Sometimes it's useful to have default search options - eg, "orders that haven't been fulfilled" or "houses listed in the last month".

This can be done by overriding options. Eg:

class BookSearch < SearchlightSearch

  # def base_query...

  def options
    super.tap { |opts|
      opts["in_print"] ||= "either"
    }
  end

  def search_in_print
    return query if options["in_print"].to_s == "either"
    query.where(in_print: checked?(options["in_print"]))
  end

end

Subclassing

You can subclass an existing search class and support all the same options with a different base query. This may be useful for single table inheritance, for example.

class VillageSearch < CitySearch
  def base_query
    Village.all
  end
end

Or you can use super to get the superclass's base_query value and modify it:

class SmallTownSearch < CitySearch
  def base_query
    super.where("`cities`.`population` < ?", 1_000)
  end
end

Custom Options

You can provide a Searchlight search any options you like; only those with a matching search_ method will determine what methods are run. Eg, if you want to do AccountSearch.new("super_user" => true) to find restricted results, just ensure that you check options["super_user"] when building your query.

Usage in Rails

ActionView adapter

Searchlight plays nicely with Rails forms - just include the ActionView adapter as follows:

require "searchlight/adapters/action_view"

class MySearch < Searchlight::Search
  include Searchlight::Adapters::ActionView

  # ...etc
end

This will enable using a Searchlight::Search with form_for:

# app/views/cities/index.html.haml
...
= form_for(@search, url: search_cities_path) do |f|
  %fieldset
    = f.label      :name, "Name"
    = f.text_field :name

  %fieldset
    = f.label      :country_name_like, "Country Name Like"
    = f.text_field :country_name_like

  %fieldset
    = f.label  :is_megacity, "Megacity?"
    = f.select :is_megacity, [['Yes', true], ['No', false], ['Either', '']]

  %fieldset
    = f.label  :continent, "Continent"
    = f.select :continent, ['Africa', 'Asia', 'Europe'], include_blank: true

  = f.submit "Search"
  
- @results.each do |city|
  = render partial: 'city', locals: {city: city}

Controllers

As long as your form submits options your search understands, you can easily hook it up in your controller:

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController

  def index
    @search  = OrderSearch.new(search_params) # For use in a form
    @results = @search.results                # For display along with form
  end
  
  protected
  
  def search_params
    # Ensure the user can only browse or search their own orders
    (params[:order_search] || {}).merge(user_id: current_user.id)
  end
end

Compatibility

For any given version, check .travis.yml to see what Ruby versions we're testing for compatibility.

Installation

Add this line to your application's Gemfile:

gem 'searchlight'

And then execute:

$ bundle

Or install it yourself as:

$ gem install searchlight

Contributing

rake runs the tests; rake mutant runs mutation tests using mutant.

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Shout Outs

  • The excellent Mr. Adam Hunter, co-creator of Searchlight.
  • TMA for supporting the initial development of Searchlight.

More Repositories

1

authority

*CURRENTLY UNMAINTAINED*. Authority helps you authorize actions in your Rails app. It's ORM-neutral and has very little fancy syntax; just group your models under one or more Authorizer classes and write plain Ruby methods on them.
Ruby
1,209
star
2

secret_santa

Ruby script for choosing and notifying secret santas
Ruby
28
star
3

elixir_common_i18n

Common Gettext localization / internationalization strings for Elixir
11
star
4

demo_hashes

Demonstration of how hashes work - explanation at http://nathanmlong.com/2015/10/reimplementing-rubys-hash/
Ruby
11
star
5

absinthe_relay_keyset_connection

Elixir
10
star
6

nokogiri_bang_finders

Adds XML finders to Nokogiri that raise if nothing is found.
Ruby
9
star
7

persistentPanel

A jQuery plugin to toggle a panel and remember its position
JavaScript
8
star
8

.dotfiles

Vim configuration, etc
Vim Script
7
star
9

jquery.persistentForm

Yet another form autosaving plugin!
JavaScript
6
star
10

swappy

Elixir anagram-generation library
Elixir
5
star
11

consolation

Yet another safe JS console
JavaScript
4
star
12

hash_cache

A small collection of hashes that cache
Ruby
3
star
13

caches

A small collection of Ruby caches with good performance and hash-like access patterns
Ruby
3
star
14

broadway_rabbitmq_experiment

Elixir
3
star
15

mendel

Mendel breeds the best combinations of N lists without building all possible combinations.
Ruby
3
star
16

demos

Little demos of how Elixir works
Elixir
2
star
17

Sprockets-Spaghetti

A tiny, silly demo of Sprockets 1 for my teammates
JavaScript
2
star
18

consult

(Experimental) Drop-in customer service chat for Phoenix apps
Elixir
2
star
19

hash_presentation

CSS
1
star
20

data_structure_sandbox

A sandbox for me to play around with data structures.
Ruby
1
star
21

nathanl.github.com

Personal site
JavaScript
1
star
22

phoenix-static-experiment

Experiment in performance difference between static and dynamic rendering in Phoenix
JavaScript
1
star
23

bookfinder

A demo app for the Searchlight ruby gem
Ruby
1
star
24

Vigor

A Ruby tool to generate your .vimrc
Ruby
1
star
25

markov

A Markov chain generator
Ruby
1
star
26

searchlight_demo

Demo for the Searchlight Ruby gem
Ruby
1
star
27

lullabeam_rpi3

Nerves System for Rpi3 to support Lullabeam
Elixir
1
star
28

anagram_wordlists

Lists of words that may be used in generating anagrams
1
star
29

Stack-Training-Resources

TMA Stack Training and Resources
1
star