123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- const tsquery = require('pg-tsquery')()
- const stream = require('stream')
- const Promise = require('bluebird')
- const pipeline = Promise.promisify(stream.pipeline)
- /* global WIKI */
- module.exports = {
- async activate() {
- if (WIKI.config.db.type !== 'postgres') {
- throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!')
- }
- },
- async deactivate() {
- 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.`)
- },
- /**
- * INIT
- */
- async init() {
- WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`)
- // -> Create Search Index
- const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
- if (!indexExists) {
- WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`)
- await WIKI.models.knex.schema.createTable('pagesVector', table => {
- table.increments()
- table.string('path')
- table.string('locale')
- table.string('title')
- table.string('description')
- table.specificType('tokens', 'TSVECTOR')
- table.text('content')
- })
- }
- // -> Create Words Index
- const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords')
- if (!wordsExists) {
- WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`)
- await WIKI.models.knex.raw(`
- CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat(
- 'SELECT to_tsvector(''simple'', pages."title") || to_tsvector(''simple'', pages."description") || to_tsvector(''simple'', pages."content") FROM pages WHERE pages."isPublished" AND NOT pages."isPrivate"'
- )`)
- 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)`)
- }
- WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`)
- },
- /**
- * QUERY
- *
- * @param {String} q Query
- * @param {Object} opts Additional options
- */
- async query(q, opts) {
- try {
- let suggestions = []
- const results = await WIKI.models.knex.raw(`
- SELECT id, path, locale, title, description
- FROM "pagesVector", to_tsquery(?) query
- WHERE query @@ "tokens"
- ORDER BY ts_rank(tokens, query) DESC
- `, [tsquery(q)])
- 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)
- }
- return {
- results: results.rows,
- suggestions,
- totalHits: results.rows.length
- }
- } catch (err) {
- WIKI.logger.warn('Search Engine Error:')
- WIKI.logger.warn(err)
- }
- },
- /**
- * CREATE
- *
- * @param {Object} page Page to create
- */
- async created(page) {
- await WIKI.models.knex.raw(`
- 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'))
- )
- `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.safeContent])
- },
- /**
- * UPDATE
- *
- * @param {Object} page Page to update
- */
- async updated(page) {
- await WIKI.models.knex.raw(`
- UPDATE "pagesVector" SET
- 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 = ?
- `, [page.title, page.description, page.title, page.description, page.safeContent, page.path, page.localeCode])
- },
- /**
- * DELETE
- *
- * @param {Object} page Page to delete
- */
- async deleted(page) {
- await WIKI.models.knex('pagesVector').where({
- locale: page.localeCode,
- path: page.path
- }).del().limit(1)
- },
- /**
- * RENAME
- *
- * @param {Object} page Page to rename
- */
- async renamed(page) {
- await WIKI.models.knex('pagesVector').where({
- locale: page.localeCode,
- path: page.sourcePath
- }).update({
- locale: page.localeCode,
- path: page.destinationPath
- })
- },
- /**
- * REBUILD INDEX
- */
- async rebuild() {
- WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`)
- await WIKI.models.knex('pagesVector').truncate()
- 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()
- }
- })
- )
- await WIKI.models.knex.raw(`
- INSERT INTO "pagesWords" (word)
- SELECT word FROM ts_stat(
- 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
- )
- `)
- WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`)
- }
- }
|