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.
No comments:
Post a Comment