Browse Source

feat: azure search module + rebuild index

Nick 6 năm trước cách đây
mục cha
commit
21ee8c0c0b

+ 1 - 0
client/components/admin.vue

@@ -104,6 +104,7 @@
         router-view
         router-view
 
 
     nav-footer
     nav-footer
+    search-results
 </template>
 </template>
 
 
 <script>
 <script>

+ 43 - 20
client/components/admin/admin-search.vue

@@ -28,6 +28,7 @@
                 :key='engine.key'
                 :key='engine.key'
                 :label='engine.title'
                 :label='engine.title'
                 :value='engine.key'
                 :value='engine.key'
+                :disabled='!engine.isAvailable'
                 color='primary'
                 color='primary'
                 hide-details
                 hide-details
               )
               )
@@ -87,6 +88,7 @@ import _ from 'lodash'
 
 
 import enginesQuery from 'gql/admin/search/search-query-engines.gql'
 import enginesQuery from 'gql/admin/search/search-query-engines.gql'
 import enginesSaveMutation from 'gql/admin/search/search-mutation-save-engines.gql'
 import enginesSaveMutation from 'gql/admin/search/search-mutation-save-engines.gql'
+import enginesRebuildMutation from 'gql/admin/search/search-mutation-rebuild-index.gql'
 
 
 export default {
 export default {
   data() {
   data() {
@@ -101,7 +103,7 @@ export default {
       this.engine = _.find(this.engines, ['key', newValue]) || {}
       this.engine = _.find(this.engines, ['key', newValue]) || {}
     },
     },
     engines(newValue, oldValue) {
     engines(newValue, oldValue) {
-      this.selectedEngine = 'db'
+      this.selectedEngine = _.get(_.find(this.engines, 'isEnabled'), 'key', 'db')
     }
     }
   },
   },
   methods: {
   methods: {
@@ -115,29 +117,50 @@ export default {
     },
     },
     async save() {
     async save() {
       this.$store.commit(`loadingStart`, 'admin-search-saveengines')
       this.$store.commit(`loadingStart`, 'admin-search-saveengines')
-      await this.$apollo.mutate({
-        mutation: enginesSaveMutation,
-        variables: {
-          engines: this.engines.map(tgt => _.pick(tgt, [
-            'isEnabled',
-            'key',
-            'config'
-          ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: enginesSaveMutation,
+          variables: {
+            engines: this.engines.map(tgt => ({
+              isEnabled: tgt.key === this.selectedEngine,
+              key: tgt.key,
+              config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))
+            }))
+          }
+        })
+        if (_.get(resp, 'data.search.updateSearchEngines.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            message: 'Search engine configuration saved successfully.',
+            style: 'success',
+            icon: 'check'
+          })
+        } else {
+          throw new Error(_.get(resp, 'data.search.updateSearchEngines.responseResult.message', 'An unexpected error occured'))
         }
         }
-      })
-      this.$store.commit('showNotification', {
-        message: 'Search engine configuration saved successfully.',
-        style: 'success',
-        icon: 'check'
-      })
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
       this.$store.commit(`loadingStop`, 'admin-search-saveengines')
       this.$store.commit(`loadingStop`, 'admin-search-saveengines')
     },
     },
     async rebuild () {
     async rebuild () {
-      this.$store.commit('showNotification', {
-        style: 'indigo',
-        message: `Coming soon...`,
-        icon: 'directions_boat'
-      })
+      this.$store.commit(`loadingStart`, 'admin-search-rebuildindex')
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: enginesRebuildMutation
+        })
+        if (_.get(resp, 'data.search.rebuildIndex.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            message: 'Index rebuilt successfully.',
+            style: 'success',
+            icon: 'check'
+          })
+        } else {
+          throw new Error(_.get(resp, 'data.search.rebuildIndex.responseResult.message', 'An unexpected error occured'))
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+      this.$store.commit(`loadingStop`, 'admin-search-rebuildindex')
     }
     }
   },
   },
   apollo: {
   apollo: {

+ 17 - 8
client/components/common/nav-header.vue

@@ -89,29 +89,38 @@
             v-model='searchAdvMenuShown'
             v-model='searchAdvMenuShown'
             left
             left
             offset-y
             offset-y
-            min-width='350'
+            min-width='450'
             :close-on-content-click='false'
             :close-on-content-click='false'
+            nudge-bottom='7'
+            nudge-right='5'
             )
             )
             v-btn.nav-header-search-adv(icon, outline, color='grey darken-2', slot='activator')
             v-btn.nav-header-search-adv(icon, outline, color='grey darken-2', slot='activator')
               v-icon(color='white') expand_more
               v-icon(color='white') expand_more
