aliaset
Version:
twind monorepo
143 lines (125 loc) • 4.01 kB
text/typescript
import flexsearch, { type CreateOptions } from 'flexsearch'
export type IndexOptions = { data?: (string | null)[][] } & CreateOptions
export function createIndex({ data = [], ...options }: IndexOptions = {}) {
const index = new flexsearch.Document({
preset: 'memory',
// optimize: true,
// resolution: 9,
charset: 'latin:simple',
language: 'en',
// encode: 'extra',
fastupdate: false,
...options,
document: {
id: 'id',
// tag: 'category',
index: [
{
field: 'title',
tokenize: 'forward',
},
{
field: 'content',
tokenize: 'strict',
minlength: 2,
},
],
},
})
for (const [key, value] of data) {
index.import(key, value)
}
return index
}
export function createSearch({
dev,
store,
currentVersion = typeof __SVELTEKIT_APP_VERSION__ === 'string'
? __SVELTEKIT_APP_VERSION__
: undefined,
...options
}: {
dev?: boolean
currentVersion?: string
store: {
href: string
section: string
label: string
category: string
title: string
content: string
}[]
} & IndexOptions) {
const index = createIndex({ cache: 50, ...options })
// const categories = ['guides', 'packages', 'api']
return (request: Request) => {
const term = new URL(request.url).searchParams.get('q') || ''
const expectedVersion = request.headers.get('x-app-version')
const matchingVersion = expectedVersion && currentVersion && expectedVersion === currentVersion
const needsUpdate = expectedVersion && currentVersion && expectedVersion !== currentVersion
const results = term
? index
.search(term, 35, { suggest: true })
.flatMap((x) => x.result)
.slice(0, 35)
.map((id) => {
const value = store[id]
return {
href: value.href,
section: value.section,
label: value.label === value.title ? undefined : value.label,
category: value.category,
title: excerpt(value.title, term),
excerpt: excerpt(value.content, term),
}
})
: // TODO: sort by section?
// title:guides
// content:guides
// title:packages
// content:packages
// title:api
// content:api
// .sort((a, b) => {
// const byCategory = categories.indexOf(a.category) - categories.indexOf(b.category)
// if (byCategory) return byCategory
// return 0
// })
[]
return new Response(JSON.stringify({ term, results, needsUpdate: needsUpdate || undefined }), {
headers: {
'content-type': 'application/json',
vary: 'x-app-version',
'cache-control': dev
? 'private, no-store'
: matchingVersion
? // if current app version matches the client app version the response is immutable
'public, max-age=604800, immutable'
: // if different versions clients must re-validate
'public, max-age=0, must-revalidate',
},
})
}
}
function escape(text: string) {
return text.replace(/</g, '<').replace(/>/g, '>')
}
function excerpt(content: string, query: string) {
if (!query) {
return escape(content)
}
const index = content.toLowerCase().indexOf(query.toLowerCase())
if (index === -1) {
return escape(content.slice(0, 100))
}
const startIndex = index <= 20 ? 0 : content.lastIndexOf(' ', index - 15)
const prefix = startIndex <= 5 ? content.slice(0, index) : `…${content.slice(startIndex, index)}`
const lastIndex = index + query.length + (80 - (prefix.length + query.length))
const endIndex = content.indexOf(' ', lastIndex)
const suffix = content.slice(index + query.length, endIndex > 0 ? endIndex : lastIndex)
return (
escape(prefix) +
`<mark>${escape(content.slice(index, index + query.length))}</mark>` +
escape(suffix)
)
}