Skip to content

Commit

Permalink
Implement worker and clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
lukebrody committed Feb 1, 2025
1 parent d983686 commit ac11d31
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 38 deletions.
39 changes: 28 additions & 11 deletions react/src/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Navigator } from '../navigation/Navigator'
import { useColors } from '../page_template/colors'
import { useSetting } from '../page_template/settings'
import '../common.css'
import { loadSearchIndex, NormalizedSearchIndex, search } from '../search'
import { SearchParams } from '../search'

export function SearchBox(props: {
onChange?: (inp: string) => void
Expand All @@ -26,7 +26,7 @@ export function SearchBox(props: {

const searchQuery = queryRef.current

const fullIndex = useRef<Promise<NormalizedSearchIndex> | undefined>()
const searchWorker = useRef<SearchWorker | undefined>()

const reset = (): void => {
setQuery('')
Expand Down Expand Up @@ -74,19 +74,18 @@ export function SearchBox(props: {
setFocused(0)
return
}
if (fullIndex.current === undefined) {
fullIndex.current = loadSearchIndex()
if (searchWorker.current === undefined) {
searchWorker.current = createSearchWorker()
}
const full = await fullIndex.current
// we can skip searching if the query has changed since we were waiting on the index
const result = await searchWorker.current({ unnormalizedPattern: searchQuery, maxResults: 10, showHistoricalCDs })
// we should throw away the result if the query has changed since we submitted the search
if (queryRef.current !== searchQuery) {
return
}
const result = search(full, searchQuery, 10, { showHistoricalCDs })
setMatches(result)
setFocused(f => Math.min(f, result.length - 1))
})()
}, [searchQuery, showHistoricalCDs, fullIndex])
}, [searchQuery, showHistoricalCDs, searchWorker])

return (
<form
Expand All @@ -110,12 +109,12 @@ export function SearchBox(props: {
}}
value={query}
onFocus={() => {
if (fullIndex.current === undefined) {
fullIndex.current = loadSearchIndex()
if (searchWorker.current === undefined) {
searchWorker.current = createSearchWorker()
}
}}
onBlur={() => {
fullIndex.current = undefined
searchWorker.current = undefined
}}
/>

Expand Down Expand Up @@ -165,3 +164,21 @@ export function SearchBox(props: {
</form>
)
}

const workerTerminatorRegistry = new FinalizationRegistry<Worker>((worker) => { worker.terminate() })

type SearchWorker = (params: SearchParams) => Promise<string[]>

function createSearchWorker(): SearchWorker {
const worker = new Worker(new URL('../searchWorker', import.meta.url))
const result: SearchWorker = (params) => {
worker.postMessage(params)
return new Promise((resolve) => {
worker.addEventListener('message', (message: MessageEvent<string[]>) => {
resolve(message.data)
}, { once: true })
})
}
workerTerminatorRegistry.register(result, worker)
return result
}
36 changes: 14 additions & 22 deletions react/src/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function normalize(a: string): string {
return a.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f,\(\)\[\]]/g, '').replaceAll('-', ' ')
}

export interface NormalizedSearchIndex {
interface NormalizedSearchIndex {
entries: {
element: string
tokens: Haystack[]
Expand Down Expand Up @@ -57,20 +57,7 @@ function combinedScore(result: SearchResult): number {
}

function compareSearchResults(a: SearchResult, b: SearchResult): number {
// The order of these comparisons relates to various optimizations
// if (combinedScore(a) !== combinedScore(b)) {
return combinedScore(a) - combinedScore(b)
// }
// if (a.matchScore !== b.matchScore) {
// return a.matchScore - b.matchScore
// }
// if (a.positionScore !== b.positionScore) {
// return a.positionScore - b.positionScore
// }
// if (a.priority !== b.priority) {
// return a.priority - b.priority
// }
// return a.normalizedPopulationRank - b.normalizedPopulationRank
}

function tokenize(pattern: string): string[] {
Expand All @@ -83,8 +70,13 @@ function tokenize(pattern: string): string[] {
return []
}

// Expects `pattern` to be normalized
export function search(searchIndex: NormalizedSearchIndex, unnormalizedPattern: string, maxResults: number, options: { showHistoricalCDs: boolean }): string[] {
export interface SearchParams {
unnormalizedPattern: string
maxResults: number
showHistoricalCDs: boolean
}

function search(searchIndex: NormalizedSearchIndex, { unnormalizedPattern, maxResults, showHistoricalCDs }: SearchParams): string[] {
const start = performance.now()

const pattern = normalize(unnormalizedPattern)
Expand Down Expand Up @@ -112,7 +104,7 @@ export function search(searchIndex: NormalizedSearchIndex, unnormalizedPattern:
let entriesPatternChecks = 0

entries: for (const [populationRank, { tokens, element, priority, signature }] of searchIndex.entries.entries()) {
if (!options.showHistoricalCDs && isHistoricalCD(element)) {
if (!showHistoricalCDs && isHistoricalCD(element)) {
continue
}

Expand Down Expand Up @@ -229,12 +221,12 @@ export function search(searchIndex: NormalizedSearchIndex, unnormalizedPattern:
return results.map(result => result.element)
}

export async function loadSearchIndex(): Promise<NormalizedSearchIndex> {
export async function createIndex(): Promise<(params: SearchParams) => string[]> {
const start = performance.now()
const searchIndex = await loadProtobuf('/index/pages_all.gz', 'SearchIndex')
const result = processRawSearchIndex(searchIndex)
console.log(`Took ${performance.now() - start}ms to load search index`)
return result
const rawIndex = await loadProtobuf('/index/pages_all.gz', 'SearchIndex')
const index = processRawSearchIndex(rawIndex)
debug(`Took ${performance.now() - start}ms to load search index`)
return params => search(index, params)
}

function processRawSearchIndex(searchIndex: { elements: string[], priorities: number[] }): NormalizedSearchIndex {
Expand Down
8 changes: 6 additions & 2 deletions react/src/searchWorker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { loadSearchIndex } from './search'
import { createIndex, SearchParams } from './search'

postMessage(await loadSearchIndex())
const search = await createIndex()

onmessage = (message: MessageEvent<SearchParams>) => {
postMessage(search(message.data))
}
6 changes: 3 additions & 3 deletions react/unit/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { test } from 'uvu'
import * as assert from 'uvu/assert'

import './util/fetch'
import { loadSearchIndex, search } from '../src/search'
import { createIndex } from '../src/search'

const searchIndex = await loadSearchIndex()
const search = await createIndex()

// We curry based on testFn so we can use test.only, test.skip, etc
const firstResult = (testFn: (name: string, testBlock: () => void) => void) => (query: string, result: string): void => {
testFn(`First result for '${query}' is '${result}'`, () => {
assert.is(search(searchIndex, query, 10, { showHistoricalCDs: false })[0], result)
assert.is(search({ unnormalizedPattern: query, maxResults: 10, showHistoricalCDs: false })[0], result)
})
}

Expand Down

0 comments on commit ac11d31

Please sign in to comment.