Showing posts with label liftweb. Show all posts
Showing posts with label liftweb. Show all posts

Saturday, November 6, 2010

integrating elasticsearch and lift

I recently came across elasticsearch and was waiting for a good opportunity to try it out. The opportunity came in the shape a the new project I am working on which requires a lucene grade search engine and is in lift/sacala which lends itself well as I could use the java elasticsearch client api.

Elasticsearch is quite new, and I could not find real documentation on how to integrate it, however, going over the java source code and high level documentation on the web site, I was able to come up with the following simple integration:

First define a search engine abstraction:

object SearchEngine extends Logger
{
 private var _node:Node = null
 private var _client:Client = null
 
 def startup()
 {
  if (!enabled)
  {
   warn("search engine is disabled. if this is not intentional, please change the configuration file accordingly")
   return
  }
  
  _node = nodeBuilder().client(true).node
  if (null == _node) return
  _client = _node.client
 }
 
 def shutdown()
 {
  if (null != _node) _node.close();
  _client = null
 }
 
 def enabled:Boolean = "true" == (Props.get("search.enabled") openOr "false")
 
 def connected:Boolean = null != _client
 
 def index(indexName:String, typeName:String, identifier:String, json:JValue)
 {
  if (null == indexName) throw new Exception("invalid (null) index")
  if (null == typeName) throw new Exception("invalid (null) type")
  if (null == identifier) throw new Exception("invalid (null) identifier")
  if (null == json) throw new Exception("invalid (null) json")
  
  if (!enabled) return
  confirmConnection
  
  val request:IndexRequestBuilder = _client.prepareIndex(indexName, typeName, identifier)
  request.setSource(pretty(render(json)))
  val response:IndexResponse = request.execute().actionGet() 
 }
 
 def delete(indexName:String, typeName:String, identifier:String)
 {
  if (null == indexName) throw new Exception("invalid (null) index")
  if (null == typeName) throw new Exception("invalid (null) type")
  if (null == identifier) throw new Exception("invalid (null) identifier")
  
  if (!enabled) return
  confirmConnection
  
  val request:DeleteRequestBuilder = _client.prepareDelete(indexName, typeName, identifier)
  val response:DeleteResponse = request.execute().actionGet()
 }
 
 def search(indexName:String, query:XContentQueryBuilder, from:Integer, size:Integer, explain:Boolean=false):SearchHits =
 {
  if (null == indexName) throw new Exception("invalid (null) index")
  if (null == query) throw new Exception("invalid (null) query")
 
  if (!enabled) return null
  confirmConnection
 
  val request:SearchRequestBuilder = _client.prepareSearch(indexName)
  request.setSearchType(SearchType.QUERY_THEN_FETCH) 
  request.setQuery(query) 
  request.setFrom(from.intValue) 
  request.setSize(size.intValue) 
  request.setExplain(explain) 
 
  val response:SearchResponse = request.execute().actionGet()
  return response.hits
}
 
 private def confirmConnection
 {
  if (!connected) startup
  if (!connected) throw new Exception("cannot connect to search engine, perhaps it needs to be disabled?")
 }
 
}


Naturally, the next step is to add a call into "SearchEngine.startup" from your Boot.scala so the app connects to the search server on startup.

Next define a pair of model & companion traits to hide the integration details from the domain objects:

trait SearchableModelMeta[T <: SearchableModel[T]] extends BaseModelMeta[T]
{
 self: T  with SearchableModelMeta[T] with BaseModelMeta[T] =>
 
 private def searchIndexName:String = this.pluralXmlName
 
 private def searchTypeName:String = this.xmlName
 
 override def beforeSave = updateSearchIndexDate _ :: super.beforeSave

 override def afterSave = storeInSearchIndex _ :: super.afterSave
 
 override def afterDelete = deleteFromSearchIndex _ :: super.afterSave
 
 def reindexAll()
 {
  findAll.foreach((instance:T) => { storeInSearchIndex(instance) })
 }
 
 def search(query:XContentQueryBuilder, from:Integer=0, size:Integer=100):SearchHits =
 {
  SearchEngine.search(this.searchIndexName, query, from, size)
 }
 
 private def updateSearchIndexDate(instance:T)
 {
  instance.indexedAt(Helpers.now)
 }

 private def storeInSearchIndex(instance:T)
 {
  SearchEngine.index(this.searchIndexName, this.searchTypeName, instance.id.toString, instance.toJson("search"))
 }
 
 private def deleteFromSearchIndex(instance:T)
 {
  SearchEngine.delete(this.searchIndexName, this.searchTypeName, instance.id.toString)
 }
}

trait SearchableModel[T <: BaseModel[T]] extends BaseModel[T]
{
 self: T =>
 
 /*
 *** columns
 */
 
 object indexedAt extends MappedDateTime(this.asInstanceOf[T])
 {
  override def dbColumnName = "indexed_at"
 }
}


