#CloudSesame
Light and Flexible CloudSearch Query Interface

#Install
* In terminal type:
```
gem install CloudSesame
```
* Or add this line to the application in Gemfile:
```
gem 'CloudSesame
```

#Setup AWS Credentials
*Create a initializer file, example: `config/initializers/cloud_sesame.rb`
```
require 'cloud_sesame'

CloudSesame::Domain::Client.configure do |config|
	config.access_key = ENV['AWS_ACCESS_KEY_ID']
	config.secret_key = ENV['AWS_SECRET_ACCESS_KEY']
end
```
#Usage
##1. Mix CloudSesame into any class or model
```
class Product < ActiveRecord::Base
	include CloudSesame
	
	# call `define_cloudsearch` to setup your CloudSearch config, default size (optional), fields, and scopes (optional)
	define_cloudsearch do 
	
		# REQUIRED: class specific config
		config.endpoint = ENV['AWS_PRODUCT_SEARCH_ENDPOINT']
		config.region 	= ENV['AWS_PRODUCT_SEARCH_REGION']
		
		# default query size is 10
		default_size <integer>
		
		# turn on sloppy query with distance
		define_sloppiness 3
		
		# turn on fuzzy search
		define_fuzziness {
			max_fuzziness <integer> # => maximum fuzziness per word, DEFAULT: 3
			min_char_size <integer>	# => minimum word size to trigger fuzziness, DEFAULT: 6
			fuzzy_percent <float>	# => [(word.size * fuzzy_percent).round, max_fuzziness].min, DEFAULT: 0.17
		}
		
		# field config
		field :product_name,	query: { weight: <integer> }	# => query_options[:fields] = ["product_name^<integer>"]
		field :description,		query: true						# => query_options[:fields] = ["description"]
		
		field :currency,		facet: true				# => enable facet
		field :discount,	 	facet: { 				# => enable facet with buckets
			buckets: %w([10,100] [25,100] [50,100] [70,100]), 
			method: 'interval' 
		}
		field :manufacturer,	facet: { size: 50 }		# => enable facet with max size
		field :category,		facet: { 				# => enable facet with sorting
			sort: 'bucket', size: 10_000 
		}
		
		field :created_at
		
		# scope examples
		
		# INPUT: "puma", 
		# OUPUT: query="shoes", filter_query="(and manufacturer:'puma')"
		scope :shoes_by_brand, ->(brand = nil) { query("shoes").and { manufacturer brand } if brand }
		
		# INPUT: 1
		# OUPUT: filter_query="(and created_at:{'2016-01-17T00:00:00Z',})"
		scope :created_in, ->(days = nil) { and { created_at r.gt(Date.today - days) } if days }
		
	end
end
```
##2. Inheriting from another class or model
```
# Inheriting Definitions from another class
class ExclusiveProduct < Product

	# load previous define cloudsearch definition
	load_definition_from Product
	
	# call define_cloudsearch again to override config 
	# or map field to a different name
	define_cloudsearch {
		field :name, as: :product_name		# => it will use name as product_name's alias, so user can query with
											# NewProduct.cloudsearch.name("shoes") instead of .product_name("shoes")
											# but the output will still be filter_query="product_name:'shoes'"
	}
end
```

##3. Query DSL

###Simple Query
```
# Simple Query String
Product.cloudsearch.query("shoes")				
# OUTPUT: "shoes"

Product.cloudsearch.query("shoes -puma")			
# OUTPUT: "shoes -puma"

# With Sloppy Query
Product.cloudsearch.query("shoes")					
# OUTPUT: "(shoes|\"shoes\"~3)"

Product.cloudsearch.query("shoes -puma")			
# OUTPUT: "(shoes -puma|\"shoes -puma\"~3)"

# With Fuzzy Search
Product.cloudsearch.query("white shoes")			
# OUTPUT: "(shoes|(white~3+shoes~3))"

Product.cloudsearch.query("white shoes -puma")		
# OUTPUT: "(shoes -puma|(white~3+shoes~3+-puma))"
```

###Pagination
```
Product.cloudsearch.page(3).size(100)				
# OUTPUT: { start: 200, size: 100 }

Product.cloudsearch.start(99).size(100)				
# OUTPUT: { start: 99, size: 100 }
```

