feat: search suggestions + results UI improvements
This commit is contained in:
parent
ab42e5e1ab
commit
f7664339f4
@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## [2.0.0-beta.XX] - 2019-XX-XX
|
## [2.0.0-beta.XX] - 2019-XX-XX
|
||||||
### Added
|
### Added
|
||||||
|
- Added Search Results overlay
|
||||||
|
- Added Search Engine - PostgreSQL
|
||||||
|
- Added Search Engine - DB Basic
|
||||||
- Added Git changes processing (add/modify/delete)
|
- Added Git changes processing (add/modify/delete)
|
||||||
- Added Storage last sync date in status panel
|
- Added Storage last sync date in status panel
|
||||||
- Added Dev Flags
|
- Added Dev Flags
|
||||||
|
@ -76,6 +76,8 @@
|
|||||||
@keyup.esc='searchClose'
|
@keyup.esc='searchClose'
|
||||||
@focus='searchFocus'
|
@focus='searchFocus'
|
||||||
@blur='searchBlur'
|
@blur='searchBlur'
|
||||||
|
@keyup.down='searchMove(`down`)'
|
||||||
|
@keyup.up='searchMove(`up`)'
|
||||||
)
|
)
|
||||||
v-progress-linear(
|
v-progress-linear(
|
||||||
indeterminate,
|
indeterminate,
|
||||||
@ -253,7 +255,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
searchEnter() {
|
searchEnter() {
|
||||||
this.searchIsLoading = true
|
this.$root.$emit('searchEnter', true)
|
||||||
|
},
|
||||||
|
searchMove(dir) {
|
||||||
|
this.$root.$emit('searchMove', dir)
|
||||||
},
|
},
|
||||||
pageNew () {
|
pageNew () {
|
||||||
this.newPageModal = true
|
this.newPageModal = true
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
.subheading No pages matching your query.
|
.subheading No pages matching your query.
|
||||||
template(v-if='results.length > 0')
|
template(v-if='results.length > 0')
|
||||||
v-subheader.white--text Found {{response.totalHits}} results
|
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')
|
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)
|
v-list-tile-avatar(tile)
|
||||||
img(src='/svg/icon-selective-highlighting.svg')
|
img(src='/svg/icon-selective-highlighting.svg')
|
||||||
v-list-tile-content
|
v-list-tile-content
|
||||||
@ -36,9 +36,9 @@
|
|||||||
)
|
)
|
||||||
template(v-if='suggestions.length > 0')
|
template(v-if='suggestions.length > 0')
|
||||||
v-subheader.white--text.mt-3 Did you mean...
|
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')
|
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-list-tile-avatar
|
||||||
v-icon search
|
v-icon search
|
||||||
v-list-tile-content
|
v-list-tile-content
|
||||||
@ -66,6 +66,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
cursor: 0,
|
||||||
pagination: 1,
|
pagination: 1,
|
||||||
response: {
|
response: {
|
||||||
results: [],
|
results: [],
|
||||||
@ -95,16 +96,40 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search(newValue, oldValue) {
|
search(newValue, oldValue) {
|
||||||
|
this.cursor = 0
|
||||||
if (newValue.length < 2) {
|
if (newValue.length < 2) {
|
||||||
this.response.results = []
|
this.response.results = []
|
||||||
|
this.response.suggestions = []
|
||||||
|
} else {
|
||||||
|
this.searchIsLoading = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setSearchTerm(term) {
|
setSearchTerm(term) {
|
||||||
this.search = 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: {
|
apollo: {
|
||||||
response: {
|
response: {
|
||||||
query: searchPagesQuery,
|
query: searchPagesQuery,
|
||||||
@ -178,6 +203,18 @@ export default {
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-items {
|
||||||
|
.highlighted {
|
||||||
|
background-color: mc('blue', '50');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-suggestions {
|
||||||
|
.highlighted {
|
||||||
|
background-color: mc('blue', '500');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes searchResultsReveal {
|
@keyframes searchResultsReveal {
|
||||||
|
@ -12,7 +12,7 @@ module.exports = {
|
|||||||
* INIT
|
* INIT
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
// -> Create Index
|
// -> Create Search Index
|
||||||
const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
|
const indexExists = await WIKI.models.knex.schema.hasTable('pagesVector')
|
||||||
if (!indexExists) {
|
if (!indexExists) {
|
||||||
await WIKI.models.knex.schema.createTable('pagesVector', table => {
|
await WIKI.models.knex.schema.createTable('pagesVector', table => {
|
||||||
@ -21,11 +21,19 @@ module.exports = {
|
|||||||
table.string('locale')
|
table.string('locale')
|
||||||
table.string('title')
|
table.string('title')
|
||||||
table.string('description')
|
table.string('description')
|
||||||
table.specificType('titleTk', 'TSVECTOR')
|
table.specificType('tokens', 'TSVECTOR')
|
||||||
table.specificType('descriptionTk', 'TSVECTOR')
|
|
||||||
table.specificType('contentTk', '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
|
* QUERY
|
||||||
@ -35,14 +43,20 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
async query(q, opts) {
|
async query(q, opts) {
|
||||||
try {
|
try {
|
||||||
|
let suggestions = []
|
||||||
const results = await WIKI.models.knex.raw(`
|
const results = await WIKI.models.knex.raw(`
|
||||||
SELECT id, path, locale, title, description
|
SELECT id, path, locale, title, description
|
||||||
FROM "pagesVector", to_tsquery(?) query
|
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)])
|
`, [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 {
|
return {
|
||||||
results: results.rows,
|
results: results.rows,
|
||||||
suggestions: [],
|
suggestions,
|
||||||
totalHits: results.rows.length
|
totalHits: results.rows.length
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -58,8 +72,8 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
async created(page) {
|
async created(page) {
|
||||||
await WIKI.models.knex.raw(`
|
await WIKI.models.knex.raw(`
|
||||||
INSERT INTO "pagesVector" (path, locale, title, description, "titleTk", "descriptionTk", "contentTk") VALUES (
|
INSERT INTO "pagesVector" (path, locale, title, description, tokens) VALUES (
|
||||||
'?', '?', '?', '?', to_tsvector('?'), to_tsvector('?'), to_tsvector('?')
|
'?', '?', '?', '?', (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])
|
`, [page.path, page.locale, page.title, page.description, page.title, page.description, page.content])
|
||||||
},
|
},
|
||||||
@ -73,9 +87,9 @@ module.exports = {
|
|||||||
UPDATE "pagesVector" SET
|
UPDATE "pagesVector" SET
|
||||||
title = '?',
|
title = '?',
|
||||||
description = '?',
|
description = '?',
|
||||||
"titleTk" = to_tsvector('?'),
|
tokens = (setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'A') ||
|
||||||
"descriptionTk" = to_tsvector('?'),
|
setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'B') ||
|
||||||
"contentTk" = to_tsvector('?')
|
setweight(to_tsvector('${this.config.dictLanguage}', '?'), 'C'))
|
||||||
WHERE path = '?' AND locale = '?' LIMIT 1
|
WHERE path = '?' AND locale = '?' LIMIT 1
|
||||||
`, [page.title, page.description, page.title, page.description, page.content, page.path, page.locale])
|
`, [page.title, page.description, page.title, page.description, page.content, page.path, page.locale])
|
||||||
},
|
},
|
||||||
@ -110,8 +124,11 @@ module.exports = {
|
|||||||
async rebuild() {
|
async rebuild() {
|
||||||
await WIKI.models.knex('pagesVector').truncate()
|
await WIKI.models.knex('pagesVector').truncate()
|
||||||
await WIKI.models.knex.raw(`
|
await WIKI.models.knex.raw(`
|
||||||
INSERT INTO "pagesVector" (path, locale, title, description, "titleTk", "descriptionTk", "contentTk")
|
INSERT INTO "pagesVector" (path, locale, title, description, "tokens")
|
||||||
SELECT path, "localeCode" AS locale, title, description, to_tsvector(title) AS "titleTk", to_tsvector(description) AS "descriptionTk", to_tsvector(content) AS "contentTk"
|
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"
|
FROM "pages"
|
||||||
WHERE pages."isPublished" AND NOT pages."isPrivate"`)
|
WHERE pages."isPublished" AND NOT pages."isPrivate"`)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user