Note this example uses a custom base model class, which is not super important to what I am trying to show here (it facilitates dynamic json assembly and naming conventions, but those are pretty straight forward). however, it is important to note that that base model mixes in LongKeyedMapper and IdPk. headers for the class & companion below.

trait BaseModelMeta[T <: BaseModel[T]] extends LongKeyedMetaMapper[T]

trait BaseModel[T <: LongKeyedMapper[T]] extends LongKeyedMapper[T] with IdPK


Last mixin the SearchableMode traits with the domain model so you can do things like:

Employee.search(...)

This of course if a very basic example which uses a handful of the available features elasticsearch and lucene offer, however, this should give anyone trying to integrate elasticsearch with lift a good head start.

Thursday, November 4, 2010

ruby on rails style routes with lift

In my latest project i am using lift/scala for restful services. spending the last couple of years using ruby on rails for such, the first thing i was looking for was a convenient way to define my generic restful routes. here is what i cam up with:

object Routes extends RestHelper with Logger
{      
 // response builder
 implicit def cvt:JxCvtPF[ResponseItem] = 
 { 
  case (XmlSelect, response, request) => response.toXml 
  case (JsonSelect, response, request) => response.toJson
 }
 
 private val PREFIX:String = "api"
 
 private var _services:mutable.ListMap[String, BaseService] = null
    
 def registerService(service:BaseService, alias:String=null)
 {
  if (null == _services) _services = new mutable.ListMap[String, BaseService]()
  val key:String = if (null != alias) alias else service.getClass.getName.split("\\.").toList.last.replace("Service$", "")
  _services += key -> service
 }  
   
 /* 
 *** standard restful routes
 */  
   
 serveJx
 {
  // GET /api/service/index.{xml|json}
  case Get(PREFIX :: StringValue(service) :: "index" :: Nil, _) => invokeApi(service, "index")   
 }
 serveJx
 { 
  // GET /api/service/1.{xml|json}
  case Get(PREFIX :: StringValue(service) :: LongValue(id) :: Nil, _) => invokeApi(service, "get", id)
 }
 serveJx
 { 
  // GET /api/service/api.{xml|json}
  case Get(PREFIX :: StringValue(service) :: StringValue(api) :: Nil, _) => invokeApi(service, api)
 }
 serveJx
 { 
  // GET /api/service/api/1.{xml|json}
  case Get(PREFIX :: StringValue(service) :: StringValue(api) :: LongValue(id) :: Nil, _) => invokeApi(service, api, id)
 }  
 serveJx
 { 
  // POST /api/service.{xml|json}
  case Post(PREFIX :: StringValue(service) :: Nil, _) => invokeApi(service, "create")
 }
 serveJx
 { 
  // POST /api/service/api.{xml|json}
  case Post(PREFIX :: StringValue(service) :: StringValue(api) :: Nil, _) => invokeApi(service, api)
 }
 serveJx
 { 
  // POST /api/service/api/1.{xml|json}
  case Post(PREFIX :: StringValue(service) :: StringValue(api) :: LongValue(id) :: Nil, _) => invokeApi(service, api, id)
 }  
 serveJx
 { 
  // PUT /api/service/1.{xml|json}
  case Put(PREFIX :: StringValue(service) :: LongValue(id) :: Nil, _) => invokeApi(service, "update", id)
 }
 serveJx
 { 
  // DELETE /api/service/1.{xml|json}
  case Delete(PREFIX :: StringValue(service) :: LongValue(id) :: Nil, _) => invokeApi(service, "delete", id)
 }
 
 private def invokeApi(service:String, api:String, id:Long=0):Box[ResponseItem] =
 {
  val serviceName:String = StringHelpers.camelify(service)
  val methodName:String = StringHelpers.camelifyMethod(api)
  
  info("processing api " + service + ":" + api + " as " + serviceName + "[Service]:" + methodName)
     
  if (!_services.contains(serviceName)) return Full(Failed("unknown service " + service))
  
  _services(serviceName) match
  {
   case serviceHandler:BaseService =>
   {     
    serviceHandler.getClass.getMethods.foreach((method:Method) =>
    {      
     if (methodName == method.getName)
     {      
      val result:Object = if (0 == id) method.invoke(serviceHandler) else method.invoke(serviceHandler, id.asInstanceOf[AnyRef])       
      result match
      {
       case response:ResponseItem => return Full(response) 
       case _ => return Empty
      }
     }
    })
    return Full(Failed("unknown API " + service + ":" + api)) 
   }
   case _ => Full(Failed("invalid API configuration, service is not a BaseService"))
  }
 }
}


Once the routes handler (above) is define, you need to register the service implementation(s) to the routes handler and register the routes handler to Lift's dispatch table, this is done in lift's boot sequence, typically defined in Boot.scala. for example:

Routes.registerService(SessionService)
Routes.registerService(UserService)
.    
.
.

LiftRules.dispatch.append(Routes)


Naturally, you can extend this rest handler to handle custom restful routes, using the serveJx or serve directives, this is explained in more details here

Please note that a bug in scala is preventing from bundling all those serveJx case statements together, thus, the DRYlessness.