Skip to content

Commit

Permalink
Implement text search (17thshard#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
marvin-roesch authored Nov 16, 2021
1 parent e40d4b9 commit 5c2b218
Show file tree
Hide file tree
Showing 30 changed files with 1,070 additions and 211 deletions.
4 changes: 1 addition & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# build stage
FROM node:14.5-alpine as build-stage
FROM node:14.5-stretch-slim as build-stage

ARG PUBLIC_URL=/

RUN apk --no-cache add autoconf automake libtool make tiff jpeg zlib zlib-dev pkgconf nasm file gcc musl-dev

WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
Expand Down
4 changes: 1 addition & 3 deletions Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# build stage
FROM node:14.5-alpine as build-stage
FROM node:14.5-stretch-slim as build-stage

ARG PUBLIC_URL=/

RUN apk --no-cache add autoconf automake libtool make tiff jpeg zlib zlib-dev pkgconf nasm file gcc musl-dev

WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
Expand Down
2 changes: 0 additions & 2 deletions build/loaders/lang-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ module.exports = function (source) {
const standardParser = parseStandardFile.bind(this)
const eventParser = parseEventFile.bind(this)

// Eval is safe here since we're getting things directly from the JSON "loader"
// eslint-disable-next-line no-eval
const messages = typeof source === 'string' ? JSON.parse(source) : source

messages.events = build(lang, 'events', eventParser)
Expand Down
148 changes: 148 additions & 0 deletions build/loaders/search-index-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
const fs = require('fs')
const lunr = require('lunr')
require('lunr-languages/lunr.stemmer.support')(lunr)

lunr.tokenizer.separator = /[\s-[\](){}]+/

function removeDuplicates (token, index, tokens) {
const [position] = token.metadata.position

for (let i = index - 1; i >= 0 && tokens[i].metadata.position[0] === position; i--) {
if (tokens[i].toString() === token.toString()) {
return null
}
}

return token
}

lunr.Pipeline.registerFunction(removeDuplicates, 'remove-duplicates')

function nGramTokenizer (obj, metadata) {
if (obj === null || obj === undefined) {
return []
}

if (Array.isArray(obj)) {
return obj.map(function (t) {
return new lunr.Token(
lunr.utils.asString(t).toLowerCase(),
lunr.utils.clone(metadata)
)
})
}

const str = obj.toString().toLowerCase()
const len = str.length
const tokens = []

for (let sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {
const char = str.charAt(sliceEnd)
const sliceLength = sliceEnd - sliceStart

if ((char.match(lunr.tokenizer.separator) || sliceEnd === len)) {
if (sliceLength > 0) {
const tokenMetadata = lunr.utils.clone(metadata) || {}
tokenMetadata.position = [sliceStart, sliceLength]
tokenMetadata.index = tokens.length

const baseToken = str.slice(sliceStart, sliceEnd)
if (baseToken.length <= 3) {
tokens.push(new lunr.Token(baseToken, tokenMetadata))
} else {
for (let i = 3; i <= baseToken.length; i++) {
const meta = lunr.utils.clone(tokenMetadata)
meta.position = [sliceStart, i]
meta.index = tokens.length

tokens.push(new lunr.Token(baseToken.slice(0, i), meta))
}
}
}

sliceStart = sliceEnd + 1
}
}

return tokens
}

module.exports = function (source) {
if (this.cacheable) {
this.cacheable()
}

this.async()

const messages = typeof source === 'string' ? JSON.parse(source) : source
const load = async (key) => {
const path = await new Promise((resolve, reject) => this.resolve(
this.rootContext,
`@/store/${key}.json`,
(error, result) => error !== null ? reject(error) : resolve(result)
))
this.addDependency(path)
return JSON.parse(fs.readFileSync(path)).reduce(
(acc, entry) => {
acc[entry.id] = entry
return acc
},
{}
)
}

async function buildIndex () {
const searchable = (await Promise.all(['events', 'locations', 'characters', 'misc'].map(async key => ({
key,
value: await load(key)
})))).reduce(
(acc, entry) => {
acc[entry.key] = entry.value
return acc
},
{}
)
return lunr(function () {
const lunrLanguage = messages['search-language']
this.tokenizer = nGramTokenizer
if (lunrLanguage !== 'en') {
require(`lunr-languages/lunr.${lunrLanguage}`)(lunr)
this.use(lunr[lunrLanguage])
}

this.pipeline.add(removeDuplicates)

this.ref('id')
this.field('name', { boost: 10 })
this.field('details')
this.field('artist')

Object.entries(searchable).forEach(([entryType, entryData]) => {
const entries = messages[entryType] ?? []
Object.keys(entries).forEach((id) => {
const entry = entryData[id]
let artist
if (entry !== undefined && entry.image !== undefined && entry.image.credits !== undefined) {
const markdownResult = /^\[([^\]]+)]\(.*\)$/.exec(entry.image.credits)
artist = markdownResult !== null ? markdownResult[1] : entry.image.credits
}
this.add(
{ ...entries[id], id: `${entryType}/${id}`, artist },
{ boost: entryType !== 'events' ? 2 : 1 }
)
})
})
})
}

buildIndex()
.then(index =>
this.callback(
null,
JSON.stringify(index)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
)
)
.catch(error => this.callback(error))
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"hammerjs": "^2.0.8",
"is-mobile": "^2.2.2",
"jszip": "^3.7.0",
"lunr": "^2.3.9",
"lunr-languages": "^1.8.0",
"register-service-worker": "^1.7.1",
"seedrandom": "^3.0.5",
"simple-markdown": "^0.7.2",
Expand Down Expand Up @@ -53,6 +55,7 @@
"imagemin-webp": "^6.0.0",
"imagemin-zopfli": "^7.0.0",
"js-string-escape": "^1.0.1",
"nodejieba": "^2.5.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11",
Expand Down
110 changes: 105 additions & 5 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,27 @@
<transition name="scrubber" duration="1500" @after-enter="onScrubberLoaded">
<Scrubber v-if="ready" />
</transition>
<Info @open="sidebarActive = true" @open-tutorial="tutorialActive = true" @close="sidebarActive = false" />
<Settings @open="sidebarActive = true" @close="sidebarActive = false" />
<div class="app__actions">
<Search :open="openedMenu === 'search'" @open="openMenu('search')" @close="closeMenu" />
<button
data-tutorial-id="settings-button"
:class="['app__actions-button', 'app__actions-button--wide', {'app__actions-button--hidden': openedMenu === 'settings'}]"
@click="openMenu('settings')"
>
<SlidersIcon size="1x" />
{{ $t('ui.settings') }}
</button>
<button
data-tutorial-id="menu-button"
:class="['app__actions-button', {'app__actions-button--hidden': openedMenu === 'info'}]"
:title="$t('ui.menu')"
@click="openMenu('info')"
>
<MenuIcon size="1x" />
</button>
</div>
<Info :open="openedMenu === 'info'" @open-tutorial="tutorialActive = true" @close="closeMenu" />
<Settings :open="openedMenu === 'settings'" @close="closeMenu" />
<transition name="calendar-guide">
<CalendarGuide v-if="$store.state.calendarGuideOpen" />
</transition>
Expand All @@ -32,6 +51,7 @@
</template>

<script>
import { MenuIcon, SlidersIcon } from 'vue-feather-icons'
import Scrubber from '@/components/Scrubber.vue'
import Settings from '@/components/Settings.vue'
import LoadingIndicator from '@/components/LoadingIndicator.vue'
Expand All @@ -42,10 +62,13 @@ import Tutorial from '@/components/Tutorial.vue'
import FirstVisitWindow from '@/components/FirstVisitWindow.vue'
import ErrorScreen from '@/components/ErrorScreen.vue'
import '@/assets/fonts/hebrew.scss'
import Search from '@/components/search/Search.vue'
import { mapMutations, mapState } from 'vuex'
export default {
name: 'App',
components: {
Search,
ErrorScreen,
FirstVisitWindow,
Tutorial,
Expand All @@ -54,14 +77,15 @@ export default {
Info,
LoadingIndicator,
Settings,
Scrubber
Scrubber,
MenuIcon,
SlidersIcon
},
data () {
return {
ready: false,
errored: false,
mapTransitions: false,
sidebarActive: false,
tutorialActive: window.localStorage.tutorialStarted === 'true' && window.localStorage.tutorialDone !== 'true',
firstVisit: window.localStorage.tutorialStarted !== 'true' && window.localStorage.tutorialDone !== 'true'
}
Expand All @@ -75,6 +99,10 @@ export default {
}
return null
},
...mapState(['openedMenu']),
sidebarActive () {
return this.openedMenu === 'settings' || this.openedMenu === 'info'
}
},
watch: {
Expand Down Expand Up @@ -113,7 +141,8 @@ export default {
},
onScrubberLoaded () {
this.mapTransitions = true
}
},
...mapMutations(['openMenu', 'closeMenu'])
}
}
</script>
Expand Down Expand Up @@ -174,6 +203,77 @@ body {
padding-left: 225px;
}
}
.app__actions {
position: fixed;
top: 2rem;
display: flex;
grid-gap: 1rem;
max-width: calc(100% - 4rem);
z-index: 60;
[dir=ltr] & {
right: 2rem;
}
[dir=rtl] & {
left: 2rem;
}
&-button {
display: flex;
align-items: center;
position: relative;
font-size: 1rem;
line-height: 1;
appearance: none;
outline: none;
box-sizing: border-box;
border: none;
z-index: 61;
background: #F5ECDA;
border-radius: 2rem;
padding: 0.75rem 0.75rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
color: #242629;
pointer-events: auto;
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.5);
&:hover, &:active, &:focus {
background: saturate(darken(#F5ECDA, 10%), 5%);
}
&--wide {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
&--hidden {
cursor: default !important;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
pointer-events: none;
opacity: 0;
transform: scale(0);
}
[dir=ltr] & {
transform-origin: calc(100% - 1rem) 50%;
&--wide .feather {
margin-right: 0.5rem;
}
}
[dir=rtl] & {
transform-origin: 1rem 50%;
&--wide .feather {
margin-left: 0.5rem;
}
}
}
}
}
button {
Expand Down
2 changes: 1 addition & 1 deletion src/components/CalendarGuide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default {
opacity: 0;
}
&-enter-to, &-leave-from {
&-enter-to, &-leave {
opacity: 1;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/GoToDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export default {
}
}
&-enter-to, &-leave-from {
&-enter-to, &-leave {
opacity: 1;
.go-to-date {
Expand Down
Loading

0 comments on commit 5c2b218

Please sign in to comment.