mirror of https://github.com/Requarks/wiki.git
13 changed files with 259 additions and 24 deletions
Split View
Diff Options
-
1.eslintrc.json
-
4CHANGELOG.md
-
8agent.js
-
4assets/js/app.js
-
4client/js/components/search.js
-
2controllers/ws.js
-
4libs/entries.js
-
22libs/git.js
-
5libs/local.js
-
206libs/search.js
-
4package.json
-
15server.js
-
4views/common/header.pug
4
assets/js/app.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,206 @@ |
|||
'use strict' |
|||
|
|||
const Promise = require('bluebird') |
|||
const _ = require('lodash') |
|||
const path = require('path') |
|||
const searchIndex = require('search-index') |
|||
const stopWord = require('stopword') |
|||
const streamToPromise = require('stream-to-promise') |
|||
|
|||
module.exports = { |
|||
|
|||
_si: null, |
|||
_isReady: false, |
|||
|
|||
/** |
|||
* Initialize search index |
|||
* |
|||
* @return {undefined} Void |
|||
*/ |
|||
init () { |
|||
let self = this |
|||
let dbPath = path.resolve(ROOTPATH, appconfig.paths.data, 'search') |
|||
self._isReady = new Promise((resolve, reject) => { |
|||
searchIndex({ |
|||
deletable: true, |
|||
fieldedSearch: true, |
|||
indexPath: dbPath, |
|||
logLevel: 'error', |
|||
stopwords: _.get(stopWord, appconfig.lang, []) |
|||
}, (err, si) => { |
|||
if (err) { |
|||
winston.error('[SERVER.Search] Failed to initialize search index.', err) |
|||
reject(err) |
|||
} else { |
|||
self._si = Promise.promisifyAll(si) |
|||
self._si.flushAsync().then(() => { |
|||
winston.info('[SERVER.Search] Search index flushed and ready.') |
|||
resolve(true) |
|||
}) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
return self |
|||
}, |
|||
|
|||
/** |
|||
* 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._isReady.then(() => { |
|||
return self.delete(content._id).then(() => { |
|||
return self._si.concurrentAddAsync({ |
|||
fieldOptions: [{ |
|||
fieldName: 'entryPath', |
|||
searchable: true, |
|||
weight: 2 |
|||
}, |
|||
{ |
|||
fieldName: 'title', |
|||
nGramLength: [1, 2], |
|||
searchable: true, |
|||
weight: 3 |
|||
}, |
|||
{ |
|||
fieldName: 'subtitle', |
|||
searchable: true, |
|||
weight: 1, |
|||
storeable: false |
|||
}, |
|||
{ |
|||
fieldName: 'parent', |
|||
searchable: false |
|||
}, |
|||
{ |
|||
fieldName: 'content', |
|||
searchable: true, |
|||
weight: 0, |
|||
storeable: false |
|||
}] |
|||
}, [{ |
|||
entryPath: content._id, |
|||
title: content.title, |
|||
subtitle: content.subtitle || '', |
|||
parent: content.parent || '', |
|||
content: content.content || '' |
|||
}]).then(() => { |
|||
winston.log('verbose', '[SERVER.Search] Entry ' + content._id + ' 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._isReady.then(() => { |
|||
return streamToPromise(self._si.search({ |
|||
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) |
|||
} |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Flush the index |
|||
* |
|||
* @returns {Promise} Promise of the flush operation |
|||
*/ |
|||
flush () { |
|||
let self = this |
|||
return self._isReady.then(() => { |
|||
return self._si.flushAsync() |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* Search the index |
|||
* |
|||
* @param {Array<String>} terms |
|||
* @returns {Promise<Object>} Hits and suggestions |
|||
*/ |
|||
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 streamToPromise(self._si.search({ |
|||
query: [{ |
|||
AND: { '*': arrTerms } |
|||
}], |
|||
pageSize: 10 |
|||
})).then((hits) => { |
|||
if (hits.length > 0) { |
|||
hits = _.map(_.sortBy(hits, ['score']), h => { |
|||
return h.document |
|||
}) |
|||
} |
|||
if (hits.length < 5) { |
|||
return streamToPromise(self._si.match({ |
|||
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) |
|||
} |
|||
}) |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save