Search-Index integration + cache flush on start
This commit is contained in:
parent
8af9212837
commit
12ea967a84
@ -32,6 +32,7 @@
|
|||||||
"lcdata": true,
|
"lcdata": true,
|
||||||
"mark": true,
|
"mark": true,
|
||||||
"rights": true,
|
"rights": true,
|
||||||
|
"search": true,
|
||||||
"upl": true,
|
"upl": true,
|
||||||
"winston": true,
|
"winston": true,
|
||||||
"ws": true,
|
"ws": true,
|
||||||
|
@ -5,6 +5,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Offline mode (no remote git sync) can now be enabled by setting `git: false` in config.yml
|
- Offline mode (no remote git sync) can now be enabled by setting `git: false` in config.yml
|
||||||
|
- Improved search engine (Now using search-index engine instead of MongoDB text search)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Cache is now flushed when starting / restarting the server
|
||||||
|
|
||||||
## [v1.0-beta.4] - 2017-02-11
|
## [v1.0-beta.4] - 2017-02-11
|
||||||
### Fixed
|
### Fixed
|
||||||
|
8
agent.js
8
agent.js
@ -113,7 +113,13 @@ var job = new Cron({
|
|||||||
// -> Update cache and search index
|
// -> Update cache and search index
|
||||||
|
|
||||||
if (fileStatus !== 'active') {
|
if (fileStatus !== 'active') {
|
||||||
return entries.updateCache(entryPath)
|
return entries.updateCache(entryPath).then(entry => {
|
||||||
|
process.send({
|
||||||
|
action: 'searchAdd',
|
||||||
|
content: entry
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
File diff suppressed because one or more lines are too long
@ -42,7 +42,7 @@ if ($('#search-input').length) {
|
|||||||
searchmoveidx: (val, oldVal) => {
|
searchmoveidx: (val, oldVal) => {
|
||||||
if (val > 0) {
|
if (val > 0) {
|
||||||
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1])
|
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1])
|
||||||
? 'res.' + vueHeader.searchmovearr[val - 1]._id
|
? 'res.' + vueHeader.searchmovearr[val - 1].entryPath
|
||||||
: 'sug.' + vueHeader.searchmovearr[val - 1]
|
: 'sug.' + vueHeader.searchmovearr[val - 1]
|
||||||
} else {
|
} else {
|
||||||
vueHeader.searchmovekey = ''
|
vueHeader.searchmovekey = ''
|
||||||
@ -61,7 +61,7 @@ if ($('#search-input').length) {
|
|||||||
let i = vueHeader.searchmoveidx - 1
|
let i = vueHeader.searchmoveidx - 1
|
||||||
|
|
||||||
if (vueHeader.searchmovearr[i]) {
|
if (vueHeader.searchmovearr[i]) {
|
||||||
window.location.assign('/' + vueHeader.searchmovearr[i]._id)
|
window.location.assign('/' + vueHeader.searchmovearr[i].entryPath)
|
||||||
} else {
|
} else {
|
||||||
vueHeader.searchq = vueHeader.searchmovearr[i]
|
vueHeader.searchq = vueHeader.searchmovearr[i]
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ module.exports = (socket) => {
|
|||||||
|
|
||||||
socket.on('search', (data, cb) => {
|
socket.on('search', (data, cb) => {
|
||||||
cb = cb || _.noop
|
cb = cb || _.noop
|
||||||
entries.search(data.terms).then((results) => {
|
search.find(data.terms).then((results) => {
|
||||||
return cb(results) || true
|
return cb(results) || true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -256,7 +256,9 @@ module.exports = {
|
|||||||
return fs.statAsync(fpath).then((st) => {
|
return fs.statAsync(fpath).then((st) => {
|
||||||
if (st.isFile()) {
|
if (st.isFile()) {
|
||||||
return self.makePersistent(entryPath, contents).then(() => {
|
return self.makePersistent(entryPath, contents).then(() => {
|
||||||
return self.updateCache(entryPath)
|
return self.updateCache(entryPath).then(entry => {
|
||||||
|
return search.add(entry)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(new Error('Entry does not exist!'))
|
return Promise.reject(new Error('Entry does not exist!'))
|
||||||
|
22
libs/git.js
22
libs/git.js
@ -68,13 +68,13 @@ module.exports = {
|
|||||||
_initRepo (appconfig) {
|
_initRepo (appconfig) {
|
||||||
let self = this
|
let self = this
|
||||||
|
|
||||||
winston.info('[' + PROCNAME + '][GIT] Checking Git repository...')
|
winston.info('[' + PROCNAME + '.Git] Checking Git repository...')
|
||||||
|
|
||||||
// -> Check if path is accessible
|
// -> Check if path is accessible
|
||||||
|
|
||||||
return fs.mkdirAsync(self._repo.path).catch((err) => {
|
return fs.mkdirAsync(self._repo.path).catch((err) => {
|
||||||
if (err.code !== 'EEXIST') {
|
if (err.code !== 'EEXIST') {
|
||||||
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.')
|
winston.error('[' + PROCNAME + '.Git] Invalid Git repository path or missing permissions.')
|
||||||
}
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
self._git = new Git({ 'git-dir': self._repo.path })
|
self._git = new Git({ 'git-dir': self._repo.path })
|
||||||
@ -89,7 +89,7 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (appconfig.git === false) {
|
if (appconfig.git === false) {
|
||||||
winston.info('[' + PROCNAME + '][GIT] Remote syncing is disabled. Not recommended!')
|
winston.info('[' + PROCNAME + '.Git] Remote syncing is disabled. Not recommended!')
|
||||||
return Promise.resolve(true)
|
return Promise.resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +126,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
winston.error('[' + PROCNAME + '][GIT] Git remote error!')
|
winston.error('[' + PROCNAME + '.Git] Git remote error!')
|
||||||
throw err
|
throw err
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.')
|
winston.info('[' + PROCNAME + '.Git] Git repository is OK.')
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -159,12 +159,12 @@ module.exports = {
|
|||||||
|
|
||||||
// Fetch
|
// Fetch
|
||||||
|
|
||||||
winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...')
|
winston.info('[' + PROCNAME + '.Git] Performing pull from remote repository...')
|
||||||
return self._git.pull('origin', self._repo.branch).then((cProc) => {
|
return self._git.pull('origin', self._repo.branch).then((cProc) => {
|
||||||
winston.info('[' + PROCNAME + '][GIT] Pull completed.')
|
winston.info('[' + PROCNAME + '.Git] Pull completed.')
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!')
|
winston.error('[' + PROCNAME + '.Git] Unable to fetch from git origin!')
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -174,12 +174,12 @@ module.exports = {
|
|||||||
let out = cProc.stdout.toString()
|
let out = cProc.stdout.toString()
|
||||||
|
|
||||||
if (_.includes(out, 'commit')) {
|
if (_.includes(out, 'commit')) {
|
||||||
winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...')
|
winston.info('[' + PROCNAME + '.Git] Performing push to remote repository...')
|
||||||
return self._git.push('origin', self._repo.branch).then(() => {
|
return self._git.push('origin', self._repo.branch).then(() => {
|
||||||
return winston.info('[' + PROCNAME + '][GIT] Push completed.')
|
return winston.info('[' + PROCNAME + '.Git] Push completed.')
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.')
|
winston.info('[' + PROCNAME + '.Git] Push skipped. Repository is already in sync.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -98,11 +98,12 @@ module.exports = {
|
|||||||
* @return {Void} Void
|
* @return {Void} Void
|
||||||
*/
|
*/
|
||||||
createBaseDirectories (appconfig) {
|
createBaseDirectories (appconfig) {
|
||||||
winston.info('[SERVER] Checking data directories...')
|
winston.info('[SERVER.Local] Checking data directories...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
|
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
|
||||||
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'))
|
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'))
|
||||||
|
fs.emptyDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'))
|
||||||
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'))
|
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'))
|
||||||
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'))
|
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'))
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ module.exports = {
|
|||||||
winston.error(err)
|
winston.error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
winston.info('[SERVER] Data and Repository directories are OK.')
|
winston.info('[SERVER.Local] Data and Repository directories are OK.')
|
||||||
|
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
206
libs/search.js
Normal file
206
libs/search.js
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -91,6 +91,8 @@
|
|||||||
"simplemde": "^1.11.2",
|
"simplemde": "^1.11.2",
|
||||||
"socket.io": "^1.7.2",
|
"socket.io": "^1.7.2",
|
||||||
"sticky-js": "^1.0.7",
|
"sticky-js": "^1.0.7",
|
||||||
|
"stopword": "^0.1.1",
|
||||||
|
"stream-to-promise": "^2.2.0",
|
||||||
"validator": "^6.2.0",
|
"validator": "^6.2.0",
|
||||||
"validator-as-promised": "^1.0.2",
|
"validator-as-promised": "^1.0.2",
|
||||||
"winston": "^2.3.0"
|
"winston": "^2.3.0"
|
||||||
@ -139,7 +141,6 @@
|
|||||||
"app",
|
"app",
|
||||||
"appconfig",
|
"appconfig",
|
||||||
"appdata",
|
"appdata",
|
||||||
"bgAgent",
|
|
||||||
"db",
|
"db",
|
||||||
"entries",
|
"entries",
|
||||||
"git",
|
"git",
|
||||||
@ -147,6 +148,7 @@
|
|||||||
"lang",
|
"lang",
|
||||||
"lcdata",
|
"lcdata",
|
||||||
"rights",
|
"rights",
|
||||||
|
"search",
|
||||||
"upl",
|
"upl",
|
||||||
"winston",
|
"winston",
|
||||||
"ws",
|
"ws",
|
||||||
|
15
server.js
15
server.js
@ -37,6 +37,7 @@ global.entries = require('./libs/entries').init()
|
|||||||
global.git = require('./libs/git').init(false)
|
global.git = require('./libs/git').init(false)
|
||||||
global.lang = require('i18next')
|
global.lang = require('i18next')
|
||||||
global.mark = require('./libs/markdown')
|
global.mark = require('./libs/markdown')
|
||||||
|
global.search = require('./libs/search').init()
|
||||||
global.upl = require('./libs/uploads').init()
|
global.upl = require('./libs/uploads').init()
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@ -239,7 +240,19 @@ io.on('connection', ctrl.ws)
|
|||||||
// Start child processes
|
// Start child processes
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
global.bgAgent = fork('agent.js')
|
let bgAgent = fork('agent.js')
|
||||||
|
|
||||||
|
bgAgent.on('message', m => {
|
||||||
|
if (!m.action) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (m.action) {
|
||||||
|
case 'searchAdd':
|
||||||
|
search.add(m.content)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
process.on('exit', (code) => {
|
process.on('exit', (code) => {
|
||||||
bgAgent.disconnect()
|
bgAgent.disconnect()
|
||||||
|
@ -26,8 +26,8 @@
|
|||||||
ul.searchresults-list
|
ul.searchresults-list
|
||||||
li(v-if='searchres.length === 0')
|
li(v-if='searchres.length === 0')
|
||||||
a: em No results matching your query
|
a: em No results matching your query
|
||||||
li(v-for='sres in searchres', v-bind:class='{ "is-active": searchmovekey === "res." + sres._id }')
|
li(v-for='sres in searchres', v-bind:class='{ "is-active": searchmovekey === "res." + sres.entryPath }')
|
||||||
a(v-bind:href='"/" + sres._id') {{ sres.title }}
|
a(v-bind:href='"/" + sres.entryPath') {{ sres.title }}
|
||||||
p.searchresults-label(v-if='searchsuggest.length > 0') Did you mean...?
|
p.searchresults-label(v-if='searchsuggest.length > 0') Did you mean...?
|
||||||
ul.searchresults-list(v-if='searchsuggest.length > 0')
|
ul.searchresults-list(v-if='searchsuggest.length > 0')
|
||||||
li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }')
|
li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }')
|
||||||
|
Loading…
Reference in New Issue
Block a user