engine.js 6.34 KB
Newer Older
Nick's avatar
Nick committed
1
const tsquery = require('pg-tsquery')()
2 3 4 5 6
const stream = require('stream')
const Promise = require('bluebird')
const pipeline = Promise.promisify(stream.pipeline)

/* global WIKI */
7 8

module.exports = {
Nick's avatar
Nick committed
9
  async activate() {
10 11 12
    if (WIKI.config.db.type !== 'postgres') {
      throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!')
    }
13
  },
Nick's avatar
Nick committed
14
  async deactivate() {
15 16 17 18
    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.`)
19 20 21 22
  },
  /**
   * INIT
   */
Nick's avatar
Nick committed
23
  async init() {
24 25
    WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`)

26
    // -> Create Search Index
Nick's avatar
Nick committed
27 28
    const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
    if (!indexExists) {
29
      WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`)
Nick's avatar
Nick committed
30 31 32 33 34 35
      await WIKI.models.knex.schema.createTable('pagesVector', table => {
        table.increments()
        table.string('path')
        table.string('locale')
        table.string('title')
        table.string('description')
36
        table.specificType('tokens', 'TSVECTOR')
37
        table.text('content')
Nick's avatar
Nick committed
38 39
      })
    }
40 41 42
    // -> Create Words Index
    const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords')
    if (!wordsExists) {
43
      WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`)
44 45
      await WIKI.models.knex.raw(`
        CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat(
46
          'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
47 48 49 50
        )`)
      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)`)
    }
51 52

    WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`)
53 54 55 56 57 58 59 60
  },
  /**
   * QUERY
   *
   * @param {String} q Query
   * @param {Object} opts Additional options
   */
  async query(q, opts) {
Nick's avatar
Nick committed
61
    try {
62
      let suggestions = []
63
      let qry = `
Nick's avatar
Nick committed
64
        SELECT id, path, locale, title, description
65
        FROM "pagesVector", to_tsquery(?,?) query
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
        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)
83 84 85 86
      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)
      }
Nick's avatar
Nick committed
87 88
      return {
        results: results.rows,
89
        suggestions,
Nick's avatar
Nick committed
90 91 92 93 94 95
        totalHits: results.rows.length
      }
    } catch (err) {
      WIKI.logger.warn('Search Engine Error:')
      WIKI.logger.warn(err)
    }
96 97 98 99 100 101 102
  },
  /**
   * CREATE
   *
   * @param {Object} page Page to create
   */
  async created(page) {
Nick's avatar
Nick committed
103
    await WIKI.models.knex.raw(`
104 105
      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'))
Nick's avatar
Nick committed
106
      )
107
    `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.safeContent])
108 109 110 111 112 113 114
  },
  /**
   * UPDATE
   *
   * @param {Object} page Page to update
   */
  async updated(page) {
Nick's avatar
Nick committed
115 116
    await WIKI.models.knex.raw(`
      UPDATE "pagesVector" SET
117 118 119 120 121 122
        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 = ?
123
    `, [page.title, page.description, page.title, page.description, page.safeContent, page.path, page.localeCode])
124 125 126 127 128 129 130
  },
  /**
   * DELETE
   *
   * @param {Object} page Page to delete
   */
  async deleted(page) {
Nick's avatar
Nick committed
131
    await WIKI.models.knex('pagesVector').where({
132
      locale: page.localeCode,
Nick's avatar
Nick committed
133 134
      path: page.path
    }).del().limit(1)
135 136 137 138 139 140 141
  },
  /**
   * RENAME
   *
   * @param {Object} page Page to rename
   */
  async renamed(page) {
Nick's avatar
Nick committed
142
    await WIKI.models.knex('pagesVector').where({
143
      locale: page.localeCode,
144
      path: page.path
Nick's avatar
Nick committed
145
    }).update({
NGPixel's avatar
NGPixel committed
146
      locale: page.destinationLocaleCode,
Nick's avatar
Nick committed
147
      path: page.destinationPath
148
    })
149 150 151 152 153
  },
  /**
   * REBUILD INDEX
   */
  async rebuild() {
154
    WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`)
Nick's avatar
Nick committed
155
    await WIKI.models.knex('pagesVector').truncate()
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    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()
        }
      })
    )

Nick's avatar
Nick committed
177
    await WIKI.models.knex.raw(`
178 179 180 181 182 183
      INSERT INTO "pagesWords" (word)
        SELECT word FROM ts_stat(
          'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
        )
      `)

184
    WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`)
185 186
  }
}