###Sorting
```
Product.cloudsearch.sort(name: :desc)				
# OUTPUT: { sort: "name desc" }

Product.cloudsearch.sort(name: :desc, price: :asc)	
# OUTPUT: { sort: "name desc,price asc" }
```

###Return
```
Product.cloudsearch.all_fields						
# OUTPUT: { return: "_all_fields" }

Product.cloudsearch.no_fields						
# OUTPUT: { return: "_no_fields" }

Product.cloudsearch.score							
# OUTPUT: { return: "_score" }
```

###Scope
```
Product.coudsearch.shoes_by_brand("puma")			
# OUTPUT: { query:"shoes" filter_query:"(and manufacturer:'puma')" }

Product.cloudsearch.created_in(7)					
# OUTPUT: { filter_query:"(and created_at:{Date.today - 7,}) }

Product.cloudsearch.or { 							
	created_in(7)									
	discount 25..100							
}	
# OUTPUT: { filter_query:"(or (and created_at:{Date.today - 7,}) discount:{25,100] }
```

###AND & OR Block
```
Product.cloudsearch.and { ... }					
# OUTPUT: "(and ...)"

Product.cloudsearch.and {						
	or! { ... }									
	or! { ... }									 
}
# OUTPUT: "(and (or ...) (or ...))"
# NOTE: AND & OR are a ruby syntax, can not be used directly,
# so aliased them to (and!, all) and (or!, any)

Product.cloudsearch.and.not { ... }				
# OUTPUT: "(not (and ...))"

Product.cloudsearch.or { and!.not { ...} }		
# OUTPUT: "(or (not (and ...)))"
```

###Field Methods
* field can be set by calling #<field_name> *values
```
Product.cloudsearch.name("shoes")				
# OUTPUT: "name:'shoes'"

Product.cloudsearch.name "shoes", "sneaker"
# OUTPUT: name:'shoes' name:'sneaker'

Product.cloudsearch.name("shoes").price(100)	
# OUTPUT: "(and name:'shoes' price:100)"

Product.cloudsearch.and { name "shoes"; price 25..100 }
# OUTPUT: "(and name:'shoes' price:[25,100])"
```
* #not, #prefix (#start_with, #begin_with), #near can be chained after #<field_name> and takes multiple values
```
Product.cloudsearch.and { name.not "shoes"; ... }
# OUTPUT: "(and (not name:'shoes') ...)"

Product.cloudsearch.and { name.start_with "shoes" }
# OUTPUT: "(and (prefix field='name' 'shoes'))"

Product.cloudsearch.and { name.near "shoes" }
# OUTPUT: "(and (near field='name' 'shoes'))"

Product.cloudsearch.and { name.not.start_with "shoes" }
# OUTPUT: "(and (not (near field='name' 'shoes')))"

Product.cloudsearch.and { name(start_with("shoes"), near("puma")).not("nike") }
# OUTPUT: "(and (prefix field='name' 'shoes') (near field='name' 'puma') (not name:'nike'))"
```
* #prefix (#start_with, #begin_with), #near can be called directly to generate a single field value
```
Product.cloudsearch.and { name.not start_with("shoes"), near("something") } 
# OUTPUT: "(not (prefix field='name' 'shoes') (not (near field='name' 'something')))"
```

###Date, Time and Rage
* field method accepts Ruby Date and Range Object and will automatically parse them into the CloudSearch format 
```
Date.today	=> "'2016-01-18T00:00:00Z'"
Time.now	=> "'2016-01-18T14:36:57Z'"
25..100 	=> "[25,100]"
25...100	=> "[25,100}"
```
* use #range or #r for more complicated range, range object accepts Date/Time object as well
```
r.gt(100)		=> "{100,}"
r.gte(100)		=> "[100,}"
r.lt(100)		=> "{,100}"
r.lte(100)		=> "{,100]"
r.gte(100).lt(200)	=> "[100,200}"
r.gt(Date.today)	=> "{'2016-01-18T00:00:00Z',}"
```

###Search Related Methods
* #search  => send off the request, save and returns the response and clear the request
* #found   => returns the hits found from the response 
* #results => returns the hits.hit from the response
* #each    => Calls the given block once for each result, passing that result as a parameter. Returns the results itself.
* #map     => Creates a new array containing the results returned by the block.

###Other Methods
* #