-            v-card
-              v-toolbar(flat, :color='$vuetify.dark ? `grey darken-3-d5` : `grey lighten-4`', dense)
+            v-card.radius-0(dark)
+              v-toolbar(flat, color='grey darken-4', dense)
+                v-icon.mr-2 search
                 v-subheader.pl-0 Advanced Search
                 v-subheader.pl-0 Advanced Search
-              v-card-text
+              v-card-text.pa-4
                 v-checkbox.mt-0(
                 v-checkbox.mt-0(
                   label='Restrict to Current Language'
                   label='Restrict to Current Language'
-                  color='primary'
+                  color='white'
                   v-model='searchRestrictLocale'
                   v-model='searchRestrictLocale'
                   hide-details
                   hide-details
                 )
                 )
                 v-checkbox(
                 v-checkbox(
                   label='Restrict to Below Current Path'
                   label='Restrict to Below Current Path'
-                  color='primary'
+                  color='white'
                   v-model='searchRestrictPath'
                   v-model='searchRestrictPath'
                   hide-details
                   hide-details
                 )
                 )
-              v-card-actions
-                v-btn(outline, small, color='grey') Save as defaults
+              v-divider
+              v-card-actions.grey.darken-3-d4
+                v-btn(depressed, color='grey darken-3', block)
+                  v-icon(left) chevron_right
+                  span Save as defaults
+                v-btn(depressed, color='grey darken-3', block)
+                  v-icon(left) cached
+                  span Reset
       v-flex(xs6, :md4='searchIsShown', :md6='!searchIsShown')
       v-flex(xs6, :md4='searchIsShown', :md6='!searchIsShown')
         v-toolbar.nav-header-inner(color='black', dark, flat)
         v-toolbar.nav-header-inner(color='black', dark, flat)
           v-spacer
           v-spacer

+ 2 - 2
client/components/common/search-results.vue

@@ -206,13 +206,13 @@ export default {
 
 
   &-items {
   &-items {
     .highlighted {
     .highlighted {
-      background-color: mc('blue', '50');
+      background: #FFF linear-gradient(to bottom, #FFF, mc('orange', '100'));
     }
     }
   }
   }
 
 
   &-suggestions {
   &-suggestions {
     .highlighted {
     .highlighted {
-      background-color: mc('blue', '500');
+      background: transparent linear-gradient(to bottom, mc('blue', '500'), mc('blue', '700'));
     }
     }
   }
   }
 }
 }

+ 1 - 0
client/components/history.vue

@@ -103,6 +103,7 @@
                 v-card.mt-3(light, v-html='diffHTML')
                 v-card.mt-3(light, v-html='diffHTML')
 
 
     nav-footer
     nav-footer
+    search-results
 </template>
 </template>
 
 
 <script>
 <script>

+ 1 - 0
client/components/profile.vue

@@ -23,6 +23,7 @@
         router-view
         router-view
 
 
     nav-footer
     nav-footer
+    search-results
 </template>
 </template>
 
 
 <script>
 <script>

+ 1 - 0
client/components/source.vue

@@ -16,6 +16,7 @@
                   slot
                   slot
 
 
     nav-footer
     nav-footer
+    search-results
 </template>
 </template>
 
 
 <script>
 <script>

+ 12 - 0
client/graph/admin/search/search-mutation-rebuild-index.gql

