elasticmongooseq
Version:
Mongoose elasticsearch bindings
235 lines (186 loc) • 7.17 kB
text/coffeescript
Q = require('q')
_ = require('lodash')
###
ElasticMongooseQModel represents the binding between a mongoose class and an elasticsearch type
It handles commands no a collection and document level using the default options configured in ElasticMongooseQ.
###
class ElasticMongooseQModel
constructor: (elasticMongooseQ, options) ->
@elasticMongooseQ = elasticMongooseQ
# Mongoose requires a callback with sig (schema, options) to be passed to Schema.plugin
# http://mongoosejs.com/docs/plugins.html
#
# @param {Object} options hash
# @option {String} type - type of elasticsearch object
# @option {Object} mapping - elasticsearch mapping for the object
# @option {String} parentMethod - method to obtain a parent value (elasticsearch relationship)
# @option {String} toObjectMethod - call a method to format document ready for elasticsearch
# @option {Bool} autoSync - whether to automatically sync to elasticsearch
# @option {Bool} autoRemove - whether to automatically remove from elasticsearch
plugin: (schema, options = {}) =>
# Bind elasticsearch wide config into closure
elasticModel = this
elasticIndex = @elasticMongooseQ.elasticIndex
elasticClient = @elasticMongooseQ.elasticClient
logger = @elasticMongooseQ.logger
# set up elasticsearch model
elasticType = options.type
parentMethod = options.parentMethod
toObjectMethod = options.toObjectMethod
elasticMapping = options.mapping || {}
autoSync = options.autoSync || true
autoRemove = options.autoRemove || true
# Tell elasticMongooseQ about new type registered
@elasticMongooseQ.elasticTypes.push elasticType
# Create callbacks to sync data with elasticsearch
if autoSync
schema.post "save", (doc) ->
doc.index().catch(logger.error)
if elasticModel.autoRemove
schema.post "remove", (doc) -> doc.unIndex()
stdOpts = (opts = {}) ->
defaultOptions =
index: elasticIndex
type: elasticType
_.extend defaultOptions, opts
buildObjectToIndex = (doc, callback) ->
# If a toSearchObject method is defined then use that, else use toObject
if toObjectMethod
docOrPromise = doc[toObjectMethod]()
else
docOrPromise = doc.toObject()
# If that method returns a promise then wait for that to resolve before indexing
Q(docOrPromise)
###
index to elasticsearch
@param {Object} [optional] options hash with index options
@param {Function} [optional] callback with node signature
@returns {Promise}
###
schema.methods.index = (options, callback) ->
# If called with only callback param index(callback)
if typeof options == 'function'
callback = options
options = {}
# If called with no params index()
else if typeof options == 'undefined'
options = {}
options.id ||= "#{@id}"
if parentMethod
options.parent ||= "#{@[parentMethod]()}"
indexDoc = (doc) ->
options.body = doc
indexObject = stdOpts options
elasticClient.index(indexObject)
buildObjectToIndex(this).then(indexDoc).nodeify(callback)
###
Deletes a item from the index
@param {Function} callback with node signature
@returns {Promise}
###
schema.methods.unIndex = (callback) ->
opts = stdOpts(id: "#{@id}")
elasticClient.delete(opts).nodeify(callback)
###
Sync mongo data into elasticsearch
@param {Object} options
@option {Object} query - mongodb query object
@param {Function} callback
@returns {Promise}
###
schema.statics.syncSearchIndex = (options, callback) ->
# If called with only callback param index(callback)
if typeof options == 'function'
callback = options
options = {}
# If called with no params index()
else if typeof options == 'undefined'
options = {}
batchSize = options.batchSize ||= 1000
query = options.query || {}
deferred = Q.defer()
stream = @find(query).stream()
finished = false
indexQueue = []
stream.on 'data', (doc) ->
indexQueue.push doc
if finished or indexQueue.length >= batchSize
stream.pause()
processQueue(indexQueue)
indexQueue = []
stream.on "close", () ->
finished = true
processQueue(indexQueue)
indexQueue = []
stream.on "error", deferred.reject
processQueue = (docsToIndex) ->
logger.info "processing #{docsToIndex.length} docs\n memory used is #{process.memoryUsage().heapUsed}"
bulkBody = []
indexDocuments = () ->
docsToIndex = [] # done with index queue so clear up
elasticClient.bulk(body: bulkBody)
checkIfComplete = (bulkResult) ->
bulkBody = [] # done with bulk body so reset
if finished
deferred.resolve(true)
else
stream.resume()
buildBulkBody = ->
Q.all docsToIndex.map (doc) ->
buildObjectToIndex(doc).then (itemToIndex) ->
options =
index:
_index: elasticIndex
_type: elasticType
_id: doc.id
if parentMethod
options.index._parent = "#{doc[parentMethod]()}"
doc = null # done with doc so clear up memory
bulkBody.push options
bulkBody.push itemToIndex
buildBulkBody().then(indexDocuments).then(checkIfComplete)
deferred.promise.nodeify(callback)
###
Search elasticsearch
@param {Object} query - elasticsearch query options
@param {Function} callback
@returns {Promise}
###
schema.statics.search = (query, callback) ->
queryOptions = stdOpts(body: query)
elasticClient.search(queryOptions).nodeify(callback)
###
Create an elasticsearch mapping for this model
@param {Function} callback
@returns {Promise}
###
schema.statics.putMapping = (callback) ->
mappingBody = {}
mappingBody[elasticType] = elasticMapping
mappingOptions = stdOpts
body: mappingBody
ignoreConflicts: true
elasticClient.indices.putMapping(mappingOptions).nodeify(callback)
###
Get the elasticsearch mapping for this model
@param {Function} callback
@returns {Promise}
###
schema.statics.getMapping = (callback) ->
elasticClient.indices.getMapping(stdOpts()).nodeify(callback)
###
Delete the elasticsearch mapping for this model
@param {Function} callback
@returns {Promise}
###
schema.statics.deleteMapping = (callback) ->
elasticClient.indices.deleteMapping(stdOpts()).nodeify(callback)
###
Clear the contents of the elasticsearch index
@param {Function} callback
@returns {Promise}
###
schema.statics.clearIndex = (callback) ->
opts = stdOpts(q: '*')
elasticClient.deleteByQuery(opts).nodeify(callback)
module.exports = ElasticMongooseQModel