Skip to content

Commit

Permalink
unify search class
Browse files Browse the repository at this point in the history
  • Loading branch information
mruwnik committed Aug 19, 2023
1 parent 0907332 commit 227ba95
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 85 deletions.
21 changes: 10 additions & 11 deletions app/components/search.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {useState, useEffect, useRef, MutableRefObject, FocusEvent} from 'react'
import debounce from 'lodash/debounce'
import {
setupSearch,
searchLive,
searchUnpublished,
Searcher,
Question as QuestionType,
SearchResult,
} from 'stampy-search'
Expand Down Expand Up @@ -34,21 +32,22 @@ export default function Search({

const [arePendingSearches, setPendingSearches] = useState(false)
const [results, setResults] = useState([] as SearchResult[])
const [searcher, setSearcher] = useState()

useEffect(() => {
setupSearch({
getAllQuestions: () => onSiteAnswersRef.current,
})
}, [onSiteAnswersRef])
useEffect(() => {
setSearcher(new Searcher({
getAllQuestions: () => onSiteAnswersRef.current,
}))
}, [onSiteAnswersRef])

const searchFn = (rawValue: string) => {
const searchFn = (rawValue: string) => {
const value = rawValue.trim()
if (value === searchInputRef.current) return

searchInputRef.current = value

setPendingSearches(true)
searchLive(value).then((res) => {
searcher.searchLive(value).then((res) => {
if (res) {
setPendingSearches(false)
setResults(res as SearchResult[])
Expand All @@ -59,7 +58,7 @@ export default function Search({

useEffect(() => {
initialQuery && searchFn(initialQuery)
}, [initialQuery])
}, [initialQuery, searchFn])

const handleChange = debounce(searchFn, 100)

Expand Down
29 changes: 29 additions & 0 deletions stampy-search/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Configuration is done by calling `stampySeach.setupSearch(<confing>)`, where `<c
following optional keys:

- getAllQuestions - a function that will return a list of questions to be used in the baseline search. The default is an empty array
- onResolveCallback - an optional function that will be called with the query phrase and results once it has completed. This can be used as opposed to `await searcher.searchLive('bla')`
- numResults - the number of results to be returned by default. Initially set to 5
- server - where to look for stuff. The default is '', i.e. the current server
- searchEndpoint - the endpoint to use for searching for in progress questions
Expand All @@ -32,3 +33,31 @@ following optional keys:
### CORS

For the live questions query to work properly, it has to load a web worker. For the unpublished search to work, it needs to query an API endpoint. Both of these require CORS to be setup properly on the server that is to be queried. In the case of the basic Stampy sites, this can be configured via the `ALLOW_ORIGINS` environment setting - set it to the origin of the server that will be fetching stuff (so e.g. 'http://127.0.0.1:3123' for the example server) and everything should Just Work. Maybe. Hopefully...

## Usage

import {Searcher, SearchType} from 'stampy-search'

const searcher = new Searcher({
getAllQuestions: () => <return an array of baseline questions>,
onResolveCallback: (query: string, res: SearchResult[] | null) => console.log(query, 'resolved to', res),
numResults: 12,
server: 'http://127.0.0.1:3123',
})

// search for live questions
console.log('got', await searcher.searchLive('bla bla bla'))

// search for live questions with a callback
searcher.searchConfig.onResolveCallback = (query, res) => {
// process the results
}
searcher.liveSearch('bla bla bla')

// search for unpublished questions
console.log(await searcher.searchUnpublished('bla bla bla'))

// Generic search function

console.log(searcher.search(SearchType.LiveOnSite, 'bla bla bla'))
console.log(searcher.search(SearchType.Unpublished, 'bla bla bla'))
11 changes: 7 additions & 4 deletions stampy-search/example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
<script src="stampySearch.min.js"></script>
<script>
console.log('Starting search')
stampySearch.setupSearch({
const searcher = new stampySearch.Searcher({
server: 'http://localhost:8787',
onResolveCallback: (query, res) => console.log('callback', query, res),
})
console.log(searcher)

const searchBar = document.getElementById('searchBar');
document.getElementById('live').addEventListener("click", function(event) {
const val = searchBar.value;
console.log('Searching for:', val);
stampySearch.searchLive(val).catch((err) => alert(err)).then((res) => {
searcher.searchLive(val).catch((err) => alert(err)).then((res) => {
console.log('got result', res)
if (!res) {
console.log('canceled search for "' + val + '"');
} else {
Expand All @@ -36,9 +39,9 @@
document.getElementById('inProgress').addEventListener("click", function(event) {
const val = searchBar.value;
console.log('Searching for:', val);
stampySearch.searchUnpublished(val).catch((err) => alert(err)).then((res) => {
searcher.searchUnpublished(val).then((res) => {
updateResults(res)
})
}).catch((err) => alert(err))
});

const searchResults = document.getElementById('results');
Expand Down
4 changes: 2 additions & 2 deletions stampy-search/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type {Question, SearchResult, WorkerMessage} from './search'
export {searchLive, searchUnpublished, setupSearch} from './search'
export type {Question, SearchResult, WorkerMessage, SearchType} from './search'
export {Searcher} from './search'
161 changes: 93 additions & 68 deletions stampy-search/src/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Search = {
type SearchConfig = {
numResults?: number
getAllQuestions?: () => Question[]
onResolveCallback: (query: string, res: SearchResult[] | null) => void | null
server?: string
searchEndpoint?: string
workerPath?: string
Expand All @@ -30,6 +31,10 @@ export type WorkerMessage =
searchResults: SearchResult[]
query?: string
}
export enum SearchType {
LiveOnSite,
Unpublished
}

/**
* Sort function for the highest score on top
Expand Down Expand Up @@ -113,98 +118,118 @@ export const normalize = (question: string) =>
.replace(/\s+|_|&\s*/g, ' ')
.trim()

let currentSearch: Search = null
let worker: Worker
// let currentSearch: Search = null
// let worker: Worker
const defaultSearchConfig = {
getAllQuestions: () => [] as Question[],
onResolveCallback: null,
numResults: 5,
searchEndpoint: '/questions/search',
workerPath: '/tfWorker.js',
server: '',
}
let searchConfig = defaultSearchConfig

const resolveSearch = ({searchResults, query}: WorkerResultMessage) => {
if (currentSearch) {
currentSearch.resolve(query === currentSearch.query ? searchResults : null)
currentSearch = null
}
}
export class Searcher {
currentSearch: Search
worker: Worker
searchConfig = defaultSearchConfig

const initialiseWorker = async () => {
if (worker !== undefined) return
constructor(config: SearchConfig) {
this.searchConfig = {
...defaultSearchConfig,
...config,
}
this.initialiseWorker().then(console.log)
}

const workerInstance = await fetch(`${searchConfig.server}${searchConfig.workerPath}`)
.then((response) => response.text())
.then((code) => {
const blob = new Blob([code])
return new Worker(URL.createObjectURL(blob))
initialiseWorker = async () => {
if (this.worker !== undefined) return

const workerInstance = await fetch(`${this.searchConfig.server}${this.searchConfig.workerPath}`)
.then((response) => response.text())
.then((code) => {
const blob = new Blob([code])
return new Worker(URL.createObjectURL(blob))
})

workerInstance.addEventListener('message', ({data}) => {
if (data.status == 'ready') {
this.worker = workerInstance
} else if (data.searchResults) {
this.resolveSearch(data)
}
})
}

workerInstance.addEventListener('message', ({data}) => {
if (data.status == 'ready') {
worker = workerInstance
} else if (data.searchResults) {
resolveSearch(data)
resolveSearch = ({searchResults, query}: WorkerResultMessage) => {
if (this.currentSearch) {
this.currentSearch.resolve(query === this.currentSearch.query ? searchResults : null)
this.currentSearch = null
if (this.searchConfig.onResolveCallback) {
this.searchConfig.onResolveCallback(query, searchResults)
}
}
})
}

export const searchLive = (query: string, resultsNum?: number): Promise<SearchResult[] | null> => {
// Cancel any previous searches
resolveSearch({searchResults: []})

const runSearch = () => {
const numResults = resultsNum || searchConfig.numResults
const wordCount = query.split(' ').length
}

if (wordCount > 2 && worker) {
worker.postMessage({query, numResults})
} else {
baselineSearch(query, searchConfig.getAllQuestions(), numResults).then((res) =>
resolveSearch({searchResults: res, query})
)
rejectSearch = (query: string, reason: string) => {
if (this.currentSearch && this.currentSearch.query == query) {
this.currentSearch.reject(reason)
this.currentSearch = null
}
}

const waitTillSearchReady = () => {
if (query != currentSearch?.query) {
runLiveSearch = (query: string, resultsNum?: number) => {
if (query != this.currentSearch?.query) {
return // this search has been superceeded with a newer one, so just give up
} else if (worker || searchConfig.getAllQuestions().length > 0) {
runSearch()
} else if (this.worker || this.searchConfig.getAllQuestions().length > 0) {
const numResults = resultsNum || this.searchConfig.numResults
const wordCount = query.split(' ').length

if (wordCount > 2 && this.worker) {
this.worker.postMessage({query, numResults})
} else {
baselineSearch(query, this.searchConfig.getAllQuestions(), numResults).then((res) =>
this.resolveSearch({searchResults: res, query})
)
}
} else {
setTimeout(waitTillSearchReady, 100)
setTimeout(() => this.runLiveSearch(query, resultsNum), 100)
}
}

return new Promise((resolve, reject) => {
currentSearch = {resolve, reject, query}
waitTillSearchReady()
})
}
runUnpublishedSearch = (query: string, resultsNum?: number) => {
const numResults = resultsNum || this.searchConfig.numResults
const url = `${this.searchConfig.server}${this.searchConfig.searchEndpoint}`
const params = `?question=${encodeURIComponent(query)}&numResults=${numResults}`

export const searchUnpublished = async (
question: string,
resultsNum?: number
): Promise<SearchResult[]> => {
const numResults = resultsNum || searchConfig.numResults
console.log(await fetch(searchConfig.workerPath))
const result = await fetch(
`${searchConfig.server}${searchConfig.searchEndpoint}?question=${encodeURIComponent(
question
)}&numResults=${numResults}`
)

if (result.status == 200) {
return await result.json()
return fetch(url + params)
.then(async (result) => {
if (result.status == 200) {
this.resolveSearch({searchResults: await result.json(), query})
} else {
this.rejectSearch(query, await result.text())
}
})
.catch((err) => this.rejectSearch(query, err))
}
throw new Error(await result.text())
}

export const setupSearch = async (config: SearchConfig) => {
searchConfig = {
...defaultSearchConfig,
...config,
searchLive = (query: string, resultsNum?: number): Promise<SearchResult[] | null> => {
return this.search(SearchType.LiveOnSite, query, resultsNum)
}

searchUnpublished = (query: string, resultsNum?: number): Promise<SearchResult[]> => {
return this.search(SearchType.Unpublished, query, resultsNum)
}

search = (type_: SearchType, query: string, numResults?: number): Promise<SearchResult[]> => {
// Cancel any previous searches
this.resolveSearch({searchResults: []})

const runSearch = type_ == SearchType.LiveOnSite ? this.runLiveSearch : this.runUnpublishedSearch

return new Promise((resolve, reject) => {
this.currentSearch = {resolve, reject, query}
runSearch(query, numResults)
})
}
await initialiseWorker()
}

0 comments on commit 227ba95

Please sign in to comment.