2019-03-09 18:43:32 -05:00
const tsquery = require ( 'pg-tsquery' )()
2019-03-24 00:27:42 -04:00
const stream = require ( 'stream' )
const Promise = require ( 'bluebird' )
const pipeline = Promise . promisify ( stream . pipeline )
/* global WIKI */
2019-03-09 00:51:02 -05:00
module . exports = {
2019-03-09 18:43:32 -05:00
async activate () {
2019-03-11 00:47:27 -04:00
if ( WIKI . config . db . type !== 'postgres' ) {
throw new WIKI . Error . SearchActivationFailed ( 'Must use PostgreSQL database to activate this engine!' )
}
2019-03-09 00:51:02 -05:00
},
2019-03-09 18:43:32 -05:00
async deactivate () {
2019-03-24 00:27:42 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Dropping index tables...` )
await WIKI . models . knex . schema . dropTable ( 'pagesWords' )
await WIKI . models . knex . schema . dropTable ( 'pagesVector' )
WIKI . logger . info ( `(SEARCH/POSTGRES) Index tables have been dropped.` )
2019-03-09 00:51:02 -05:00
},
/**
* INIT
*/
2019-03-09 18:43:32 -05:00
async init () {
2019-03-13 02:52:08 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Initializing...` )
2019-03-10 01:28:58 -05:00
// -> Create Search Index
2019-03-09 18:43:32 -05:00
const indexExists = await WIKI . models . knex . schema . hasTable ( 'pagesVector' )
if ( ! indexExists ) {
2019-03-13 02:52:08 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Creating Pages Vector table...` )
2019-03-09 18:43:32 -05:00
await WIKI . models . knex . schema . createTable ( 'pagesVector' , table => {
table . increments ()
table . string ( 'path' )
table . string ( 'locale' )
table . string ( 'title' )
table . string ( 'description' )
2019-03-10 01:28:58 -05:00
table . specificType ( 'tokens' , 'TSVECTOR' )
2019-03-24 00:27:42 -04:00
table . text ( 'content' )
2019-03-09 18:43:32 -05:00
})
}
2019-03-10 01:28:58 -05:00
// -> Create Words Index
const wordsExists = await WIKI . models . knex . schema . hasTable ( 'pagesWords' )
if ( ! wordsExists ) {
2019-03-13 02:52:08 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Creating Words Suggestion Index...` )
2019-03-10 01:28:58 -05:00
await WIKI . models . knex . raw ( `
CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat(
2019-03-24 12:51:52 -04:00
'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
2019-03-10 01:28:58 -05:00
)` )
await WIKI . models . knex . raw ( 'CREATE EXTENSION IF NOT EXISTS pg_trgm' )
await WIKI . models . knex . raw ( `CREATE INDEX "pageWords_idx" ON "pagesWords" USING GIN (word gin_trgm_ops)` )
}
2019-03-13 02:52:08 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Initialization completed.` )
2019-03-09 00:51:02 -05:00
},
/**
* QUERY
*
* @param {String} q Query
* @param {Object} opts Additional options
*/
async query ( q , opts ) {
2019-03-09 18:43:32 -05:00
try {
2019-03-10 01:28:58 -05:00
let suggestions = []
2020-04-11 00:19:23 -04:00
let qry = `
2019-03-09 18:43:32 -05:00
SELECT id, path, locale, title, description
2019-12-16 01:52:42 +03:00
FROM "pagesVector", to_tsquery(?,?) query
2020-04-11 00:19:23 -04:00
WHERE (query @@ "tokens" OR path ILIKE ?)
`
let qryEnd = `ORDER BY ts_rank(tokens, query) DESC`
let qryParams = [ this . config . dictLanguage , tsquery ( q ), `% ${ q . toLowerCase () } %` ]
if ( opts . locale ) {
qry = ` ${ qry } AND locale = ?`
qryParams . push ( opts . locale )
}
if ( opts . path ) {
qry = ` ${ qry } AND path ILIKE ?`
qryParams . push ( `% ${ opts . path } ` )
}
const results = await WIKI . models . knex . raw ( `
${ qry }
${ qryEnd }
` , qryParams )
2019-03-10 01:28:58 -05:00
if ( results . rows . length < 5 ) {
const suggestResults = await WIKI . models . knex . raw ( `SELECT word, word <-> ? AS rank FROM "pagesWords" WHERE similarity(word, ?) > 0.2 ORDER BY rank LIMIT 5;` , [ q , q ])
suggestions = suggestResults . rows . map ( r => r . word )
}
2019-03-09 18:43:32 -05:00
return {
results : results . rows ,
2019-03-10 01:28:58 -05:00
suggestions ,
2019-03-09 18:43:32 -05:00
totalHits : results . rows . length
}
} catch ( err ) {
WIKI . logger . warn ( 'Search Engine Error:' )
WIKI . logger . warn ( err )
}
2019-03-09 00:51:02 -05:00
},
/**
* CREATE
*
* @param {Object} page Page to create
*/
async created ( page ) {
2019-03-09 18:43:32 -05:00
await WIKI . models . knex . raw ( `
2019-03-24 00:27:42 -04:00
INSERT INTO "pagesVector" (path, locale, title, description, "tokens") VALUES (
?, ?, ?, ?, (setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'A') || setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'B') || setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'C'))
2019-03-09 18:43:32 -05:00
)
2019-03-24 00:27:42 -04:00
` , [ page . path , page . localeCode , page . title , page . description , page . title , page . description , page . safeContent ])
2019-03-09 00:51:02 -05:00
},
/**
* UPDATE
*
* @param {Object} page Page to update
*/
async updated ( page ) {
2019-03-09 18:43:32 -05:00
await WIKI . models . knex . raw ( `
UPDATE "pagesVector" SET
2019-03-11 00:47:27 -04:00
title = ?,
description = ?,
tokens = (setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'A') ||
setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'B') ||
setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'C'))
WHERE path = ? AND locale = ?
2019-03-24 00:27:42 -04:00
` , [ page . title , page . description , page . title , page . description , page . safeContent , page . path , page . localeCode ])
2019-03-09 00:51:02 -05:00
},
/**
* DELETE
*
* @param {Object} page Page to delete
*/
async deleted ( page ) {
2019-03-09 18:43:32 -05:00
await WIKI . models . knex ( 'pagesVector' ). where ({
2019-03-11 00:47:27 -04:00
locale : page . localeCode ,
2019-03-09 18:43:32 -05:00
path : page . path
}). del (). limit ( 1 )
2019-03-09 00:51:02 -05:00
},
/**
* RENAME
*
* @param {Object} page Page to rename
*/
async renamed ( page ) {
2019-03-09 18:43:32 -05:00
await WIKI . models . knex ( 'pagesVector' ). where ({
2019-03-11 00:47:27 -04:00
locale : page . localeCode ,
2019-11-23 16:21:10 -05:00
path : page . path
2019-03-09 18:43:32 -05:00
}). update ({
2019-10-13 19:59:50 -04:00
locale : page . destinationLocaleCode ,
2019-03-09 18:43:32 -05:00
path : page . destinationPath
2019-03-11 00:47:27 -04:00
})
2019-03-09 00:51:02 -05:00
},
/**
* REBUILD INDEX
*/
async rebuild () {
2019-03-13 02:52:08 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Rebuilding Index...` )
2019-03-09 18:43:32 -05:00
await WIKI . models . knex ( 'pagesVector' ). truncate ()
2019-03-24 00:27:42 -04:00
await WIKI . models . knex ( 'pagesWords' ). truncate ()
await pipeline (
WIKI . models . knex . column ( 'path' , 'localeCode' , 'title' , 'description' , 'render' ). select (). from ( 'pages' ). where ({
isPublished : true ,
isPrivate : false
}). stream (),
new stream . Transform ({
objectMode : true ,
transform : async ( page , enc , cb ) => {
const content = WIKI . models . pages . cleanHTML ( page . render )
await WIKI . models . knex . raw ( `
INSERT INTO "pagesVector" (path, locale, title, description, "tokens", content) VALUES (
?, ?, ?, ?, (setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'A') || setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'B') || setweight(to_tsvector(' ${ this . config . dictLanguage } ', ?), 'C')), ?
)
` , [ page . path , page . localeCode , page . title , page . description , page . title , page . description , content , content ])
cb ()
}
})
)
2019-03-09 18:43:32 -05:00
await WIKI . models . knex . raw ( `
2019-03-24 00:27:42 -04:00
INSERT INTO "pagesWords" (word)
SELECT word FROM ts_stat(
'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
)
` )
2019-03-13 02:52:08 -04:00
WIKI . logger . info ( `(SEARCH/POSTGRES) Index rebuilt successfully.` )
2019-03-09 00:51:02 -05:00
}
}