KubeSphere中部署Wiki系統wiki.js並啟用中文全文檢索
背景
wiki.js 是優秀的開源 Wiki 系統,相較於 xwiki ,功能目前性上比 xwiki 不夠完善,但也在不斷進步。 Wiki 寫作、分享、權限管理功能還是有的,勝在 UI 設計很漂亮,能滿足小團隊的基本知識管理需求。
以下工作是在 KubeSphere 3.2.1 + Helm 3 已經部署好的情況下進行的。
部署 KuberSphere 的方法官網有很詳細的文檔介紹,這裡不再贅敘。 kubesphere.com.cn/docs/instal…
準備 storageclass
我們使用 OpenEBS 作為存儲,OpenEBS 默認安裝的 Local StorageSlass 在 Pod 銷毀後自動刪除,不適合用於我的 MySQL 存儲,我們在 Local StorageClass 基礎上稍作修改,創建新的 StorageClass,允許 Pod 銷毀後,PV 內容繼續保留,手動決定怎麼處理。
apiVersion: v1 items: - apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: annotations: cas.openebs.io/config: | - name: StorageType value: "hostpath" - name: BasePath value: "/var/openebs/localretain/" openebs.io/cas-type: local storageclass.beta.kubernetes.io/is-default-class: "false" storageclass.kubesphere.io/supported-access-modes: '["ReadWriteOnce"]' name: localretain provisioner: openebs.io/local reclaimPolicy: Retain volumeBindingMode: WaitForFirstConsumer kind: List metadata: resourceVersion: "" selfLink: ""
部署 PostgreSQL 數據庫
我們團隊其他項目中也需要使用 PostgreSQL, 為瞭提高 PostgreSQL 數據庫的利用率和統一管理,我們獨立部署 PostgreSQL,並在安裝 wiki.js 時,配置為使用外部數據庫。
準備用戶名密碼配置
我們使用 Secret 保存 PostgreSQL 用戶密碼等敏感信息。
kind: Secret apiVersion: v1 metadata: name: postgres-prod data: POSTGRES_PASSWORD: xxxx type: Opaque
以上 POSTGRES_PASSWORD 自行準備,為 base64 編碼的數據。
準備數據庫初始化腳本
使用 ConfigMap 保存數據庫初始化腳本,在 數據庫創建時,將 ConfigMap 中的數據庫初始化腳本掛載到 /docker-entrypoint-initdb.d, 容器初始化時會自動執行該腳本。
apiVersion: v1 kind: ConfigMap metadata: name: wikijs-postgres-init data: init.sql: |- CREATE DATABASE wikijs; CREATE USER wikijs with password 'xxxx'; GRANT CONNECT ON DATABASE wikijs to wikijs; GRANT USAGE ON SCHEMA public TO wikijs; GRANT SELECT,update,INSERT,delete ON ALL TABLES IN SCHEMA public TO wikijs; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO wikijs;
以上 wikijs 用戶的密碼自行準備,明文保存。
準備存儲
我們使用 KubeSphere 默認安裝的 OpenEBS 來提供存儲服務。可以通過創建 PVC 來提供持久化存儲。
這裡聲明一個 10G 的 PVC。
kind: PersistentVolumeClaim apiVersion: v1 metadata: name: postgres-prod-data finalizers: - kubernetes.io/pvc-protection spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: localretain volumeMode: Filesystem
部署 PostgreSQL 數據庫
在前面的步驟準備好各種配置信息和存儲後,就可以開始部署 PostgreSQL 服務瞭。
我們的 Kubernetes 沒有配置存儲陣列,使用的是 OpenEBS 作為存儲,采用 Deployment 方式部署 PostgreSQL。
apiVersion: apps/v1 kind: Deployment metadata: labels: app: postgres-prod name: postgres-prod spec: replicas: 1 selector: matchLabels: app: postgres-prod template: metadata: labels: app: postgres-prod spec: containers: - name: db imagePullPolicy: IfNotPresent image: 'abcfy2/zhparser:12-alpine' ports: - name: tcp-5432 protocol: TCP containerPort: 5432 envFrom: - secretRef: name: postgres-prod volumeMounts: - name: postgres-prod-data readOnly: false mountPath: /var/lib/postgresql/data - name: wikijs-postgres-init readOnly: true mountPath: /docker-entrypoint-initdb.d volumes: - name: postgres-prod-data persistentVolumeClaim: claimName: postgres-prod-data - name: wikijs-postgres-init configMap: name: wikijs-postgres-init
創建供其他 Pod 訪問的 Service
apiVersion: v1 kind: Service metadata: name: postgres-prod spec: selector: app: postgres-prod ports: - protocol: TCP port: 5432 targetPort: tcp-5432
完成 PostgreSQL 部署
測試略
部署 wiki.js
準備用戶名密碼配置
我們使用 Secret 保存 wiki.js 用於連接數據庫的用戶名密碼等敏感信息。
apiVersion: v1 kind: Secret metadata: name: wikijs data: DB_USER: d2lraWpz DB_PASS: xxxx type: Opaque
以上 DB_PASS 自行準備,為 base64 編碼的數據。
準備數據庫連接配置
我們使用 ConfigMap 保存 wiki.js 的數據庫連接信息。
apiVersion: v1 kind: ConfigMap metadata: name: wikijs data: DB_TYPE: postgres DB_HOST: postgres-prod.infra DB_PORT: "5432" DB_NAME: wikijs HA_ACTIVE: "true"
創建數據庫用戶和數據庫
如果 PostgreSQL 數據庫裡沒有創建 wikijs 用戶和數據 ,需要手工完成一下工作:
通過『數據庫工具』連接 PostgreSQL 數據庫,執行一下 SQL 語句,完成數據庫和用戶的創建、授權。
CREATE DATABASE wikijs; CREATE USER wikijs with password 'xxxx'; GRANT CONNECT ON DATABASE wikijs to wikijs; GRANT USAGE ON SCHEMA public TO wikijs; GRANT SELECT,update,INSERT,delete ON ALL TABLES IN SCHEMA public TO wikijs; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO wikijs;
以上 wikijs 的密碼自行修改。
準備 wiki.js 的 yaml 部署文件
采用 Deployment 方式 部署 wiki.js 的 yaml 文件如下:
# wikijs-deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: labels: app: wikijs name: wikijs spec: replicas: 1 selector: matchLabels: app: wikijs template: metadata: labels: app: wikijs spec: containers: - name: wikijs image: 'requarks/wiki:2' ports: - name: http-3000 protocol: TCP containerPort: 3000 envFrom: - secretRef: name: wikijs - configMapRef: name: wikijs
創建集群內訪問 wiki.js 的 Service
# wikijs-svc.yaml apiVersion: v1 kind: Service metadata: name: wikijs spec: selector: app: wikijs ports: - protocol: TCP port: 3000 targetPort: http-3000
創建集群外訪問的 Ingress
# wikijs-ing.yaml kind: Ingress apiVersion: networking.k8s.io/v1 metadata: name: wikijs spec: ingressClassName: nginx rules: - host: wiki.xxxx.cn http: paths: - path: / pathType: ImplementationSpecific backend: service: name: wikijs port: number: 3000
以上 host 域名需要自行配置。
執行部署
$ kubectl apply -f wikijs-deploy.yaml $ kubectl apply -f wikijs-svc.yaml $ kubectl apply -f wikijs-ing.yaml
配置 wiki.js 支持中文全文檢索
wiki.js 的全文檢索支持基於 PostgreSQL 的檢索,也支持 Elasticsearch 等,相對來說, PostgreSQL 比較輕量級,本項目中,我們使用 PostgreSQL 的全文檢索。
但是,因為 PostgreSQL 不支持中文分詞,需要額外安裝插件並配置啟用中文分詞,下面描述瞭為 wiki.js 啟動基於 PostgreSQL 數據庫中文分詞的全文檢索。
授予 wikijs 用戶臨時超管權限
通過數據庫管理工具登錄有超管權限的 PostgreSQL 用戶,臨時授予 wiki.js 用戶臨時超管權限,便於啟動中文分詞功能。
ALTER USER wikijs WITH SUPERUSER;
啟用數據庫的中文分詞能力
使用數據庫管理工具登錄 PostgreSQL 數據庫的 wikijs 用戶,執行以下命令,啟動數據庫的中文分詞功能。
CREATE EXTENSION pg_trgm; CREATE EXTENSION zhparser; CREATE TEXT SEARCH CONFIGURATION pg_catalog.chinese_zh (PARSER = zhparser); ALTER TEXT SEARCH CONFIGURATION chinese_zh ADD MAPPING FOR n,v,a,i,e,l WITH simple; -- 忽略標點影響 ALTER ROLE wikijs SET zhparser.punctuation_ignore = ON; -- 短詞復合 ALTER ROLE wikijs SET zhparser.multi_short = ON; -- 測試一下 select ts_debug('chinese_zh', '青春是最美好的年歲,青春是最燦爛的日子。每一個人的青春都無比寶貴,寶貴的青春隻有與奮鬥為伴才最閃光、最出彩。');
取消 wikijs 用戶的臨時超管權限
登錄 PostgreSQL 數據庫 wikijs 用戶,取消 wikijs 用戶的超管權限。
ALTER USER wikijs WITH NOSUPERUSER;
創建支持中文分詞的配置 ConfigMap
# zh-parse.yaml kind: ConfigMap apiVersion: v1 metadata: name: wikijs-zhparser data: definition.yml: |- key: postgres title: Database - PostgreSQL description: Advanced PostgreSQL-based search engine. author: requarks.io logo: https://static.requarks.io/logo/postgresql.svg website: https://www.requarks.io/ isAvailable: true props: dictLanguage: type: String title: Dictionary Language hint: Language to use when creating and querying text search vectors. default: english enum: - simple - danish - dutch - english - finnish - french - german - hungarian - italian - norwegian - portuguese - romanian - russian - spanish - swedish - turkish - chinese_zh order: 1 engine.js: |- const tsquery = require('pg-tsquery')() const stream = require('stream') const Promise = require('bluebird') const pipeline = Promise.promisify(stream.pipeline) /* global WIKI */ module.exports = { async activate() { if (WIKI.config.db.type !== 'postgres') { throw new WIKI.Error.SearchActivationFailed('Must use PostgreSQL database to activate this engine!') } }, async deactivate() { 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.`) }, /** * INIT */ async init() { WIKI.logger.info(`(SEARCH/POSTGRES) Initializing...`) // -> Create Search Index const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector') if (!indexExists) { WIKI.logger.info(`(SEARCH/POSTGRES) Creating Pages Vector table...`) await WIKI.models.knex.schema.createTable('pagesVector', table => { table.increments() table.string('path') table.string('locale') table.string('title') table.string('description') table.specificType('tokens', 'TSVECTOR') table.text('content') }) } // -> Create Words Index const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords') if (!wordsExists) { WIKI.logger.info(`(SEARCH/POSTGRES) Creating Words Suggestion Index...`) await WIKI.models.knex.raw(` CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat( 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"' )`) 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)`) } WIKI.logger.info(`(SEARCH/POSTGRES) Initialization completed.`) }, /** * QUERY * * @param {String} q Query * @param {Object} opts Additional options */ async query(q, opts) { try { let suggestions = [] let qry = ` SELECT id, path, locale, title, description FROM "pagesVector", to_tsquery(?,?) query 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) 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) } return { results: results.rows, suggestions, totalHits: results.rows.length } } catch (err) { WIKI.logger.warn('Search Engine Error:') WIKI.logger.warn(err) } }, /** * CREATE * * @param {Object} page Page to create */ async created(page) { await WIKI.models.knex.raw(` 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')) ) `, [page.path, page.localeCode, page.title, page.description, page.title, page.description, page.safeContent]) }, /** * UPDATE * * @param {Object} page Page to update */ async updated(page) { await WIKI.models.knex.raw(` 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 = ? `, [page.title, page.description, page.title, page.description, page.safeContent, page.path, page.localeCode]) }, /** * DELETE * * @param {Object} page Page to delete */ async deleted(page) { await WIKI.models.knex('pagesVector').where({ locale: page.localeCode, path: page.path }).del().limit(1) }, /** * RENAME * * @param {Object} page Page to rename */ async renamed(page) { await WIKI.models.knex('pagesVector').where({ locale: page.localeCode, path: page.path }).update({ locale: page.destinationLocaleCode, path: page.destinationPath }) }, /** * REBUILD INDEX */ async rebuild() { WIKI.logger.info(`(SEARCH/POSTGRES) Rebuilding Index...`) await WIKI.models.knex('pagesVector').truncate() 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() } }) ) await WIKI.models.knex.raw(` INSERT INTO "pagesWords" (word) SELECT word FROM ts_stat( 'SELECT to_tsvector(''simple'', "title") || to_tsvector(''simple'', "description") || to_tsvector(''simple'', "content") FROM "pagesVector"' ) `) WIKI.logger.info(`(SEARCH/POSTGRES) Index rebuilt successfully.`) } }
更新 wikijs 的 Deployment
wiki.js 的基於 PostgreSQL 的全文檢索引擎配置位於 /wiki/server/modules/search/postgres ,我們將前面配置的 ConfigMap 加載到這個目錄。
# wikijs-zh.yaml kind: Deployment apiVersion: apps/v1 metadata: name: wikijs labels: app: wikijs spec: replicas: 1 selector: matchLabels: app: wikijs template: metadata: labels: app: wikijs spec: volumes: - name: volume-dysh4f configMap: name: wikijs-zhparser defaultMode: 420 containers: - name: wikijs image: 'requarks/wiki:2' ports: - name: http-3000 containerPort: 3000 protocol: TCP envFrom: - secretRef: name: wikijs - configMapRef: name: wikijs volumeMounts: - name: volume-dysh4f readOnly: true mountPath: /wiki/server/modules/search/postgres
配置 wiki.js ,啟用基於 PostgreSQL 的全文檢索
- 重新 apply 新的 Delployment 文件後
$ kubectl apply -f zh-parse.yaml $ kubectl apply -f wikijs-zh.yaml
- 打開 wiki.js 管理
- 點擊搜索引擎
- 選擇 Database – PostgreSQL
- 在 Dictionary Language 的下拉菜單裡選擇 chinese_zh。
- 點擊應用,並重建索引。
- 完成配置。
總結
本文介紹的 wiki.js 部署方式支持中文全文檢索的支持,集成瞭 PostgreSQL 和 zhparser 中文分詞插件。
相對於標準的 wiki.js 安裝部署過程,主要做瞭以下配置:
- PostgreSQL 鏡像采用瞭 abcfy2/zhparser:12-alpine ,這個鏡像自帶 zhparser 中文分詞插件。
- wiki.js 鏡像外掛瞭 ConfigMap ,用於修改原 Docker 鏡像裡關於 PostgreSQL 搜索引擎配置的信息,以支持 chinese_zh 選項。
以上就是KubeSphere中部署Wiki系統wiki.js並啟用中文全文檢索的詳細內容,更多關於KubeSphere部署wiki.js並啟用的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 詳解k8s ConfigMap 中 subPath 字段和 items 字段
- kubernetes之statefulset搭建MySQL集群
- Kubernetes k8s configmap 容器技術解析
- 關於k8s中subpath的使用詳解
- 查看postgresql數據庫用戶系統權限、對象權限的方法