engine.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. const _ = require('lodash')
  2. const tsquery = require('pg-tsquery')()
  3. module.exports = {
  4. async activate() {
  5. if (WIKI.config.db.type !== 'postgres') {
  6. throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!')
  7. }
  8. },
  9. async deactivate() {
  10. // not used
  11. },
  12. /**
  13. * INIT
  14. */
  15. async init() {
  16. WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`)
  17. // -> Create Search Index
  18. const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
  19. if (!indexExists) {
  20. WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`)
  21. await WIKI.models.knex.schema.createTable('pagesVector', table => {
  22. table.increments()
  23. table.string('path')
  24. table.string('locale')
  25. table.string('title')
  26. table.string('description')
  27. table.specificType('tokens', 'TSVECTOR')
  28. })
  29. }
  30. // -> Create Words Index
  31. const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords')
  32. if (!wordsExists) {
  33. WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`)
  34. await WIKI.models.knex.raw(`
  35. CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat(
  36. '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"'
  37. )`)
  38. await WIKI.models.knex.raw('CREATE EXTENSION IF NOT EXISTS pg_trgm')
  39. await WIKI.models.knex.raw(`CREATE INDEX "pageWords_idx" ON "pagesWords" USING GIN (word gin_trgm_ops)`)
  40. }
  41. WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`)
  42. },
  43. /**
  44. * QUERY
  45. *
  46. * @param {String} q Query
  47. * @param {Object} opts Additional options
  48. */
  49. async query(q, opts) {
  50. try {
  51. let suggestions = []
  52. const results = await WIKI.models.knex.raw(`
  53. SELECT id, path, locale, title, description
  54. FROM "pagesVector", to_tsquery(?) query
  55. WHERE query @@ "tokens"
  56. ORDER BY ts_rank(tokens, query) DESC
  57. `, [tsquery(q)])
  58. if (results.rows.length < 5) {
  59. 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])
  60. suggestions = suggestResults.rows.map(r => r.word)
  61. }
  62. return {
  63. results: results.rows,
  64. suggestions,
  65. totalHits: results.rows.length
  66. }
  67. } catch (err) {
  68. WIKI.logger.warn('Search Engine Error:')
  69. WIKI.logger.warn(err)
  70. }
  71. },
  72. /**
  73. * CREATE
  74. *
  75. * @param {Object} page Page to create
  76. */
  77. async created(page) {
  78. await WIKI.models.knex.raw(`
  79. INSERT INTO "pagesVector" (path, locale, title, description, tokens) VALUES (
  80. '?', '?', '?', '?', (setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'C'))
  81. )
  82. `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.content])
  83. },
  84. /**
  85. * UPDATE
  86. *
  87. * @param {Object} page Page to update
  88. */
  89. async updated(page) {
  90. await WIKI.models.knex.raw(`
  91. UPDATE "pagesVector" SET
  92. title = ?,
  93. description = ?,
  94. tokens = (setweight(to_tsvector('${this.config.dictLanguage}', ?), 'A') ||
  95. setweight(to_tsvector('${this.config.dictLanguage}', ?), 'B') ||
  96. setweight(to_tsvector('${this.config.dictLanguage}', ?), 'C'))
  97. WHERE path = ? AND locale = ?
  98. `, [page.title, page.description, page.title, page.description, page.content, page.path, page.localeCode])
  99. },
  100. /**
  101. * DELETE
  102. *
  103. * @param {Object} page Page to delete
  104. */
  105. async deleted(page) {
  106. await WIKI.models.knex('pagesVector').where({
  107. locale: page.localeCode,
  108. path: page.path
  109. }).del().limit(1)
  110. },
  111. /**
  112. * RENAME
  113. *
  114. * @param {Object} page Page to rename
  115. */
  116. async renamed(page) {
  117. await WIKI.models.knex('pagesVector').where({
  118. locale: page.localeCode,
  119. path: page.sourcePath
  120. }).update({
  121. locale: page.localeCode,
  122. path: page.destinationPath
  123. })
  124. },
  125. /**
  126. * REBUILD INDEX
  127. */
  128. async rebuild() {
  129. WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`)
  130. await WIKI.models.knex('pagesVector').truncate()
  131. await WIKI.models.knex.raw(`
  132. INSERT INTO "pagesVector" (path, locale, title, description, "tokens")
  133. SELECT path, "localeCode" AS locale, title, description,
  134. (setweight(to_tsvector('${this.config.dictLanguage}', title), 'A') ||
  135. setweight(to_tsvector('${this.config.dictLanguage}', description), 'B') ||
  136. setweight(to_tsvector('${this.config.dictLanguage}', content), 'C')) AS tokens
  137. FROM "pages"
  138. WHERE pages."isPublished" AND NOT pages."isPrivate"`)
  139. WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`)
  140. }
  141. }