feat: postgres search engine
This commit is contained in:
parent
b5db531234
commit
ab42e5e1ab
@ -2,18 +2,18 @@
|
|||||||
v-toolbar.nav-header(color='black', dark, app, clipped-left, fixed, flat, :extended='searchIsShown && $vuetify.breakpoint.smAndDown')
|
v-toolbar.nav-header(color='black', dark, app, clipped-left, fixed, flat, :extended='searchIsShown && $vuetify.breakpoint.smAndDown')
|
||||||
v-toolbar(color='deep-purple', flat, slot='extension', v-if='searchIsShown && $vuetify.breakpoint.smAndDown')
|
v-toolbar(color='deep-purple', flat, slot='extension', v-if='searchIsShown && $vuetify.breakpoint.smAndDown')
|
||||||
v-text-field(
|
v-text-field(
|
||||||
ref='searchFieldMobile',
|
ref='searchFieldMobile'
|
||||||
v-model='search',
|
v-model='search'
|
||||||
clearable,
|
clearable
|
||||||
background-color='deep-purple'
|
background-color='deep-purple'
|
||||||
color='white',
|
color='white'
|
||||||
label='Search...',
|
label='Search...'
|
||||||
single-line,
|
single-line
|
||||||
solo
|
solo
|
||||||
flat
|
flat
|
||||||
hide-details,
|
hide-details
|
||||||
prepend-inner-icon='search',
|
prepend-inner-icon='search'
|
||||||
:loading='searchIsLoading',
|
:loading='searchIsLoading'
|
||||||
@keyup.enter='searchEnter'
|
@keyup.enter='searchEnter'
|
||||||
)
|
)
|
||||||
v-layout(row)
|
v-layout(row)
|
||||||
@ -73,7 +73,9 @@
|
|||||||
prepend-inner-icon='search',
|
prepend-inner-icon='search',
|
||||||
:loading='searchIsLoading',
|
:loading='searchIsLoading',
|
||||||
@keyup.enter='searchEnter'
|
@keyup.enter='searchEnter'
|
||||||
@keyup.esc='search = ``'
|
@keyup.esc='searchClose'
|
||||||
|
@focus='searchFocus'
|
||||||
|
@blur='searchBlur'
|
||||||
)
|
)
|
||||||
v-progress-linear(
|
v-progress-linear(
|
||||||
indeterminate,
|
indeterminate,
|
||||||
@ -191,6 +193,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
search: sync('site/search'),
|
search: sync('site/search'),
|
||||||
|
searchIsFocused: sync('site/searchIsFocused'),
|
||||||
searchIsLoading: sync('site/searchIsLoading'),
|
searchIsLoading: sync('site/searchIsLoading'),
|
||||||
searchRestrictLocale: sync('site/searchRestrictLocale'),
|
searchRestrictLocale: sync('site/searchRestrictLocale'),
|
||||||
searchRestrictPath: sync('site/searchRestrictPath'),
|
searchRestrictPath: sync('site/searchRestrictPath'),
|
||||||
@ -231,6 +234,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
searchFocus() {
|
||||||
|
this.searchIsFocused = true
|
||||||
|
},
|
||||||
|
searchBlur() {
|
||||||
|
this.searchIsFocused = false
|
||||||
|
},
|
||||||
|
searchClose() {
|
||||||
|
this.search = ''
|
||||||
|
this.searchBlur()
|
||||||
|
},
|
||||||
searchToggle() {
|
searchToggle() {
|
||||||
this.searchIsShown = !this.searchIsShown
|
this.searchIsShown = !this.searchIsShown
|
||||||
if (this.searchIsShown) {
|
if (this.searchIsShown) {
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.search-results(v-if='search.length > 1')
|
.search-results(v-if='searchIsFocused || search.length > 1')
|
||||||
.search-results-container
|
.search-results-container
|
||||||
.search-results-loader(v-if='searchIsLoading && results.length < 1')
|
.search-results-help(v-if='search.length < 2')
|
||||||
|
img(src='/svg/icon-search-alt.svg')
|
||||||
|
.mt-4 Type at least 2 characters to start searching...
|
||||||
|
.search-results-loader(v-else-if='searchIsLoading && results.length < 1')
|
||||||
orbit-spinner(
|
orbit-spinner(
|
||||||
:animation-duration='1000'
|
:animation-duration='1000'
|
||||||
:size='100'
|
:size='100'
|
||||||
color='#FFF'
|
color='#FFF'
|
||||||
)
|
)
|
||||||
.headline.mt-5 Searching...
|
.headline.mt-5 Searching...
|
||||||
.search-results-none(v-if='!searchIsLoading && results.length < 1')
|
.search-results-none(v-else-if='!searchIsLoading && results.length < 1')
|
||||||
img(src='/svg/icon-no-results.svg', alt='No Results')
|
img(src='/svg/icon-no-results.svg', alt='No Results')
|
||||||
.subheading No pages matching your query.
|
.subheading No pages matching your query.
|
||||||
template(v-if='results.length > 0')
|
template(v-if='results.length > 0')
|
||||||
@ -41,7 +44,7 @@
|
|||||||
v-list-tile-content
|
v-list-tile-content
|
||||||
v-list-tile-title(v-html='term')
|
v-list-tile-title(v-html='term')
|
||||||
v-divider(v-if='idx < suggestions.length - 1')
|
v-divider(v-if='idx < suggestions.length - 1')
|
||||||
.text-xs-center.pt-4
|
.text-xs-center.pt-5(v-if='search.length > 1')
|
||||||
v-btn(outline, color='orange', @click='search = ``', v-if='results.length > 0')
|
v-btn(outline, color='orange', @click='search = ``', v-if='results.length > 0')
|
||||||
v-icon(left) save
|
v-icon(left) save
|
||||||
span Copy Search Link
|
span Copy Search Link
|
||||||
@ -73,6 +76,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
search: sync('site/search'),
|
search: sync('site/search'),
|
||||||
|
searchIsFocused: sync('site/searchIsFocused'),
|
||||||
searchIsLoading: sync('site/searchIsLoading'),
|
searchIsLoading: sync('site/searchIsLoading'),
|
||||||
searchRestrictLocale: sync('site/searchRestrictLocale'),
|
searchRestrictLocale: sync('site/searchRestrictLocale'),
|
||||||
searchRestrictPath: sync('site/searchRestrictPath'),
|
searchRestrictPath: sync('site/searchRestrictPath'),
|
||||||
@ -89,6 +93,13 @@ export default {
|
|||||||
return this.response.totalHits > 0 ? 0 : Math.ceil(this.response.totalHits / 10)
|
return this.response.totalHits > 0 ? 0 : Math.ceil(this.response.totalHits / 10)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
search(newValue, oldValue) {
|
||||||
|
if (newValue.length < 2) {
|
||||||
|
this.response.results = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setSearchTerm(term) {
|
setSearchTerm(term) {
|
||||||
this.search = term
|
this.search = term
|
||||||
@ -102,7 +113,8 @@ export default {
|
|||||||
query: this.search
|
query: this.search
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'network-only',
|
||||||
|
debounce: 300,
|
||||||
throttle: 1000,
|
throttle: 1000,
|
||||||
skip() {
|
skip() {
|
||||||
return !this.search || this.search.length < 2
|
return !this.search || this.search.length < 2
|
||||||
@ -138,6 +150,18 @@ export default {
|
|||||||
max-width: 1024px;
|
max-width: 1024px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-help {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #FFF;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 104px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-loader {
|
&-loader {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
2
client/static/svg/icon-search-alt.svg
Normal file
2
client/static/svg/icon-search-alt.svg
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 192 192" width="104px" height="104px"><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,192v-192h192v192z" fill="none"/><g fill="#ffffff"><g id="surface1"><path d="M73.84615,1.38462c-40.03846,0 -72.46154,32.42308 -72.46154,72.46154c0,40.03846 32.42308,72.46154 72.46154,72.46154c16.90385,0 32.45192,-5.97116 44.76923,-15.69231l6.46154,6.46154c-2.71153,5.10577 -1.75961,11.625 2.53846,15.92308l33.92308,34.15385c5.27885,5.27885 13.875,5.27885 19.15385,0l6.46154,-6.46154c5.27885,-5.27885 5.27885,-13.875 0,-19.15385l-34.15385,-33.92308c-4.32692,-4.32692 -10.81731,-5.07692 -15.92308,-2.30769l-6.46154,-6.46154c9.77885,-12.34615 15.69231,-28.00962 15.69231,-45c0,-40.03846 -32.42308,-72.46154 -72.46154,-72.46154zM73.84615,14.76923c32.625,0 59.07692,26.45192 59.07692,59.07692c0,32.625 -26.45192,59.07692 -59.07692,59.07692c-32.625,0 -59.07692,-26.45192 -59.07692,-59.07692c0,-32.625 26.45192,-59.07692 59.07692,-59.07692zM36.46154,55.15385c-3.80769,6.17308 -6,13.44231 -6,21.23077c0,22.35577 18.02884,40.38462 40.38462,40.38462c8.625,0 16.73077,-2.79808 23.30769,-7.38462c-1.75961,0.20192 -3.72115,0.23077 -5.53846,0.23077c-28.90384,0 -52.15385,-23.25 -52.15385,-52.15385c0,-0.77885 -0.02884,-1.52884 0,-2.30769z"/></g></g></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -8,6 +8,7 @@ const state = {
|
|||||||
mascot: true,
|
mascot: true,
|
||||||
title: siteConfig.title,
|
title: siteConfig.title,
|
||||||
search: '',
|
search: '',
|
||||||
|
searchIsFocused: false,
|
||||||
searchIsLoading: false,
|
searchIsLoading: false,
|
||||||
searchRestrictLocale: false,
|
searchRestrictLocale: false,
|
||||||
searchRestrictPath: false
|
searchRestrictPath: false
|
||||||
|
2
dev/search-engines/solr/solrconfig.xml
Normal file
2
dev/search-engines/solr/solrconfig.xml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<config>
|
||||||
|
</config>
|
@ -135,6 +135,7 @@
|
|||||||
"pem-jwk": "2.0.0",
|
"pem-jwk": "2.0.0",
|
||||||
"pg": "7.8.0",
|
"pg": "7.8.0",
|
||||||
"pg-hstore": "2.3.2",
|
"pg-hstore": "2.3.2",
|
||||||
|
"pg-tsquery": "8.0.3",
|
||||||
"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",
|
||||||
|
@ -13,28 +13,6 @@ module.exports = {
|
|||||||
init() {
|
init() {
|
||||||
// not used
|
// not used
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* SUGGEST
|
|
||||||
*
|
|
||||||
* @param {String} q Query
|
|
||||||
* @param {Object} opts Additional options
|
|
||||||
*/
|
|
||||||
async suggest(q, opts) {
|
|
||||||
const results = await WIKI.models.pages.query()
|
|
||||||
.column('title')
|
|
||||||
.where(builder => {
|
|
||||||
builder.where('isPublished', true)
|
|
||||||
if (opts.locale) {
|
|
||||||
builder.andWhere('locale', opts.locale)
|
|
||||||
}
|
|
||||||
if (opts.path) {
|
|
||||||
builder.andWhere('path', 'like', `${opts.path}%`)
|
|
||||||
}
|
|
||||||
builder.andWhere('title', 'like', `%${q}%`)
|
|
||||||
})
|
|
||||||
.limit(10)
|
|
||||||
return _.uniq(_.filter(_.flatten(results.map(r => r.title.split(' '))), w => w.indexOf(q) >= 0))
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* QUERY
|
* QUERY
|
||||||
*
|
*
|
||||||
|
@ -1,26 +1,31 @@
|
|||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const tsquery = require('pg-tsquery')()
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
activate() {
|
async activate() {
|
||||||
// not used
|
// not used
|
||||||
},
|
},
|
||||||
deactivate() {
|
async deactivate() {
|
||||||
// not used
|
// not used
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* INIT
|
* INIT
|
||||||
*/
|
*/
|
||||||
init() {
|
async init() {
|
||||||
// not used
|
// -> Create Index
|
||||||
},
|
const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
|
||||||
/**
|
if (!indexExists) {
|
||||||
* SUGGEST
|
await WIKI.models.knex.schema.createTable('pagesVector', table => {
|
||||||
*
|
table.increments()
|
||||||
* @param {String} q Query
|
table.string('path')
|
||||||
* @param {Object} opts Additional options
|
table.string('locale')
|
||||||
*/
|
table.string('title')
|
||||||
async suggest(q, opts) {
|
table.string('description')
|
||||||
|
table.specificType('titleTk', 'TSVECTOR')
|
||||||
|
table.specificType('descriptionTk', 'TSVECTOR')
|
||||||
|
table.specificType('contentTk', 'TSVECTOR')
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* QUERY
|
* QUERY
|
||||||
@ -29,6 +34,21 @@ module.exports = {
|
|||||||
* @param {Object} opts Additional options
|
* @param {Object} opts Additional options
|
||||||
*/
|
*/
|
||||||
async query(q, opts) {
|
async query(q, opts) {
|
||||||
|
try {
|
||||||
|
const results = await WIKI.models.knex.raw(`
|
||||||
|
SELECT id, path, locale, title, description
|
||||||
|
FROM "pagesVector", to_tsquery(?) query
|
||||||
|
WHERE (query @@ "titleTk") OR (query @@ "descriptionTk") OR (query @@ "contentTk")
|
||||||
|
`, [tsquery(q)])
|
||||||
|
return {
|
||||||
|
results: results.rows,
|
||||||
|
suggestions: [],
|
||||||
|
totalHits: results.rows.length
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
WIKI.logger.warn('Search Engine Error:')
|
||||||
|
WIKI.logger.warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -37,7 +57,11 @@ module.exports = {
|
|||||||
* @param {Object} page Page to create
|
* @param {Object} page Page to create
|
||||||
*/
|
*/
|
||||||
async created(page) {
|
async created(page) {
|
||||||
// not used
|
await WIKI.models.knex.raw(`
|
||||||
|
INSERT INTO "pagesVector" (path, locale, title, description, "titleTk", "descriptionTk", "contentTk") VALUES (
|
||||||
|
'?', '?', '?', '?', to_tsvector('?'), to_tsvector('?'), to_tsvector('?')
|
||||||
|
)
|
||||||
|
`, [page.path, page.locale, page.title, page.description, page.title, page.description, page.content])
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* UPDATE
|
* UPDATE
|
||||||
@ -45,7 +69,15 @@ module.exports = {
|
|||||||
* @param {Object} page Page to update
|
* @param {Object} page Page to update
|
||||||
*/
|
*/
|
||||||
async updated(page) {
|
async updated(page) {
|
||||||
// not used
|
await WIKI.models.knex.raw(`
|
||||||
|
UPDATE "pagesVector" SET
|
||||||
|
title = '?',
|
||||||
|
description = '?',
|
||||||
|
"titleTk" = to_tsvector('?'),
|
||||||
|
"descriptionTk" = to_tsvector('?'),
|
||||||
|
"contentTk" = to_tsvector('?')
|
||||||
|
WHERE path = '?' AND locale = '?' LIMIT 1
|
||||||
|
`, [page.title, page.description, page.title, page.description, page.content, page.path, page.locale])
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* DELETE
|
* DELETE
|
||||||
@ -53,7 +85,10 @@ module.exports = {
|
|||||||
* @param {Object} page Page to delete
|
* @param {Object} page Page to delete
|
||||||
*/
|
*/
|
||||||
async deleted(page) {
|
async deleted(page) {
|
||||||
// not used
|
await WIKI.models.knex('pagesVector').where({
|
||||||
|
locale: page.locale,
|
||||||
|
path: page.path
|
||||||
|
}).del().limit(1)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* RENAME
|
* RENAME
|
||||||
@ -61,12 +96,23 @@ module.exports = {
|
|||||||
* @param {Object} page Page to rename
|
* @param {Object} page Page to rename
|
||||||
*/
|
*/
|
||||||
async renamed(page) {
|
async renamed(page) {
|
||||||
// not used
|
await WIKI.models.knex('pagesVector').where({
|
||||||
|
locale: page.locale,
|
||||||
|
path: page.sourcePath
|
||||||
|
}).update({
|
||||||
|
locale: page.locale,
|
||||||
|
path: page.destinationPath
|
||||||
|
}).limit(1)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* REBUILD INDEX
|
* REBUILD INDEX
|
||||||
*/
|
*/
|
||||||
async rebuild() {
|
async rebuild() {
|
||||||
// not used
|
await WIKI.models.knex('pagesVector').truncate()
|
||||||
|
await WIKI.models.knex.raw(`
|
||||||
|
INSERT INTO "pagesVector" (path, locale, title, description, "titleTk", "descriptionTk", "contentTk")
|
||||||
|
SELECT path, "localeCode" AS locale, title, description, to_tsvector(title) AS "titleTk", to_tsvector(description) AS "descriptionTk", to_tsvector(content) AS "contentTk"
|
||||||
|
FROM "pages"
|
||||||
|
WHERE pages."isPublished" AND NOT pages."isPrivate"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user