{"id":13411901,"url":"https://github.com/nathanl/searchlight","last_synced_at":"2025-05-15T14:06:41.446Z","repository":{"id":7829639,"uuid":"9200885","full_name":"nathanl/searchlight","owner":"nathanl","description":"Searchlight helps you build searches from options via Ruby methods that you write.","archived":false,"fork":false,"pushed_at":"2024-01-26T17:59:22.000Z","size":189,"stargazers_count":532,"open_issues_count":3,"forks_count":19,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-05-15T14:06:30.132Z","etag":null,"topics":["orm","ruby","search"],"latest_commit_sha":null,"homepage":"https://github.com/nathanl/searchlight","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nathanl.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-04-03T17:43:47.000Z","updated_at":"2024-11-29T02:12:12.000Z","dependencies_parsed_at":"2024-06-18T15:30:20.631Z","dependency_job_id":"79b2c70d-4ff5-4656-892b-1a545a44f838","html_url":"https://github.com/nathanl/searchlight","commit_stats":{"total_commits":204,"total_committers":10,"mean_commits":20.4,"dds":0.553921568627451,"last_synced_commit":"63a2a1cd4370a0886b563b22626ae02e3c7c08d4"},"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanl%2Fsearchlight","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanl%2Fsearchlight/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanl%2Fsearchlight/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanl%2Fsearchlight/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nathanl","download_url":"https://codeload.github.com/nathanl/searchlight/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254355335,"owners_count":22057354,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["orm","ruby","search"],"created_at":"2024-07-30T20:01:18.262Z","updated_at":"2025-05-15T14:06:36.436Z","avatar_url":"https://github.com/nathanl.png","language":"Ruby","readme":"# Searchlight\n\n## Status\n\n**I consider searchlight \"done\"**.\nIt has no production dependencies, so there's no reason it shouldn't work indefinitely.\nI've also moved on to other things.\n\nIf you find a bug, feel free to open an issue so others can find it and discuss, but I'm unlikely to respond personally.\nIf Searchlight doesn't meet your needs anymore, fork away! :)\n\n## Description\n\nSearchlight is a low-magic way to build database searches using an ORM.\n\nSearchlight 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](https://rubygems.org/gems/sequel), [Mongoid](https://rubygems.org/gems/mongoid), etc).\n\n[![Gem Version](https://badge.fury.io/rb/searchlight.png)](https://rubygems.org/gems/searchlight)\n[![Code Climate](https://codeclimate.com/github/nathanl/searchlight.png)](https://codeclimate.com/github/nathanl/searchlight)\n[![Build Status](https://api.travis-ci.org/nathanl/searchlight.png?branch=master)](https://travis-ci.org/nathanl/searchlight)\n\n## Getting Started\n\nA [demo app](http://bookfinder-searchlight-demo.herokuapp.com) and [the code for that app](https://github.com/nathanl/searchlight_demo) are available to help you get started.\n\n## Overview\n\nSearchlight's main use is to support search forms in web applications.\n\nSearchlight doesn't write queries for you. What it does do is:\n\n- Give you an object with which you can build a search form (eg, using `form_for` in Rails)\n- Give you a sensible place to put your query logic\n- 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)\n\nFor example, if you have a Searchlight search class called `YetiSearch`, and you instantiate it like this:\n\n```ruby\n  search = YetiSearch.new(\n    # or params[:yeti_search]\n    \"active\" =\u003e true, \"name\" =\u003e \"Jimmy\", \"location_in\" =\u003e %w[NY LA]\n  )\n```\n\n... 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`.)\n\nThe `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.\n\n## Usage\n\n### Search class\n\nA search class has two main parts: a `base_query` and some `search_` methods. For example:\n\n```ruby\nclass PersonSearch \u003c Searchlight::Search\n\n  # This is the starting point for any chaining we do, and it's what\n  # will be returned if no search options are passed.\n  # In this case, it's an ActiveRecord model.\n  def base_query\n    Person.all # or `.scoped` for ActiveRecord 3\n  end\n\n  # A search method.\n  def search_first_name\n    # If `\"first_name\"` was the first key in the options_hash,\n    # `query` here will be the base query, namely, `Person.all`.\n    query.where(first_name: options[:first_name])\n  end\n\n  # Another search method.\n  def search_last_name\n    # If `\"last_name\"` was the second key in the options_hash,\n    # `query` here will be whatever `search_first_name` returned.\n    query.where(last_name: last_name)\n  end\nend\n```\n\nCalling `PersonSearch.new(\"first_name\" =\u003e \"Gregor\", \"last_name\" =\u003e \"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\" =\u003e \"\"`, the second `where` would not be added.\n\nHere'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.)\n\n```ruby\n# app/searches/city_search.rb\nclass CitySearch \u003c Searchlight::Search\n\n  # `City` here is an ActiveRecord model\n  def base_query\n    City.includes(:country)\n  end\n\n  # Reach into other tables\n  def search_continent\n    query.where('`countries`.`continent` = ?', continent)\n  end\n\n  # Other kinds of queries\n  def search_country_name_like\n    query.where(\"`countries`.`name` LIKE ?\", \"%#{country_name_like}%\")\n  end\n\n  # .checked? considers \"false\", 0 and \"0\" to be false\n  def search_is_megacity\n    query.where(\"`cities`.`population` #{checked?(is_megacity) ? '\u003e=' : '\u003c'} ?\", 10_000_000)\n  end\n\nend\n```\n\nHere are some example searches.\n\n```ruby\nCitySearch.new.results.to_sql\n  # =\u003e \"SELECT `cities`.* FROM `cities` \"\nCitySearch.new(\"name\" =\u003e \"Nairobi\").results.to_sql\n  # =\u003e \"SELECT `cities`.* FROM `cities`  WHERE `cities`.`name` = 'Nairobi'\"\n\nCitySearch.new(\"country_name_like\" =\u003e  \"aust\", \"continent\" =\u003e \"Europe\").results.count # =\u003e 6\n\nnon_megas = CitySearch.new(\"is_megacity\" =\u003e \"false\")\nnon_megas.results.to_sql \n  # =\u003e \"SELECT `cities`.* FROM `cities`  WHERE (`cities`.`population` \u003c 10000000\"\nnon_megas.results.each do |city|\n  # ...\nend\n```\n\n### Option Readers\n\nFor 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.\n\nSince it considers the keys `\"first_name\"` and `:first_name` to be interchangeable, Searchlight will raise an error if you supply both.\n\n### Examining Options\n\nSearchlight provides some methods for examining the options provided to your search.\n\n- `raw_options` contains exactly what it was instantiated with\n- `options` contains all `raw_options` that weren't `empty?`. Eg, if `raw_options` is `categories: nil, tags: [\"a\", \"\"]`, options will be `tags: [\"a\"]`.\n- `empty?(value)` returns true for `nil`, whitespace-only strings, or anything else that returns true from `value.empty?` (eg, empty arrays)\n- `checked?(value)` returns a boolean, which mostly works like `!!value` but considers `0`, `\"0\"`, and `\"false\"` to be `false`\n\nFinally, `explain` will tell you how Searchlight interpreted your options. Eg, `book_search.explain` might output:\n\n```\nInitialized with `raw_options`: [\"title_like\", \"author_name_like\", \"category_in\",\n\"tags\", \"book_thickness\", \"parts_about_lolcats\"]\n\nOf those, the non-blank ones are available as `options`: [\"title_like\",\n\"author_name_like\", \"tags\", \"book_thickness\", \"in_print\"]\n\nOf those, the following have corresponding `search_` methods: [\"title_like\",\n\"author_name_like\", \"in_print\"]. These would be used to build the query.\n\nBlank options are: [\"category_in\", \"parts_about_lolcats\"]\n\nNon-blank options with no corresponding `search_` method are: [\"tags\",\n\"book_thickness\"]\n```\n\n### Defining Defaults\n\nSometimes it's useful to have default search options - eg, \"orders that haven't been fulfilled\" or \"houses listed in the last month\".\n\nThis can be done by overriding `options`. Eg:\n\n```ruby\nclass BookSearch \u003c SearchlightSearch\n\n  # def base_query...\n\n  def options\n    super.tap { |opts|\n      opts[\"in_print\"] ||= \"either\"\n    }\n  end\n\n  def search_in_print\n    return query if options[\"in_print\"].to_s == \"either\"\n    query.where(in_print: checked?(options[\"in_print\"]))\n  end\n\nend\n```\n\n### Subclassing\n\nYou 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. \n\n```ruby\nclass VillageSearch \u003c CitySearch\n  def base_query\n    Village.all\n  end\nend\n```\n\nOr you can use `super` to get the superclass's `base_query` value and modify it:\n\n```ruby\nclass SmallTownSearch \u003c CitySearch\n  def base_query\n    super.where(\"`cities`.`population` \u003c ?\", 1_000)\n  end\nend\n```\n\n### Custom Options\n\nYou 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\" =\u003e true)` to find restricted results, just ensure that you check `options[\"super_user\"]` when building your query.\n\n## Usage in Rails\n\n### ActionView adapter\n\nSearchlight plays nicely with Rails forms - just include the `ActionView` adapter as follows:\n\n```ruby\nrequire \"searchlight/adapters/action_view\"\n\nclass MySearch \u003c Searchlight::Search\n  include Searchlight::Adapters::ActionView\n\n  # ...etc\nend\n```\n\nThis will enable using a `Searchlight::Search` with `form_for`:\n\n```ruby\n# app/views/cities/index.html.haml\n...\n= form_for(@search, url: search_cities_path) do |f|\n  %fieldset\n    = f.label      :name, \"Name\"\n    = f.text_field :name\n\n  %fieldset\n    = f.label      :country_name_like, \"Country Name Like\"\n    = f.text_field :country_name_like\n\n  %fieldset\n    = f.label  :is_megacity, \"Megacity?\"\n    = f.select :is_megacity, [['Yes', true], ['No', false], ['Either', '']]\n\n  %fieldset\n    = f.label  :continent, \"Continent\"\n    = f.select :continent, ['Africa', 'Asia', 'Europe'], include_blank: true\n\n  = f.submit \"Search\"\n  \n- @results.each do |city|\n  = render partial: 'city', locals: {city: city}\n```\n\n### Controllers\n\nAs long as your form submits options your search understands, you can easily hook it up in your controller:\n\n```ruby\n# app/controllers/orders_controller.rb\nclass OrdersController \u003c ApplicationController\n\n  def index\n    @search  = OrderSearch.new(search_params) # For use in a form\n    @results = @search.results                # For display along with form\n  end\n  \n  protected\n  \n  def search_params\n    # Ensure the user can only browse or search their own orders\n    (params[:order_search] || {}).merge(user_id: current_user.id)\n  end\nend\n```\n\n## Compatibility\n\nFor any given version, check `.travis.yml` to see what Ruby versions we're testing for compatibility.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'searchlight'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install searchlight\n\n## Contributing\n\n`rake` runs the tests; `rake mutant` runs mutation tests using [mutant](https://github.com/mbj/mutant).\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n\n## Shout Outs\n\n- The excellent [Mr. Adam Hunter](https://github.com/adamhunter), co-creator of Searchlight.\n- [TMA](http://tma1.com) for supporting the initial development of Searchlight.\n","funding_links":[],"categories":["Ruby","Active Record Plugins"],"sub_categories":["Rails Search"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanl%2Fsearchlight","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnathanl%2Fsearchlight","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanl%2Fsearchlight/lists"}