You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
6.3 KiB

5 years ago
  1. const tsquery = require('pg-tsquery')()
  2. const stream = require('stream')
  3. const Promise = require('bluebird')
  4. const pipeline = Promise.promisify(stream.pipeline)
  5. /* global WIKI */
  6. module.exports = {
  7. async activate() {
  8. if (WIKI.config.db.type !== 'postgres') {
  9. throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!')
  10. }
  11. },
  12. async deactivate() {
  13. WIKI.logger.info(`(SEARCH/POSTGRES) Dropping index tables...`)
  14. await WIKI.models.knex.schema.dropTable('pagesWords')
  15. await WIKI.models.knex.schema.dropTable('pagesVector')
  16. WIKI.logger.info(`(SEARCH/POSTGRES) Index tables have been dropped.`)
  17. },
  18. /**
  19. * INIT
  20. */
  21. async init() {
  22. WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`)
  23. // -> Create Search Index
  24. const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
  25. if (!indexExists) {
  26. WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`)
  27. await WIKI.models.knex.schema.createTable('pagesVector', table => {
  28. table.increments()
  29. table.string('path')
  30. table.string('locale')
  31. table.string('title')
  32. table.string('description')
  33. table.specificType('tokens', 'TSVECTOR')
  34. table.text('content')
  35. })
  36. }
  37. // -> Create Words Index
  38. const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords')
  39. if (!wordsExists) {
  40. WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`)
  41. await WIKI.models.knex.raw(`
  42. CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat(
  43. 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
  44. )`)
  45. await WIKI.models.knex.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm')
  46. await WIKI.models.knex.raw(`CREATE INDEX "pageWords_idx" ON "pagesWords" USING GIN (word gin_trgm_ops)`)
  47. }
  48. WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`)
  49. },
  50. /**
  51. * QUERY
  52. *
  53. * @param {String} q Query
  54. * @param {Object} opts Additional options
  55. */
  56. async query(q, opts) {
  57. try {
  58. let suggestions = []
  59. let qry = `
  60. SELECT id, path, locale, title, description
  61. FROM "pagesVector", to_tsquery(?,?) query
  62. WHERE (query @@ "tokens" OR path ILIKE ?)
  63. `
  64. let qryEnd = `ORDER BY ts_rank(tokens, query) DESC`
  65. let qryParams = [this.config.dictLanguage, tsquery(q), `%${q.toLowerCase()}%`]
  66. if (opts.locale) {
  67. qry = `${qry} AND locale = ?`
  68. qryParams.push(opts.locale)
  69. }
  70. if (opts.path) {
  71. qry = `${qry} AND path ILIKE ?`
  72. qryParams.push(`%${opts.path}`)
  73. }
  74. const results = await WIKI.models.knex.raw(`
  75. ${qry}
  76. ${qryEnd}
  77. `, qryParams)
  78. if (results.rows.length < 5) {
  79. 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])
  80. suggestions = suggestResults.rows.map(r => r.word)
  81. }
  82. return {
  83. results: results.rows,
  84. suggestions,
  85. totalHits: results.rows.length
  86. }
  87. } catch (err) {
  88. WIKI.logger.warn('Search Engine Error:')
  89. WIKI.logger.warn(err)
  90. }
  91. },
  92. /**
  93. * CREATE
  94. *
  95. * @param {Object} page Page to create
  96. */
  97. async created(page) {
  98. await WIKI.models.knex.raw(`
  99. INSERT INTO "pagesVector" (path, locale, title, description, "tokens") VALUES (
  100. ?, ?, ?, ?, (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C'))
  101. )
  102. `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.safeContent])
  103. },
  104. /**
  105. * UPDATE
  106. *
  107. * @param {Object} page Page to update
  108. */
  109. async updated(page) {
  110. await WIKI.models.knex.raw(`
  111. UPDATE "pagesVector" SET
  112. title = ?,
  113. description = ?,
  114. tokens = (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') ||
  115. setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') ||
  116. setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C'))
  117. WHERE path = ? AND locale = ?
  118. `, [page.title, page.description, page.title, page.description, page.safeContent, page.path, page.localeCode])
  119. },
  120. /**
  121. * DELETE
  122. *
  123. * @param {Object} page Page to delete
  124. */
  125. async deleted(page) {
  126. await WIKI.models.knex('pagesVector').where({
  127. locale: page.localeCode,
  128. path: page.path
  129. }).del().limit(1)
  130. },
  131. /**
  132. * RENAME
  133. *
  134. * @param {Object} page Page to rename
  135. */
  136. async renamed(page) {
  137. await WIKI.models.knex('pagesVector').where({
  138. locale: page.localeCode,
  139. path: page.path
  140. }).update({
  141. locale: page.destinationLocaleCode,
  142. path: page.destinationPath
  143. })
  144. },
  145. /**
  146. * REBUILD INDEX
  147. */
  148. async rebuild() {
  149. WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`)
  150. await WIKI.models.knex('pagesVector').truncate()
  151. await WIKI.models.knex('pagesWords').truncate()
  152. await pipeline(
  153. WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'render').select().from('pages').where({
  154. isPublished: true,
  155. isPrivate: false
  156. }).stream(),
  157. new stream.Transform({
  158. objectMode: true,
  159. transform: async (page, enc, cb) => {
  160. const content = WIKI.models.pages.cleanHTML(page.render)
  161. await WIKI.models.knex.raw(`
  162. INSERT INTO "pagesVector" (path, locale, title, description, "tokens", content) VALUES (
  163. ?, ?, ?, ?, (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C')), ?
  164. )
  165. `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, content, content])
  166. cb()
  167. }
  168. })
  169. )
  170. await WIKI.models.knex.raw(`
  171. INSERT INTO "pagesWords" (word)
  172. SELECT word FROM ts_stat(
  173. 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"'
  174. )
  175. `)
  176. WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`)
  177. }
  178. }