CMS Search

This commit is contained in:
cgfhtw
2024-10-14 11:22:20 +02:00
parent 322544c7fb
commit 123f29a750
9 changed files with 298 additions and 7 deletions
+94
View File
@@ -671,3 +671,97 @@ $config['room'] = [
LEFT JOIN public.tbl_adresse address
USING (adresse_id)"
];
$config['cms'] = [
'primarykey' => 'contentsprache_id',
'table' => 'cms',
'prepare' => "
cms_auth (content_id) AS (
SELECT content_id
FROM campus.tbl_content c
WHERE NOT EXISTS (SELECT 1 FROM campus.tbl_contentgruppe g WHERE g.content_id=c.content_id)
UNION
SELECT content_id
FROM public.vw_gruppen g
JOIN campus.tbl_contentgruppe c USING (gruppe_kurzbz)
WHERE uid = (TABLE auth)
),
cms_active (content_id, template_kurzbz) AS (
SELECT content_id, template_kurzbz
FROM cms_auth
JOIN campus.tbl_content USING (content_id)
WHERE aktiv = TRUE
),
cms_active_redirect (content_id) AS (
SELECT content_id
FROM cms_active
WHERE template_kurzbz = 'redirect'
),
cms_active_redirect_linked (content_id) AS (
SELECT content_id
FROM cms_active_redirect
JOIN campus.tbl_contentsprache USING (content_id)
WHERE LEFT((xpath('string(/content/url)', content))[1]::text, 1) <> '#'
),
cms_active_others (content_id) AS (
SELECT content_id
FROM cms_active
WHERE template_kurzbz IN ('contentmittitel', 'contentohnetitel', 'contentmittitel_filterwidget')
),
cms (contentsprache_id) AS (
SELECT contentsprache_id
FROM campus.tbl_contentsprache
WHERE content_id IN (
SELECT content_id
FROM cms_active_redirect_linked
UNION
SELECT content_id
FROM cms_active_others
)
AND version = campus.get_highest_content_version(content_id)
)
",
'searchfields' => [
'content' => [
'alias' => ['inhalt'],
'comparison' => "vector",
'field' => "(setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B'))",
'join' => [
'table' => "campus.tbl_contentsprache",
'using' => "contentsprache_id"
]
],
'content_id' => [
'alias' => ['id'],
'comparison' => "equal-int",
'field' => "content_id",
'join' => [
'table' => "campus.tbl_contentsprache",
'using' => "contentsprache_id"
]
],
'lang' => [
'alias' => ['language', 'sprache'],
'comparison' => "equals",
'field' => "sprache",
'join' => [
'table' => "campus.tbl_contentsprache",
'using' => "contentsprache_id"
]
]
],
'resultfields' => [
"contentsprache.content_id",
"content.template_kurzbz",
"contentsprache.version",
"contentsprache.sprache AS language",
"contentsprache.titel AS title",
"contentsprache.content",
"(xpath('string(/content/url)', contentsprache.content))[1] AS content_url"
],
'resultjoin' => "
JOIN campus.tbl_contentsprache contentsprache
USING (contentsprache_id)
JOIN campus.tbl_content content
USING (content_id)"
];
+1 -1
View File
@@ -25,7 +25,7 @@ $config['similar'] = [
$config['vector'] = [
'priority' => 1,
'rank' => "ts_rank_cd({field}, to_tsquery('simple', {word}))",
'rank' => "ts_rank({field}, to_tsquery('simple', {word}))",
'compare' => "to_tsquery('simple', {word}) @@ {field}"
];
@@ -0,0 +1,47 @@
<?php
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
* This controller operates between (interface) the JS (GUI) and the back-end
* Provides data to the ajax get calls about languages
* This controller works with JSON calls on the HTTP GET and the output is always JSON
*/
class Language extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'get' => self::PERM_LOGGED
]);
// Load models
$this->load->model('system/Sprache_model', 'SpracheModel');
}
public function get()
{
$this->SpracheModel->addOrder('sprache');
$result = $this->SpracheModel->load();
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
}
}
+6
View File
@@ -168,6 +168,9 @@ class SearchBarLib
FROM public.tbl_sprache
WHERE sprache=" . $this->_ci->db->escape($lang) . "
LIMIT 1
),
auth (uid) AS (
SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid
)";
if ($sql_with) {
@@ -248,6 +251,9 @@ class SearchBarLib
WHERE sprache=" . $this->_ci->db->escape($lang) . "
LIMIT 1
)");
array_unshift($with, "auth (uid) AS (
SELECT " . $this->_ci->db->escape(getAuthUID()) . " AS uid
)");
return success("
WITH " . implode(", ", $with) . "
+3 -1
View File
@@ -23,6 +23,7 @@ import studstatus from "./studstatus.js";
import stv from "./stv.js";
import notiz from "./notiz.js";
import betriebsmittel from "./betriebsmittel.js";
import language from "./language.js";
export default {
search,
@@ -32,5 +33,6 @@ export default {
studstatus,
stv,
notiz,
betriebsmittel
betriebsmittel,
language
};
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAll() {
return this.$fhcApi.get('/api/frontend/v1/language/get');
}
};
@@ -0,0 +1,81 @@
import TemplateFrame from "./template/frame.js";
export default {
components: {
TemplateFrame
},
emits: [ 'actionexecuted' ],
props: {
res: Object,
actions: Object
},
inject: [
'languages',
'query'
],
computed: {
preview() {
if (this.res.template_kurzbz != 'redirect') {
let text = this.res.content.replace(/<!\[CDATA\[|\]\]>/ig, '').replace(/<[^>]+>/ig, '').replace(/^\s+|\s+$/g, '');
if (text.length > 1000) {
// NOTE(chris): focus on searched text!
let lower = text.toLowerCase();
let firstOccurence = Math.min(this.query.split(' ').reduce((a, c) => {
// NOTE(chris): filter query for words that affects the content field and get the lowest index of them
if (c == 'or')
return a;
let i = c.indexOf(':');
if (i < 0 || (i > 0 && ['content', 'inhalt'].includes(c.split(':')[0]))) {
let posInText = lower.indexOf(c);
if (posInText >= 0)
a.push(posInText);
}
return a;
}, []));
if (firstOccurence) {
if (firstOccurence + 997 >= text.length) {
firstOccurence = text.length - 997;
if (firstOccurence > 0)
return '...' + text.substr(firstOccurence, 997);
} else {
return '...' + text.substr(firstOccurence, 994) + '...';
}
}
text = text.substr(0, 997) + '...';
}
return text;
}
let url = this.res.content_url;
if (url.substr(0, 16) == '../index.ci.php/')
url = this.$fhcApi.getUri(url.substr(16));
else if (url.substr(0, 3) == '../')
url = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/\/+$/, '') + url.substr(2);
return '<a href="' + url + '">' + url + '</a>';
},
flag() {
if (!this.languages || !this.languages[this.res.language])
return "";
return "data:image/jpeg;base64," + this.languages[this.res.language].flagge;
}
},
template: `
<template-frame
class="searchbar-result-cms"
:res="res"
:actions="actions"
:title="res.title"
image-fallback="fas fa-newspaper fa-4x p-4 text-white bg-primary"
@actionexecuted="$emit('actionexecuted')"
>
<img v-if="flag" :src="flag" class="ms-2">
<div v-if="preview" class="searchbar_table" v-html="preview"></div>
<div v-else class="searchbar_table text-muted">
No Content
</div>
</template-frame>`
};
+29 -5
View File
@@ -4,6 +4,7 @@ import ResultPrestudent from "./result/prestudent.js";
import ResultEmployee from "./result/employee.js";
import ResultOrganisationunit from "./result/organisationunit.js";
import ResultRoom from "./result/room.js";
import ResultCms from "./result/cms.js";
import ResultMergedperson from "./result/mergedperson.js";
import ResultMergedstudent from "./result/mergedstudent.js";
@@ -17,10 +18,17 @@ export default {
ResultEmployee,
ResultOrganisationunit,
ResultRoom,
ResultCms,
ResultMergedperson,
ResultMergedstudent
},
props: [ "searchoptions", "searchfunction" ],
provide() {
return {
languages: Vue.computed(() => this.languages),
query: Vue.computed(() => this.lastQuery)
};
},
data() {
return {
searchtimer: null,
@@ -35,9 +43,22 @@ export default {
searching: false,
error: null,
abortController: null,
retry: 5
retry: 0,
languages: null,
lastQuery: ''
};
},
created() {
this.$fhcApi.factory
.language.getAll()
.then(result => {
this.languages = result.data.reduce((a, c) => {
a[c.sprache] = c;
return a;
}, {});
})
.catch(this.$fhcAlert.handleSystemError);
},
beforeMount() {
this.updateSearchOptions();
},
@@ -82,12 +103,14 @@ export default {
this.abortController = new AbortController();
this
.searchfunction(this.searchsettings, { signal: this.abortController.signal })
.searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal })
.then(response => {
if (!response.data) {
this.error = 'Bei der Suche ist ein Fehler aufgetreten.';
} else {
let res = response.data.map(el => ({...el, ...JSON.parse(el.data)}));
this.lastQuery = response.meta.searchstring;
if (this.searchoptions.mergeResults) {
let counter = 0;
let mergeTypes = [];
@@ -131,11 +154,11 @@ export default {
this.searchresult = res;
}
this.searching = false;
this.retry = 5;
this.retry = 0;
})
.catch(error => {
if (error.code == "ERR_CANCELED") {
return this.retry = 5;
return this.retry = 0;
}
if (error.code == "ECONNABORTED" && this.retry) {
this.retry--;
@@ -144,7 +167,7 @@ export default {
this.error = 'Bei der Suche ist ein Fehler aufgetreten.' + ' ' + error.message;
this.searching = false;
this.retry = 5;
this.retry = 0;
});
},
refreshsearch() {
@@ -239,6 +262,7 @@ export default {
<result-employee v-else-if="res.type === 'unassigned_employee'" :res="res" :actions="searchoptions.actions.employee" @actionexecuted="hideresult"></result-employee>
<result-organisationunit v-else-if="res.type === 'organisationunit'" :res="res" :actions="searchoptions.actions.organisationunit" @actionexecuted="hideresult"></result-organisationunit>
<result-room v-else-if="res.type === 'room'" :res="res" :actions="searchoptions.actions.room" @actionexecuted="hideresult"></result-room>
<result-cms v-else-if="res.type === 'cms'" :res="res" :actions="searchoptions.actions.cms" @actionexecuted="hideresult"></result-cms>
<result-mergedperson v-else-if="res.type === 'mergedperson'" :res="res" :actions="searchoptions.actions.mergedperson" @actionexecuted="hideresult"></result-mergedperson>
<result-mergedstudent v-else-if="res.type === 'mergedstudent'" :res="res" :actions="searchoptions.actions.mergedstudent" @actionexecuted="hideresult"></result-mergedstudent>
<div v-else>Unbekannter Ergebnistyp: '{{ res.type }}'.</div>
+15
View File
@@ -169,3 +169,18 @@ FROM pg_indexes WHERE indexname = 'idx_tbl_organisationseinheit_fts_bezeichnung_
else
echo 'public.tbl_organisationseinheit: added index "idx_tbl_organisationseinheit_fts_bezeichnung_vector"<br>';
}
// Add index for titel || ' ' || content to campus.tbl_contentsprache
if (!$db->db_num_rows(@$db->db_query("SELECT 1
FROM pg_indexes WHERE indexname = 'idx_tbl_contentsprache_fts_titel_content_vector' LIMIT 1;")))
{
$qry = "
CREATE INDEX idx_tbl_contentsprache_fts_titel_content_vector
ON campus.tbl_contentsprache
USING GIN ((setweight(to_tsvector('simple', COALESCE(titel, '')), 'A') || setweight(to_tsvector('simple', COALESCE(content, '')::text), 'B')));
";
if (!$db->db_query($qry))
echo '<strong>campus.tbl_contentsprache ' . $db->db_last_error() . '</strong><br>';
else
echo 'campus.tbl_contentsprache: added index "idx_tbl_contentsprache_fts_titel_content_vector"<br>';
}