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

170
    WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`)
171 172
  }
}