UNPKG

nodebb-plugin-dbsearch

Version:
183 lines (155 loc) 7.5 kB
'use strict'; const winston = require.main.require('winston'); const nconf = require.main.require('nconf'); const db = require.main.require('./src/database'); const pubsub = require.main.require('./src/pubsub'); let searchLanguage = 'english'; pubsub.on('dbsearch-language-changed', (e) => { searchLanguage = e.data; }); async function initDB() { await db.pool.query('CREATE TABLE IF NOT EXISTS "searchtopic" ( "id" TEXT NOT NULL PRIMARY KEY, "content" TEXT, "uid" TEXT, "cid" TEXT )'); await db.pool.query(`CREATE INDEX IF NOT EXISTS "idx__searchtopic__content" ON "searchtopic" USING GIN (to_tsvector('${searchLanguage}', "content"))`); await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchtopic__uid" ON "searchtopic"("uid")'); await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchtopic__cid" ON "searchtopic"("cid")'); await db.pool.query('CREATE TABLE IF NOT EXISTS "searchpost" ( "id" TEXT NOT NULL PRIMARY KEY, "content" TEXT, "uid" TEXT, "cid" TEXT )'); await db.pool.query(`CREATE INDEX IF NOT EXISTS "idx__searchpost__content" ON "searchpost" USING GIN (to_tsvector('${searchLanguage}', "content"))`); await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchpost__uid" ON "searchpost"("uid")'); await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchpost__cid" ON "searchpost"("cid")'); await db.pool.query('CREATE TABLE IF NOT EXISTS "searchchat" ( "id" TEXT NOT NULL PRIMARY KEY, "content" TEXT, "rid" BIGINT, "uid" TEXT )'); await db.pool.query(`CREATE INDEX IF NOT EXISTS "idx__searchchat__content" ON "searchchat" USING GIN (to_tsvector('${searchLanguage}', "content"))`); await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchchat__rid" ON "searchchat"("rid")'); await db.pool.query('CREATE INDEX IF NOT EXISTS "idx__searchchat__uid" ON "searchchat"("uid")'); } async function handleError(err) { if (err && err.code === '42P01') { winston.warn('dbsearch was not initialized'); await initDB(); return; } throw err; } exports.createIndices = async function (language) { searchLanguage = language; if (nconf.get('isPrimary') && !nconf.get('jobsDisabled')) { await initDB(); } }; exports.changeIndexLanguage = async function (language) { searchLanguage = language; pubsub.publish('dbsearch-language-changed', language); await db.pool.query('DROP INDEX "idx__searchtopic__content"'); await db.pool.query(`CREATE INDEX "idx__searchtopic__content" ON "searchtopic" USING GIN (to_tsvector('${language}', "content"))`); await db.pool.query('DROP INDEX "idx__searchpost__content"'); await db.pool.query(`CREATE INDEX "idx__searchpost__content" ON "searchpost" USING GIN (to_tsvector('${language}', "content"))`); await db.pool.query('DROP INDEX "idx__searchchat__content"'); await db.pool.query(`CREATE INDEX "idx__searchchat__content" ON "searchchat" USING GIN (to_tsvector('${language}', "content"))`); }; exports.searchIndex = async function (key, data, ids) { if (!ids.length) { return; } ids = ids.map(String); try { await db.pool.query({ name: `dbsearch-searchIndex-${key}`, text: `INSERT INTO "search${key}" SELECT d."id", d."data"->>'content' "content", (d."data"->>'uid')::text "uid", (d."data"->>'cid')::text "cid" FROM UNNEST($1::text[], $2::jsonb[]) d("id", "data") ON CONFLICT ("id") DO UPDATE SET "content" = COALESCE(EXCLUDED."content", "search${key}"."content"), "uid" = COALESCE(EXCLUDED."uid", "search${key}"."uid"), "cid" = COALESCE(EXCLUDED."cid", "search${key}"."cid")`, values: [ids, data], }); } catch (err) { winston.error(`Error indexing ${err.stack}`); await handleError(err); await exports.searchIndex(key, data, ids); } }; exports.search = async function (key, data, limit) { if (Array.isArray(data.uid) && data.uid.filter(Boolean).length) { data.uid = data.uid.filter(Boolean); } else { data.uid = null; } if (Array.isArray(data.cid) && data.cid.filter(Boolean).length) { data.cid = data.cid.filter(Boolean); } else { data.cid = null; } try { const res = await db.pool.query({ name: `dbsearch-search-${key}`, text: `SELECT ARRAY(SELECT s."id" FROM "search${key}" s WHERE ($1::text IS NULL OR to_tsvector($5::regconfig, "content") @@ plainto_tsquery($5::regconfig, $1::text)) AND ($2::text[] IS NULL OR "uid" = ANY($2::text[])) AND ($3::text[] IS NULL OR "cid" = ANY($3::text[])) ORDER BY ts_rank_cd(to_tsvector($5::regconfig, "content"), plainto_tsquery($5::regconfig, $1::text)) DESC, s."id" ASC LIMIT $4::integer) r`, values: [data.content, data.uid, data.cid, parseInt(limit, 10), searchLanguage], }); return res.rows[0].r; } catch (err) { await handleError(err); return []; } }; exports.searchRemove = async function (key, ids) { if (!key || !ids.length) { return; } ids = ids.map(String); try { await db.pool.query({ name: `dbsearch-searchRemove-${key}`, text: `DELETE FROM "search${key}" s WHERE s."id" = ANY($1::text[])`, values: [ids], }); } catch (err) { await handleError(err); } }; exports.chat = {}; exports.chat.index = async (data, ids) => { if (!ids.length) { return; } ids = ids.map(String); try { await db.pool.query({ name: `dbsearch-searchIndex-chat`, text: `INSERT INTO "searchchat" SELECT d."id", d."data"->>'content' "content", (d."data"->>'rid')::bigint "rid", (d."data"->>'uid')::text "uid" FROM UNNEST($1::text[], $2::jsonb[]) d("id", "data") ON CONFLICT ("id") DO UPDATE SET "content" = COALESCE(EXCLUDED."content", "searchchat"."content"), "uid" = COALESCE(EXCLUDED."uid", "searchchat"."uid"), "rid" = COALESCE(EXCLUDED."rid", "searchchat"."rid")`, values: [ids, data], }); } catch (err) { winston.error(`Error indexing ${err.stack}`); await handleError(err); await exports.chat.index(data, ids); } }; exports.chat.search = async (data, limit) => { if (Array.isArray(data.uid) && data.uid.filter(Boolean).length) { data.uid = data.uid.filter(Boolean); } else { data.uid = null; } if (Array.isArray(data.roomId) && data.roomId.filter(Boolean).length) { data.roomId = data.roomId.filter(Boolean); } else { data.roomId = null; } try { const res = await db.pool.query({ name: `dbsearch-search-chat`, text: `SELECT ARRAY(SELECT s."id" FROM "searchchat" s WHERE ($1::text IS NULL OR to_tsvector($5::regconfig, "content") @@ plainto_tsquery($5::regconfig, $1::text)) AND ($2::text[] IS NULL OR "uid" = ANY($2::text[])) AND ($3::bigint[] IS NULL OR "rid" = ANY($3::bigint[])) ORDER BY ts_rank_cd(to_tsvector($5::regconfig, "content"), plainto_tsquery($5::regconfig, $1::text)) DESC, s."id" ASC LIMIT $4::integer) r`, values: [data.content, data.uid, data.roomId, parseInt(limit, 10), searchLanguage], }); return res.rows[0].r; } catch (err) { await handleError(err); return []; } }; exports.getIndexedTopicCount = async () => { const res = await db.pool.query(`SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = 'searchtopic'`); return res.rows && res.rows[0] ? parseInt(res.rows[0].estimate, 10) : 0; }; exports.getIndexedPostCount = async () => { const res = await db.pool.query(`SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = 'searchpost'`); return res.rows && res.rows[0] ? parseInt(res.rows[0].estimate, 10) : 0; }; exports.getIndexedChatMessageCount = async () => { const res = await db.pool.query(`SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = 'searchchat'`); return res.rows && res.rows[0] ? parseInt(res.rows[0].estimate, 10) : 0; };