# SearchLingo SearchLingo is a framework for defining simple, user-friendly query languages and translating them into their underlying queries. It was originally designed after I found myself implementing the same basic query parsing over and over again across different projects. I wanted a way to simplify the process without having to worry about application-specific aspects of searching. The way the searches themselves are performed lies outside the scope of this project. Although originally designed to work with basic searching with ActiveRecord models, it should be usable with other data stores provided they let you chain queries together onto a single object. ## Installation Add this line to your application's Gemfile: ```ruby gem 'search_lingo' ``` And then execute: $ bundle Or install it yourself as: $ gem install search_lingo ## Usage Create a class which inherits from SearchLingo::AbstractSearch. Provide an implementation of #default_parse in that class. Register parsers for specific types of search tokens using the parser class method. Instantiate your search class by passing in the query string and the scope on which to perform the search. Use the #results method to compile the search and return the results. Take a look at the examples/ directory for more concrete examples. ## How It Works A search is instantiated with a query string and a search scope (commonly an ActiveRecord model). The search breaks the query string down into a series of tokens, and each token is processed by a declared series of parsers. If a parser succeeds, the process immediately terminates and advances to the next token. If none of the declared parsers succeeds, and the token is compound -- that is, the token is composed of an operator and a term (e.g., "foo: bar"), the token is simplified and then processed by the declared parsers again. If the second pass also fails, then the (now simplified) token falls through to the #default_parse method defined by the search class. (It is important that this method be implemented in such a way that it always succeeds.) ## Search Classes Search classes should inherit from SearchLingo::AbstractSearch and they should override the #default_parse instance method. It is important that this method be defined in such a way that it always succeeds, as the results will be sent to the query object via #public_send. In addtion, the class method parser can be used to declare additional parsers that should be used by the search class. (See the section "Parsing" for more information on what makes a suitable parser.) ## Parsers Any object that can respond to the #call method can be used as a parser. If the parser succeeds, it should return an Array of arguments that can be sent to the query object using #public_send, e.g., [:where, { id: 42 }]. If the parser fails, it should return a falsey value (typically nil). For very simple parsers which need not be reusable, you can pass the parsing logic to the parser method as a block: class MySearch < SearchLingo::AbstractSearch parser do |token| token.match /\Aid:[[:space:]]*([[:digit:]]+)\z/ do |m| [:where, { id: m[1] }] end end end Parsers can also be implemented as lambdas: module Parsers ID_PARSER = lambda do |token| token.match h/\Aid:[[:space:]]*([[:digit:]]+)\z/ do |m| [:where, { id: m[1] }] end end end class MySearch < SearchLingo::AbstractSearch parser Parsers::ID_PARSER end class MyOtherSearch < SearchLingo::AbstractSearch parser Parsers::ID_PARSER end Finally, for the most complicated cases, you could implement parsers as classes: module Parsers class IdParser def initialize(table, operator = nil) @table = table @prefix = /#{operator}:\s*/ if operator end def call(token) token.match /\A#{@prefix}([[:digit:]]+)\z/ do |m| [:where, { @table => { id: m[1] } }] end end end end class EventSearch < SearchLingo::AbstractSearch parser Parsers::IdParser.new :events # => match "42" parser Parsers::IdParser.new :categories, 'category' # => match "category: 42" end class CategorySearch < SearchLingo::AbstractSearch parser Parsers::IdParser.new :categories end ## Tokenization Queries are comprised of one or more tokens separated by spaces. A simple token is a term which can be a single word (or date, number, etc.) or multiple terms within a pair of double quotes. A compound token is a simple token preceded by an operator followed by zero or more spaces. QUERY := TOKEN* TOKEN := COMPOUND_TOKEN | TERM COMPOUND_TOKEN := OPERATOR TERM OPERATOR := [[:graph:]]+: TERM := "[^"]*" | [[:graph:]]+ Terms can be things like: * foo * "foo bar" * 6/14/15 * 1000.00 Operators can be things like: * foo: * bar_baz: (If you want to perform a query with a term that could potentially be parsed as an operator, you would place the term in quotes, i.e., "foo:".) ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing 1. Fork it ( https://github.com/jparker/search_lingo/fork ) 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 a new Pull Request