|
|
|
"use strict";
|
|
|
|
|
|
|
|
var Promise = require('bluebird'),
|
|
|
|
_ = require('lodash'),
|
|
|
|
path = require('path'),
|
|
|
|
searchIndex = require('search-index'),
|
|
|
|
stopWord = require('stopword');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search Model
|
|
|
|
*/
|
|
|
|
module.exports = {
|
|
|
|
|
|
|
|
_si: null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize Search model
|
|
|
|
*
|
|
|
|
* @param {Object} appconfig The application config
|
|
|
|
* @return {Object} Search model instance
|
|
|
|
*/
|
|
|
|
init(appconfig) {
|
|
|
|
|
|
|
|
let self = this;
|
|
|
|
let dbPath = path.resolve(ROOTPATH, appconfig.datadir.db, 'search-index');
|
|
|
|
|
|
|
|
searchIndex({
|
|
|
|
deletable: true,
|
|
|
|
fieldedSearch: true,
|
|
|
|
indexPath: dbPath,
|
|
|
|
logLevel: 'error',
|
|
|
|
stopwords: stopWord.getStopwords(appconfig.lang).sort()
|
|
|
|
}, (err, si) => {
|
|
|
|
if(err) {
|
|
|
|
winston.error('Failed to initialize search-index.', err);
|
|
|
|
} else {
|
|
|
|
self._si = Promise.promisifyAll(si);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return self;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
find(terms) {
|
|
|
|
|
|
|
|
let self = this;
|
|
|
|
terms = _.chain(terms)
|
|
|
|
.deburr()
|
|
|
|
.toLower()
|
|
|
|
.trim()
|
|
|
|
.replace(/[^a-z0-9 ]/g, '')
|
|
|
|
.value();
|
|
|
|
|
|
|
|
let arrTerms = _.chain(terms)
|
|
|
|
.split(' ')
|
|
|
|
.filter((f) => { return !_.isEmpty(f); })
|
|
|
|
.value();
|
|
|
|
|
|
|
|
|
|
|
|
return self._si.searchAsync({
|
|
|
|
query: {
|
|
|
|
AND: [{ '*': arrTerms }]
|
|
|
|
},
|
|
|
|
pageSize: 10
|
|
|
|
}).get('hits').then((hits) => {
|
|
|
|
|
|
|
|
if(hits.length < 5) {
|
|
|
|
return self._si.matchAsync({
|
|
|
|
beginsWith: terms,
|
|
|
|
threshold: 3,
|
|
|
|
limit: 5,
|
|
|
|
type: 'simple'
|
|
|
|
}).then((matches) => {
|
|
|
|
|
|
|
|
return {
|
|
|
|
match: hits,
|
|
|
|
suggest: matches
|
|
|
|
};
|
|
|
|
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
match: hits,
|
|
|
|
suggest: []
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
|
|
|
if(err.type === 'NotFoundError') {
|
|
|
|
return {
|
|
|
|
match: [],
|
|
|
|
suggest: []
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
winston.error(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a document to the index
|
|
|
|
*
|
|
|
|
* @param {Object} content Document content
|
|
|
|
* @return {Promise} Promise of the add operation
|
|
|
|
*/
|
|
|
|
add(content) {
|
|
|
|
|
|
|
|
let self = this;
|
|
|
|
|
|
|
|
return self.delete(content.entryPath).then(() => {
|
|
|
|
|
|
|
|
return self._si.addAsync({
|
|
|
|
entryPath: content.entryPath,
|
|
|
|
title: content.meta.title,
|
|
|
|
subtitle: content.meta.subtitle || '',
|
|
|
|
parent: content.parent.title || '',
|
|
|
|
content: content.text || ''
|
|
|
|
}, {
|
|
|
|
fieldOptions: [{
|
|
|
|
fieldName: 'entryPath',
|
|
|
|
searchable: true,
|
|
|
|
weight: 2
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fieldName: 'title',
|
|
|
|
nGramLength: [1, 2],
|
|
|
|
searchable: true,
|
|
|
|
weight: 3
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fieldName: 'subtitle',
|
|
|
|
searchable: true,
|
|
|
|
weight: 1,
|
|
|
|
store: false
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fieldName: 'parent',
|
|
|
|
searchable: false,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fieldName: 'content',
|
|
|
|
searchable: true,
|
|
|
|
weight: 0,
|
|
|
|
store: false
|
|
|
|
}]
|
|
|
|
}).then(() => {
|
|
|
|
winston.info('Entry ' + content.entryPath + ' added/updated to index.');
|
|
|
|
return true;
|
|
|
|
}).catch((err) => {
|
|
|
|
winston.error(err);
|
|
|
|
});
|
|
|
|
|
|
|
|
}).catch((err) => {
|
|
|
|
winston.error(err);
|
|
|
|
});
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete an entry from the index
|
|
|
|
*
|
|
|
|
* @param {String} The entry path
|
|
|
|
* @return {Promise} Promise of the operation
|
|
|
|
*/
|
|
|
|
delete(entryPath) {
|
|
|
|
|
|
|
|
let self = this;
|
|
|
|
|
|
|
|
return self._si.searchAsync({
|
|
|
|
query: {
|
|
|
|
AND: [{ 'entryPath': [entryPath] }]
|
|
|
|
}
|
|
|
|
}).then((results) => {
|
|
|
|
|
|
|
|
if(results.totalHits > 0) {
|
|
|
|
let delIds = _.map(results.hits, 'id');
|
|
|
|
return self._si.delAsync(delIds);
|
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
}).catch((err) => {
|
|
|
|
|
|
|
|
if(err.type === 'NotFoundError') {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
winston.error(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|