@@ -0,0 +1,12 @@
+mutation {
+  search {
+    rebuildIndex {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

+ 2 - 2
client/graph/admin/search/search-mutation-save-engines.gql

@@ -1,6 +1,6 @@
-mutation($searchEngines: [SearchEngineInput]) {
+mutation($engines: [SearchEngineInput]) {
   search {
   search {
-    updateSearchEngines(searchEngines: $searchEngines) {
+    updateSearchEngines(engines: $engines) {
       responseResult {
       responseResult {
         succeeded
         succeeded
         errorCode
         errorCode

+ 1 - 0
client/graph/admin/search/search-query-engines.gql

@@ -7,6 +7,7 @@ query {
       description
       description
       logo
       logo
       website
       website
+      isAvailable
       config {
       config {
         key
         key
         value
         value

+ 2 - 0
package.json

@@ -45,6 +45,7 @@
     "apollo-server-express": "2.3.3",
     "apollo-server-express": "2.3.3",
     "auto-load": "3.0.4",
     "auto-load": "3.0.4",
     "axios": "0.18.0",
     "axios": "0.18.0",
+    "azure-search-client": "3.1.5",
     "bcryptjs-then": "1.0.1",
     "bcryptjs-then": "1.0.1",
     "bluebird": "3.5.3",
     "bluebird": "3.5.3",
     "body-parser": "1.18.3",
     "body-parser": "1.18.3",
@@ -136,6 +137,7 @@
     "pg": "7.8.0",
     "pg": "7.8.0",
     "pg-hstore": "2.3.2",
     "pg-hstore": "2.3.2",
     "pg-tsquery": "8.0.3",
     "pg-tsquery": "8.0.3",
+    "pg-query-stream": "2.0.0",
     "pm2": "3.2.9",
     "pm2": "3.2.9",
     "pug": "2.0.3",
     "pug": "2.0.3",
     "qr-image": "3.2.0",
     "qr-image": "3.2.0",

+ 12 - 1
server/graph/resolvers/search.js

@@ -38,7 +38,7 @@ module.exports = {
   SearchMutation: {
   SearchMutation: {
     async updateSearchEngines(obj, args, context) {
     async updateSearchEngines(obj, args, context) {
       try {
       try {
-        for (let searchEngine of args.searchEngines) {
+        for (let searchEngine of args.engines) {
           await WIKI.models.searchEngines.query().patch({
           await WIKI.models.searchEngines.query().patch({
             isEnabled: searchEngine.isEnabled,
             isEnabled: searchEngine.isEnabled,
             config: _.reduce(searchEngine.config, (result, value, key) => {
             config: _.reduce(searchEngine.config, (result, value, key) => {
@@ -47,12 +47,23 @@ module.exports = {
             }, {})
             }, {})
           }).where('key', searchEngine.key)
           }).where('key', searchEngine.key)
         }
         }
+        await WIKI.models.searchEngines.initEngine({ activate: true })
         return {
         return {
           responseResult: graphHelper.generateSuccess('Search Engines updated successfully')
           responseResult: graphHelper.generateSuccess('Search Engines updated successfully')
         }
         }
       } catch (err) {
       } catch (err) {
         return graphHelper.generateError(err)
         return graphHelper.generateError(err)
       }
       }
+    },
+    async rebuildIndex (obj, args, context) {
+      try {
+        await WIKI.data.searchEngine.rebuild()
+        return {
+          responseResult: graphHelper.generateSuccess('Index rebuilt successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
     }
     }
   }
   }
 }
 }

+ 1 - 1
server/graph/schemas/page.graphql

@@ -102,7 +102,7 @@ type PageSearchResponse {
 }
 }
 
 
 type PageSearchResult {
 type PageSearchResult {
-  id: Int!
+  id: String!
   title: String!
   title: String!
   description: String!
   description: String!
   path: String!
   path: String!

+ 4 - 1
server/graph/schemas/search.graphql

@@ -27,8 +27,10 @@ type SearchQuery {
 
 
 type SearchMutation {
 type SearchMutation {
   updateSearchEngines(
   updateSearchEngines(
-    searchEngines: [SearchEngineInput]
+    engines: [SearchEngineInput]
   ): DefaultResponse @auth(requires: ["manage:system"])
   ): DefaultResponse @auth(requires: ["manage:system"])
+
+  rebuildIndex: DefaultResponse @auth(requires: ["manage:system"])
 }
 }
 
 
 # -----------------------------------------------
 # -----------------------------------------------
@@ -42,6 +44,7 @@ type SearchEngine {
   description: String
   description: String
   logo: String
   logo: String
   website: String
   website: String
+  isAvailable: Boolean
   config: [KeyValuePair]
   config: [KeyValuePair]
 }
 }
 
 

+ 4 - 0
server/helpers/error.js

@@ -69,6 +69,10 @@ module.exports = {
     message: 'Invalid locale or namespace.',
     message: 'Invalid locale or namespace.',
     code: 1009
     code: 1009
   }),
   }),
+  SearchActivationFailed: CustomError('SearchActivationFailed', {
+    message: 'Search Engine activation failed.',
+    code: 1019
+  }),
   UserCreationFailed: CustomError('UserCreationFailed', {
   UserCreationFailed: CustomError('UserCreationFailed', {
     message: 'An unexpected error occured during user creation.',
     message: 'An unexpected error occured during user creation.',
     code: 1010
     code: 1010

+ 3 - 0
server/models/pages.js

@@ -210,6 +210,7 @@ module.exports = class Page extends Model {
       isPrivate: opts.isPrivate
       isPrivate: opts.isPrivate
     })
     })
     await WIKI.models.pages.renderPage(page)
     await WIKI.models.pages.renderPage(page)
+    await WIKI.data.searchEngine.created(page)
     if (!opts.skipStorage) {
     if (!opts.skipStorage) {
       await WIKI.models.storage.pageEvent({
       await WIKI.models.storage.pageEvent({
         event: 'created',
         event: 'created',
@@ -245,6 +246,7 @@ module.exports = class Page extends Model {
       isPrivate: ogPage.isPrivate
       isPrivate: ogPage.isPrivate
     })
     })
     await WIKI.models.pages.renderPage(page)
     await WIKI.models.pages.renderPage(page)
+    await WIKI.data.searchEngine.updated(page)
     if (!opts.skipStorage) {
     if (!opts.skipStorage) {
       await WIKI.models.storage.pageEvent({
       await WIKI.models.storage.pageEvent({
         event: 'updated',
         event: 'updated',
@@ -273,6 +275,7 @@ module.exports = class Page extends Model {
     })
     })
     await WIKI.models.pages.query().delete().where('id', page.id)
     await WIKI.models.pages.query().delete().where('id', page.id)
     await WIKI.models.pages.deletePageFromCache(page)
     await WIKI.models.pages.deletePageFromCache(page)
+    await WIKI.data.searchEngine.deleted(page)
     if (!opts.skipStorage) {
     if (!opts.skipStorage) {
       await WIKI.models.storage.pageEvent({
       await WIKI.models.storage.pageEvent({
         event: 'deleted',
         event: 'deleted',

+ 15 - 16
server/models/searchEngines.js

@@ -95,11 +95,25 @@ module.exports = class SearchEngine extends Model {
     }
     }
   }
   }
 
 
-  static async initEngine() {
+  static async initEngine({ activate = false } = {}) {
     const searchEngine = await WIKI.models.searchEngines.query().findOne('isEnabled', true)
     const searchEngine = await WIKI.models.searchEngines.query().findOne('isEnabled', true)
     if (searchEngine) {
     if (searchEngine) {
       WIKI.data.searchEngine = require(`../modules/search/${searchEngine.key}/engine`)
       WIKI.data.searchEngine = require(`../modules/search/${searchEngine.key}/engine`)
       WIKI.data.searchEngine.config = searchEngine.config
       WIKI.data.searchEngine.config = searchEngine.config
+      if (activate) {
+        try {
+          await WIKI.data.searchEngine.activate()
+        } catch (err) {
+          // -> Revert to basic engine
+          if (err instanceof WIKI.Error.SearchActivationFailed) {
+            await WIKI.models.searchEngines.query().patch({ isEnabled: false }).where('key', searchEngine.key)
+            await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')
+            await WIKI.models.searchEngines.initEngine()
+          }
+          throw err
+        }
+      }
+
       try {
       try {
         await WIKI.data.searchEngine.init()
         await WIKI.data.searchEngine.init()
       } catch (err) {
       } catch (err) {
@@ -107,19 +121,4 @@ module.exports = class SearchEngine extends Model {
       }
       }
     }
     }
   }
   }
-
-  static async pageEvent({ event, page }) {
-    const searchEngines = await WIKI.models.storage.query().where('isEnabled', true)
-    if (searchEngines && searchEngines.length > 0) {
-      _.forEach(searchEngines, logger => {
-        WIKI.queue.job.syncStorage.add({
-          event,
-          logger,
-          page
-        }, {
-          removeOnComplete: true
-        })
-      })
-    }
-  }
 }
 }

+ 1 - 0
server/modules/search/algolia/definition.yml

@@ -4,6 +4,7 @@ description: Algolia is a powerful search-as-a-service solution, made easy to us
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/algolia.svg
 logo: https://static.requarks.io/logo/algolia.svg
 website: https://www.algolia.com/
 website: https://www.algolia.com/
+isAvailable: false
 props:
 props:
   appId:
   appId:
     type: String
     type: String

+ 1 - 0
server/modules/search/aws/definition.yml

@@ -4,4 +4,5 @@ description: Amazon CloudSearch is a managed service in the AWS Cloud that makes
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/aws-cloudsearch.svg
 logo: https://static.requarks.io/logo/aws-cloudsearch.svg
 website: https://aws.amazon.com/cloudsearch/
 website: https://aws.amazon.com/cloudsearch/
+isAvailable: false
 props: {}
 props: {}

+ 18 - 1
server/modules/search/azure/definition.yml

@@ -4,4 +4,21 @@ description: AI-Powered cloud search service for web and mobile app development.
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/azure.svg
 logo: https://static.requarks.io/logo/azure.svg
 website: https://azure.microsoft.com/services/search/
 website: https://azure.microsoft.com/services/search/
-props: {}
+isAvailable: true
+props:
+  serviceName:
+    type: String
+    title: Service Name
+    hint: The name of the Azure Search Service. Found under Properties.
+    order: 1
+  adminKey:
+    type: String
+    title: Admin API Key
+    hint: Either the primary or secondary admin key. Found under Keys.
+    order: 2
+  indexName:
+    type: String
+    title: Index Name
+    hint: 'Name to use when creating the index. (default: wiki)'
+    default: wiki
+    order: 3

+ 202 - 15
server/modules/search/azure/engine.js

@@ -1,26 +1,213 @@
-module.exports = {
-  activate() {
+const _ = require('lodash')
+const { SearchService, QueryType } = require('azure-search-client')
+const request = require('request-promise')
+const { pipeline } = require('stream')
 
 
+module.exports = {
+  async activate() {
+    // not used
   },
   },
-  deactivate() {
-
+  async deactivate() {
+    // not used
   },
   },
-  query() {
+  /**
+   * INIT
+   */
+  async init() {
+    this.client = new SearchService(this.config.serviceName, this.config.adminKey)
 
 
+    // -> Create Search Index
+    const indexes = await this.client.indexes.list()
+    if (!_.find(_.get(indexes, 'result.value', []), ['name', this.config.indexName])) {
+      await this.client.indexes.create({
+        name: this.config.indexName,
+        fields: [
+          {
+            name: 'id',
+            type: 'Edm.String',
+            key: true,
+            searchable: false
+          },
+          {
+            name: 'locale',
+            type: 'Edm.String',
+            searchable: false
+          },
+          {
+            name: 'path',
+            type: 'Edm.String',
+            searchable: false
+          },
+          {
+            name: 'title',
+            type: 'Edm.String',
+            searchable: true
+          },
+          {
+            name: 'description',
+            type: 'Edm.String',
+            searchable: true
+          },
+          {
+            name: 'content',
+            type: 'Edm.String',
+            searchable: true
+          }
+        ],
+        scoringProfiles: [
+          {
+            name: 'fieldWeights',
+            text: {
+              weights: {
+                title: 4,
+                description: 3,
+                content: 1
+              }
+            }
+          }
+        ],
+        suggesters: [
+          {
+            name: 'suggestions',
+            searchMode: 'analyzingInfixMatching',
+            sourceFields: ['title', 'description', 'content']
+          }
+        ],
+      })
+    }
   },
   },
-  created() {
-
+  /**
+   * QUERY
+   *
+   * @param {String} q Query
+   * @param {Object} opts Additional options
+   */
+  async query(q, opts) {
+    try {
+      let suggestions = []
+      const results = await this.client.indexes.use(this.config.indexName).search({
+        count: true,
+        scoringProfile: 'fieldWeights',
+        search: q,
+        select: 'id, locale, path, title, description',
+        queryType: QueryType.simple,
+        top: 50
+      })
+      if (results.result.value.length < 5) {
+        // Using plain request, not yet available in library...
+        try {
+          const suggestResults = await request({
+            uri: `https://${this.config.serviceName}.search.windows.net/indexes/${this.config.indexName}/docs/autocomplete`,
+            method: 'post',
+            qs: {
+              'api-version': '2017-11-11-Preview'
+            },
+            headers: {
+              'api-key': this.config.adminKey,
+              'Content-Type': 'application/json'
+            },
+            json: true,
+            body: {
+              autocompleteMode: 'oneTermWithContext',
+              search: q,
+              suggesterName: 'suggestions'
+            }
+          })
+          suggestions = suggestResults.value.map(s => s.queryPlusText)
+        } catch (err) {
+          WIKI.logger.warn('Search Engine suggestion failure: ', err)
+        }
+      }
+      return {
+        results: results.result.value,
+        suggestions,
+        totalHits: results.result['@odata.count']
+      }
+    } catch (err) {
+      WIKI.logger.warn('Search Engine Error:')
+      WIKI.logger.warn(err)
+    }
   },
   },
-  updated() {
-
+  /**
+   * CREATE
+   *
+   * @param {Object} page Page to create
+   */
+  async created(page) {
+    await this.client.indexes.use(this.config.indexName).index([
+      {
+        id: page.hash,
+        locale: page.localeCode,
+        path: page.path,
+        title: page.title,
+        description: page.description,
+        content: page.content
+      }
+    ])
   },
   },
-  deleted() {
-
+  /**
+   * UPDATE
+   *
+   * @param {Object} page Page to update
+   */
+  async updated(page) {
+    await this.client.indexes.use(this.config.indexName).index([
+      {
+        id: page.hash,
+        locale: page.localeCode,
+        path: page.path,
+        title: page.title,
+        description: page.description,
+        content: page.content
+      }
+    ])
   },
   },
-  renamed() {
-
+  /**
+   * DELETE
+   *
+   * @param {Object} page Page to delete
+   */
+  async deleted(page) {
+    await this.client.indexes.use(this.config.indexName).index([
+      {
+        '@search.action': 'delete',
+        id: page.hash
+      }
+    ])
   },
   },
-  rebuild() {
-
+  /**
+   * RENAME
+   *
+   * @param {Object} page Page to rename
+   */
+  async renamed(page) {
+    await this.client.indexes.use(this.config.indexName).index([
+      {
+        '@search.action': 'delete',
+        id: page.sourceHash
+      }
+    ])
+    await this.client.indexes.use(this.config.indexName).index([
+      {
+        id: page.destinationHash,
+        locale: page.localeCode,
+        path: page.destinationPath,
+        title: page.title,
+        description: page.description,
+        content: page.content
+      }
+    ])
+  },
+  /**
+   * REBUILD INDEX
+   */
+  async rebuild() {
+    await pipeline(
+      WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'content').select().from('pages').where({
+        isPublished: true,
+        isPrivate: false
+      }).stream(),
+      this.client.indexes.use(this.config.indexName).createIndexingStream()
+    )
   }
   }
 }
 }

+ 1 - 0
server/modules/search/db/definition.yml

@@ -4,4 +4,5 @@ description: Default basic database-based search engine.
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/database.svg
 logo: https://static.requarks.io/logo/database.svg
 website: https://www.requarks.io/
 website: https://www.requarks.io/
+isAvailable: true
 props: {}
 props: {}

+ 1 - 0
server/modules/search/elasticsearch/definition.yml

@@ -4,6 +4,7 @@ description: Elasticsearch is a distributed, RESTful search and analytics engine
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/elasticsearch.svg
 logo: https://static.requarks.io/logo/elasticsearch.svg
 website: https://www.elastic.co/products/elasticsearch
 website: https://www.elastic.co/products/elasticsearch
+isAvailable: false
 props:
 props:
   apiVersion:
   apiVersion:
     type: String
     type: String

+ 1 - 0
server/modules/search/manticore/definition.yml

@@ -4,4 +4,5 @@ description: High performance full-text search engine with SQL and JSON support.
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/manticore.svg
 logo: https://static.requarks.io/logo/manticore.svg
 website: https://manticoresearch.com/
 website: https://manticoresearch.com/
+isAvailable: false
 props: {}
 props: {}

+ 1 - 0
server/modules/search/postgres/definition.yml

@@ -4,6 +4,7 @@ description: Advanced PostgreSQL-based search engine.
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/postgresql.svg
 logo: https://static.requarks.io/logo/postgresql.svg
 website: https://www.requarks.io/
 website: https://www.requarks.io/
+isAvailable: true
 props:
 props:
   dictLanguage:
   dictLanguage:
     type: String
     type: String

+ 15 - 13
server/modules/search/postgres/engine.js

@@ -3,7 +3,9 @@ const tsquery = require('pg-tsquery')()
 
 
 module.exports = {
 module.exports = {
   async activate() {
   async activate() {
-    // not used
+    if (WIKI.config.db.type !== 'postgres') {
+      throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!')
+    }
   },
   },
   async deactivate() {
   async deactivate() {
     // not used
     // not used
@@ -75,7 +77,7 @@ module.exports = {
       INSERT INTO "pagesVector" (path, locale, title, description, tokens) VALUES (
       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'))
         '?', '?', '?', '?', (setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'A') || setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'B') || setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'C'))
       )
       )
-    `, [page.path, page.locale, page.title, page.description, page.title, page.description, page.content])
+    `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.content])
   },
   },
   /**
   /**
    * UPDATE
    * UPDATE
@@ -85,13 +87,13 @@ module.exports = {
   async updated(page) {
   async updated(page) {
     await WIKI.models.knex.raw(`
     await WIKI.models.knex.raw(`
       UPDATE "pagesVector" SET
       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 = '?' LIMIT 1
-    `, [page.title, page.description, page.title, page.description, page.content, page.path, page.locale])
+        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.content, page.path, page.localeCode])
   },
   },
   /**
   /**
    * DELETE
    * DELETE
@@ -100,7 +102,7 @@ module.exports = {
    */
    */
   async deleted(page) {
   async deleted(page) {
     await WIKI.models.knex('pagesVector').where({
     await WIKI.models.knex('pagesVector').where({
-      locale: page.locale,
+      locale: page.localeCode,
       path: page.path
       path: page.path
     }).del().limit(1)
     }).del().limit(1)
   },
   },
@@ -111,12 +113,12 @@ module.exports = {
    */
    */
   async renamed(page) {
   async renamed(page) {
     await WIKI.models.knex('pagesVector').where({
     await WIKI.models.knex('pagesVector').where({
-      locale: page.locale,
+      locale: page.localeCode,
       path: page.sourcePath
       path: page.sourcePath
     }).update({
     }).update({
-      locale: page.locale,
+      locale: page.localeCode,
       path: page.destinationPath
       path: page.destinationPath
-    }).limit(1)
+    })
   },
   },
   /**
   /**
    * REBUILD INDEX
    * REBUILD INDEX

+ 1 - 0
server/modules/search/solr/definition.yml

@@ -4,6 +4,7 @@ description: Solr is the popular, blazing-fast, open source enterprise search pl
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/solr.svg
 logo: https://static.requarks.io/logo/solr.svg
 website: http://lucene.apache.org/solr/
 website: http://lucene.apache.org/solr/
+isAvailable: false
 props:
 props:
   host:
   host:
     type: String
     type: String

+ 1 - 0
server/modules/search/sphinx/definition.yml

@@ -4,4 +4,5 @@ description: Sphinx is an open source full text search server, designed from the
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/sphinx.svg
 logo: https://static.requarks.io/logo/sphinx.svg
 website: http://sphinxsearch.com/
 website: http://sphinxsearch.com/
+isAvailable: false
 props: {}
 props: {}