Skip to content

Commit

Permalink
Replace flexsearch with fuse
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoattal committed Dec 21, 2024
1 parent 2ee7608 commit c80bef0
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 112 deletions.
5 changes: 4 additions & 1 deletion examples/nuxt3/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
],

runtimeConfig: {
public: {
configFromNuxt: 'test',
},
},
})

compatibilityDate: '2024-12-20',
})

Check failure on line 14 in examples/nuxt3/nuxt.config.ts

View workflow job for this annotation

GitHub Actions / Build and test

Newline required at end of file but not found
4 changes: 2 additions & 2 deletions examples/vue3/cypress/e2e/search.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('Search', () => {
cy.visit('/')
cy.get('[data-test-id="search-btn"]').click()
cy.get('[data-test-id="search-modal"] input').type('Demo')
cy.get('[data-test-id="search-item"]').should('have.length', 5)
cy.get('[data-test-id="search-item"]').should('have.length.greaterThan', 3)
cy.get('[data-test-id="search-item"]').contains('untitled').click()
cy.get('[data-test-id="story-variant-single-view"]').contains('untitled')
cy.get('[data-test-id="search-btn"]').click()
Expand Down Expand Up @@ -47,7 +47,7 @@ describe('Search', () => {
cy.visit('/')
cy.get('[data-test-id="search-btn"]').click()
cy.get('[data-test-id="search-modal"] input').type('welcome')
cy.get('[data-test-id="search-item"]').should('have.length', 2)
cy.get('[data-test-id="search-item"]').should('have.length.greaterThan', 3)
cy.get('[data-test-id="search-item"]').contains('Introduction')
})
})
2 changes: 1 addition & 1 deletion packages/histoire-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@histoire/controls": "workspace:^",
"@histoire/shared": "workspace:^",
"@histoire/vendors": "workspace:^",
"flexsearch": "0.7.43",
"fuse.js": "^7.0.0",
"shiki-es": "^0.14.0"
},
"devDependencies": {
Expand Down
94 changes: 41 additions & 53 deletions packages/histoire-app/src/app/components/search/SearchPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import type { SearchResult, SearchResultType, Story, Variant } from '../../types
import type { SearchData } from './types'
import { Icon } from '@iconify/vue'
import { useDebounce, useFocus } from '@vueuse/core'
import FlexSearch from 'flexsearch'
import language from 'flexsearch/dist/module/lang/en.js'
import charset from 'flexsearch/dist/module/lang/latin/advanced.js'
import Fuse from 'fuse.js'
import { registeredCommands } from 'virtual:$histoire-commands'
import { computed, ref, watch } from 'vue'
import { useCommandStore } from '../../stores/command.js'
Expand Down Expand Up @@ -57,30 +55,20 @@ const rateLimitedSearch = useDebounce(searchInputText, 50)
const storyStore = useStoryStore()
let titleSearchIndex: FlexSearch.Document<any, any>
let titleSearchIndex: Fuse<{ id: number, text: string }>
let titleIdMap: SearchData['idMap']
function createIndex() {
return new FlexSearch.Document({
preset: 'match',
document: {
id: 'id',
index: [
'text',
],
},
worker: true,
charset,
language,
tokenize: 'forward',
return new Fuse<{ id: number, text: string }>([], {
keys: ['text'],
})
}
async function loadSearchIndex(data: SearchData) {
titleSearchIndex = createIndex()
for (const key of Object.keys(data.index)) {
await titleSearchIndex.import(key, data.index[key])
for (const document of data.index) {
titleSearchIndex.add(document)
}
titleIdMap = data.idMap
Expand All @@ -92,27 +80,27 @@ onUpdate((searchData) => {
loadSearchIndex(searchData)
})
let docSearchIndex: FlexSearch.Document<any, any>
let docSearchIndex: Fuse<{ id: number, text: string }>
let docIdMap: SearchData['idMap']
async function loadDocSearchIndex() {
async function load(data: SearchData) {
docSearchIndex = createIndex()
for (const key of Object.keys(data.index)) {
await docSearchIndex.import(key, data.index[key])
for (const document of data.index) {
docSearchIndex.add(document)
}
docIdMap = data.idMap
if (rateLimitedSearch.value) {
searchOnDocField(rateLimitedSearch.value)
await searchOnDocField(rateLimitedSearch.value)
}
}
const searchDataModule = await DocSearchData()
load(searchDataModule.searchData)
await load(searchDataModule.searchData)
// Handle HMR
searchDataModule.onUpdate((searchData) => {
load(searchData)
Expand All @@ -127,29 +115,29 @@ const titleResults = ref<SearchResult[]>([])
watch(rateLimitedSearch, async (value) => {
const list: SearchResult[] = []
const raw = await titleSearchIndex.search(value)
const result = titleSearchIndex.search(value)
let rank = 0
for (const field of raw) {
for (const id of field.result) {
const idMapData = titleIdMap[id]
if (!idMapData) continue
switch (idMapData.kind) {
case 'story': {
list.push(storyResultFactory(storyStore.getStoryById(idMapData.id), rank))
rank++
break
}
case 'variant': {
const [storyId] = idMapData.id.split(':')
const story = storyStore.getStoryById(storyId)
const variant = storyStore.getVariantById(idMapData.id)
list.push(variantResultFactory(story, variant, rank))
rank++
break
}
for (const document of result) {
const idMapData = titleIdMap[document.item.id]
if (!idMapData) continue
switch (idMapData.kind) {
case 'story': {
list.push(storyResultFactory(storyStore.getStoryById(idMapData.id), rank))
rank++
break
}
case 'variant': {
const [storyId] = idMapData.id.split(':')
const story = storyStore.getStoryById(storyId)
const variant = storyStore.getVariantById(idMapData.id)
list.push(variantResultFactory(story, variant, rank))
rank++
break
}
}
}
titleResults.value = list
})
Expand All @@ -158,21 +146,21 @@ const docsResults = ref<SearchResult[]>([])
async function searchOnDocField(query: string) {
if (docSearchIndex) {
const list: SearchResult[] = []
const raw = await docSearchIndex.search(query)
const result = docSearchIndex.search(query)
let rank = 0
for (const field of raw) {
for (const id of field.result) {
const idMapData = docIdMap[id]
if (!idMapData) continue
switch (idMapData.kind) {
case 'story': {
list.push(storyResultFactory(storyStore.getStoryById(idMapData.id), rank, 'docs'))
rank++
break
}
for (const document of result) {
const idMapData = docIdMap[document.item.id]
if (!idMapData) continue
switch (idMapData.kind) {
case 'story': {
list.push(storyResultFactory(storyStore.getStoryById(idMapData.id), rank, 'docs'))
rank++
break
}
}
}
docsResults.value = list
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/histoire-app/src/app/components/search/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface SearchData {
index: Record<string, any>
index: Array<{ id: number, text: string }>
idMap: Record<number, { id: string, kind: string }>
}
1 change: 0 additions & 1 deletion packages/histoire/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"connect": "^3.7.0",
"defu": "^6.1.4",
"diacritics": "^1.3.0",
"flexsearch": "0.7.43",
"fs-extra": "^11.2.0",
"globby": "^14.0.2",
"gray-matter": "^4.0.3",
Expand Down
60 changes: 7 additions & 53 deletions packages/histoire/src/node/search.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import type { Context } from './context.js'
import { createRequire } from 'node:module'
import { noCase } from 'change-case'
import FlexSearch from 'flexsearch'
import path from 'pathe'
import { loadModule } from './load.js'

const require = createRequire(import.meta.url)

export async function generateTitleSearchData(ctx: Context) {
const searchIndex = await createIndex()
const searchIndex = []
const { idMap, addToIdMap } = createIdMap()

for (const storyFile of ctx.storyFiles) {
if (storyFile.story) {
searchIndex.add({
searchIndex.push({
id: addToIdMap(storyFile.story.id, 'story'),
text: convertTitleToSentence(storyFile.story.title),
})
for (const variant of storyFile.story.variants) {
searchIndex.add({
searchIndex.push({
id: addToIdMap(`${storyFile.story.id}:${variant.id}`, 'variant'),
text: convertTitleToSentence(`${storyFile.story.title} ${variant.title}`),
})
Expand All @@ -27,46 +21,30 @@ export async function generateTitleSearchData(ctx: Context) {
}

return {
index: await exportSearchIndex(searchIndex),
index: searchIndex,
idMap,
}
}

export async function generateDocSearchData(ctx: Context) {
const searchIndex = await createIndex()
const searchIndex = []
const { idMap, addToIdMap } = createIdMap()

for (const storyFile of ctx.storyFiles) {
if (storyFile.story && storyFile.story.docsText) {
searchIndex.add({
searchIndex.push({
id: addToIdMap(storyFile.story.id, 'story'),
text: storyFile.story.docsText,
})
}
}

return {
index: await exportSearchIndex(searchIndex),
index: searchIndex,
idMap,
}
}

async function createIndex() {
const flexsearchRoot = path.dirname(require.resolve('flexsearch/package.json'))
return new FlexSearch.Document({
preset: 'match',
document: {
id: 'id',
index: [
'text',
],
},
charset: await loadModule(`${flexsearchRoot}/dist/module/lang/latin/advanced.js`),
language: await loadModule(`${flexsearchRoot}/dist/module/lang/en.js`),
tokenize: 'forward',
})
}

function createIdMap() {
let uid = 0
const idMap: Record<number, { id: string, kind: string }> = {}
Expand All @@ -83,30 +61,6 @@ function createIdMap() {
}
}

const exportKeys = new Set([
'reg',
'text.cfg',
'text.map',
'text.ctx',
'tag',
'store',
])

async function exportSearchIndex(index) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const exportedData: Record<string, any> = {}
const exportedKeys = new Set<string>()
await index.export((key, data) => {
exportedData[key] = data
exportedKeys.add(key)
if (exportedKeys.size === exportKeys.size) {
resolve(exportedData)
}
})
})
}

function convertTitleToSentence(text: string) {
return text.split(' ').map(str => noCase(str)).join(' ')
}
Expand Down

0 comments on commit c80bef0

Please sign in to comment.