feat: search suggestions + results UI improvements

This commit is contained in:
Nick 2019-03-10 01:28:58 -05:00
parent ab42e5e1ab
commit f7664339f4
4 changed files with 80 additions and 18 deletions

View File

@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [2.0.0-beta.XX] - 2019-XX-XX
### Added
- Added Search Results overlay
- Added Search Engine - PostgreSQL
- Added Search Engine - DB Basic
- Added Git changes processing (add/modify/delete)
- Added Storage last sync date in status panel
- Added Dev Flags

View File

@ -76,6 +76,8 @@
@keyup.esc='searchClose'
@focus='searchFocus'
@blur='searchBlur'
@keyup.down='searchMove(`down`)'
@keyup.up='searchMove(`up`)'
)
v-progress-linear(
indeterminate,
@ -253,7 +255,10 @@ export default {
}
},
searchEnter() {
this.searchIsLoading = true
this.$root.$emit('searchEnter', true)
},
searchMove(dir) {
this.$root.$emit('searchMove', dir)
},
pageNew () {
this.newPageModal = true

View File

@ -16,9 +16,9 @@
.subheading No pages matching your query.
template(v-if='results.length > 0')
v-subheader.white--text Found {{response.totalHits}} results
v-list.radius-7(two-line)
v-list.search-results-items.radius-7(two-line)
template(v-for='(item, idx) of results')
v-list-tile(@click='', :key='item.id')
v-list-tile(@click='goToPage(item)', :key='item.id', :class='idx === cursor ? `highlighted` : ``')
v-list-tile-avatar(tile)
img(src='/svg/icon-selective-highlighting.svg')
v-list-tile-content
@ -36,9 +36,9 @@
)
template(v-if='suggestions.length > 0')
v-subheader.white--text.mt-3 Did you mean...
v-list.radius-7(dense, dark)
v-list.search-results-suggestions.radius-7(dense, dark)
template(v-for='(term, idx) of suggestions')
v-list-tile(:key='term', @click='setSearchTerm(term)')
v-list-tile(:key='term', @click='setSearchTerm(term)', :class='idx + results.length === cursor ? `highlighted` : ``')
v-list-tile-avatar
v-icon search
v-list-tile-content
@ -66,6 +66,7 @@ export default {
},
data() {
return {
cursor: 0,
pagination: 1,
response: {
results: [],
@ -95,16 +96,40 @@ export default {
},
watch: {
search(newValue, oldValue) {
this.cursor = 0
if (newValue.length < 2) {
this.response.results = []
this.response.suggestions = []
} else {
this.searchIsLoading = true
}
}
},
methods: {
setSearchTerm(term) {
this.search = term
},
goToPage(item) {
window.location.assign(`/${item.locale}/${item.path}`)
}
},
mounted() {
this.$root.$on('searchMove', (dir) => {
this.cursor += (dir === 'up' ? -1 : 1)
if (this.cursor < -1) {
this.cursor = -1
} else if (this.cursor > this.results.length + this.suggestions.length - 1) {
this.cursor = this.results.length + this.suggestions.length - 1
}
})
this.$root.$on('searchEnter', () => {
if (this.cursor >= 0 && this.cursor < this.results.length) {
this.goToPage(_.nth(this.results, this.cursor))
} else if (this.cursor >= 0) {
this.setSearchTerm(_.nth(this.suggestions, this.cursor - this.results.length))
}
})
},
apollo: {
response: {
query: searchPagesQuery,
@ -178,6 +203,18 @@ export default {
width: 200px;
}
}
&-items {
.highlighted {
background-color: mc('blue', '50');
}
}
&-suggestions {
.highlighted {
background-color: mc('blue', '500');
}
}
}
@keyframes searchResultsReveal {

View File

@ -12,7 +12,7 @@ module.exports = {
* INIT
*/
async init() {
// -> Create Index
// -> Create Search Index
const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
if (!indexExists) {
await WIKI.models.knex.schema.createTable('pagesVector', table => {
@ -21,11 +21,19 @@ module.exports = {
table.string('locale')
table.string('title')
table.string('description')
table.specificType('titleTk', 'TSVECTOR')
table.specificType('descriptionTk', 'TSVECTOR')
table.specificType('contentTk', 'TSVECTOR')
table.specificType('tokens', 'TSVECTOR')
})
}
// -> Create Words Index
const wordsExists = await WIKI.models.knex.schema.hasTable('pagesWords')
if (!wordsExists) {
await WIKI.models.knex.raw(`
CREATE TABLE "pagesWords" AS SELECT word FROM ts_stat(
'SELECT to_tsvector(''simple'', pages."title") || to_tsvector(''simple'', pages."description") || to_tsvector(''simple'', pages."content") FROM pages WHERE pages."isPublished" AND NOT pages."isPrivate"'
)`)
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)`)
}
},
/**
* QUERY
@ -35,14 +43,20 @@ module.exports = {
*/
async query(q, opts) {
try {
let suggestions = []
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")
WHERE query @@ "tokens"
ORDER BY ts_rank(tokens, query) DESC
`, [tsquery(q)])
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: [],
suggestions,
totalHits: results.rows.length
}
} catch (err) {
@ -58,8 +72,8 @@ module.exports = {
*/
async created(page) {
await WIKI.models.knex.raw(`
INSERT INTO "pagesVector" (path, locale, title, description, "titleTk", "descriptionTk", "contentTk") VALUES (
'?', '?', '?', '?', to_tsvector('?'), to_tsvector('?'), to_tsvector('?')
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.locale, page.title, page.description, page.title, page.description, page.content])
},
@ -73,9 +87,9 @@ module.exports = {
UPDATE "pagesVector" SET
title = '?',
description = '?',
"titleTk" = to_tsvector('?'),
"descriptionTk" = to_tsvector('?'),
"contentTk" = to_tsvector('?')
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])
},
@ -110,8 +124,11 @@ module.exports = {
async rebuild() {
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"
INSERT INTO "pagesVector" (path, locale, title, description, "tokens")
SELECT path, "localeCode" AS locale, title, description,
(setweight(to_tsvector('${this.config.dictLanguage}', title), 'A') ||
setweight(to_tsvector('${this.config.dictLanguage}', description), 'B') ||
setweight(to_tsvector('${this.config.dictLanguage}', content), 'C')) AS tokens
FROM "pages"
WHERE pages."isPublished" AND NOT pages."isPrivate"`)
}