@@ -35,43 +35,64 @@ const fuseOptions: IFuseOptions<DocMetadata> = {
3535const FUSE_WEIGHT = 0.3 ;
3636const BACKEND_WEIGHT = 0.7 ;
3737
38- type BackendDocResult = { fileKey : string ; similarityScore : number } ;
38+ type BackendDocumentResult = {
39+ fileKey : string ;
40+ similarityScore : number ;
41+ } ;
3942
40- function mergeHybridResults (
43+ /**
44+ * Merges Fuse.js (local fuzzy search) and backend (semantic search) results
45+ * into a single ranked list.
46+ */
47+ const mergeHybridResults = (
4148 fuseResults : Fuse . FuseResult < DocMetadata > [ ] ,
42- backendResults : BackendDocResult [ ] ,
43- allDocs : DocMetadata [ ]
44- ) : DocMetadata [ ] {
49+ backendResults : BackendDocumentResult [ ] ,
50+ allDocuments : DocMetadata [ ]
51+ ) : DocMetadata [ ] => {
4552 const normalizeFuse = ( score ?: number ) => 1 - Math . min ( ( score ?? 1 ) / 0.5 , 1 ) ;
4653 const normalizeBackend = ( score : number ) => Math . min ( score , 1 ) ;
4754
4855 const backendMap = new Map (
49- backendResults . map ( ( r ) => [ r . fileKey , normalizeBackend ( r . similarityScore ) ] )
56+ backendResults . map ( ( result ) => [
57+ result . fileKey ,
58+ normalizeBackend ( result . similarityScore ) ,
59+ ] )
5060 ) ;
51- const combined = new Map < string , { doc : DocMetadata ; score : number } > ( ) ;
5261
62+ const combined = new Map < string , { document : DocMetadata ; score : number } > ( ) ;
63+
64+ // Combine both Fuse and backend scores where available
5365 for ( const fuseItem of fuseResults ) {
54- const doc = fuseItem . item ;
66+ const document = fuseItem . item ;
5567 const fuseScore = normalizeFuse ( fuseItem . score ) ;
56- const backendScore = backendMap . get ( doc . docKey ) ;
68+ const backendScore = backendMap . get ( document . docKey ) ;
5769 const combinedScore = backendScore
5870 ? BACKEND_WEIGHT * backendScore + FUSE_WEIGHT * fuseScore
5971 : fuseScore ;
60- combined . set ( doc . docKey , { doc , score : combinedScore } ) ;
72+ combined . set ( document . docKey , { document , score : combinedScore } ) ;
6173 }
6274
75+ // Include backend-only results not found by Fuse
6376 for ( const [ fileKey , backendScore ] of backendMap ) {
6477 if ( ! combined . has ( fileKey ) ) {
65- const doc = allDocs . find ( ( d ) => d . docKey === fileKey ) ;
66- if ( doc )
67- combined . set ( fileKey , { doc, score : BACKEND_WEIGHT * backendScore } ) ;
78+ const document = allDocuments . find (
79+ ( doc ) =>
80+ doc . docKey === fileKey ||
81+ doc . url . endsWith ( fileKey ) ||
82+ doc . url . includes ( fileKey )
83+ ) ;
84+ if ( document )
85+ combined . set ( fileKey , {
86+ document,
87+ score : BACKEND_WEIGHT * backendScore ,
88+ } ) ;
6889 }
6990 }
7091
7192 return Array . from ( combined . values ( ) )
7293 . sort ( ( a , b ) => b . score - a . score )
73- . map ( ( v ) => v . doc ) ;
74- }
94+ . map ( ( value ) => value . document ) ;
95+ } ;
7596
7697const SearchResultItem : FC < { doc : DocMetadata ; onClickLink : ( ) => void } > = ( {
7798 doc,
@@ -105,16 +126,17 @@ const SearchResultItem: FC<{ doc: DocMetadata; onClickLink: () => void }> = ({
105126 ) ;
106127} ;
107128
129+ /**
130+ * SearchView — combines Fuse.js fuzzy search (client-side)
131+ * with backend semantic vector results for robust doc discovery.
132+ */
108133export const SearchView : FC < {
109134 onClickLink ?: ( ) => void ;
110135 isOpen ?: boolean ;
111136} > = ( { onClickLink = ( ) => { } , isOpen = false } ) => {
112137 const inputRef = useRef < HTMLInputElement > ( null ) ;
113138 const searchQueryParam = useSearchParams ( ) . get ( 'search' ) ;
114139 const [ results , setResults ] = useState < DocMetadata [ ] > ( [ ] ) ;
115- const [ currentQuery , setCurrentQuery ] = useState < string | null > (
116- searchQueryParam
117- ) ;
118140
119141 const { search, setSearch } = useSearch ( {
120142 defaultValue : searchQueryParam ,
@@ -127,38 +149,52 @@ export const SearchView: FC<{
127149
128150 const docs = getIntlayer ( 'doc-metadata' , locale ) as DocMetadata [ ] ;
129151 const blogs = getIntlayer ( 'blog-metadata' , locale ) as BlogMetadata [ ] ;
130- const allDocs = [ ...docs , ...blogs ] ;
131- const fuse = new Fuse ( allDocs , fuseOptions ) ;
152+ const allDocuments = [ ...docs , ...blogs ] ;
153+ const fuse = new Fuse ( allDocuments , fuseOptions ) ;
132154
133155 useEffect ( ( ) => {
134- if ( backendData ?. data && currentQuery ) {
135- const backendResults : BackendDocResult [ ] = backendData . data . map (
136- ( doc ) => ( {
137- fileKey : doc . fileKey ,
138- similarityScore : doc . similarityScore ?? 0.5 ,
139- } )
156+ if ( backendData ?. data && search ) {
157+ // Normalize backend response (string paths → structured results)
158+ const backendResults : BackendDocumentResult [ ] = backendData . data . map (
159+ ( item : any ) => {
160+ if ( typeof item === 'string' ) {
161+ return {
162+ fileKey : item . replace ( / ^ \. \/ / , '' ) ,
163+ similarityScore : 0.5 ,
164+ } ;
165+ }
166+ return {
167+ fileKey : item . fileKey ,
168+ similarityScore : item . similarityScore ?? 0.5 ,
169+ } ;
170+ }
140171 ) ;
141172
142- const fuseResults = fuse . search ( currentQuery ) ;
173+ const fuseResults = fuse . search ( search ) ;
143174 let mergedResults : DocMetadata [ ] ;
144175
145176 if ( fuseResults . length > 0 ) {
146- // Hybrid mode: combine Fuse.js and backend semantic results
147177 mergedResults = mergeHybridResults (
148178 fuseResults ,
149179 backendResults ,
150- filesData
180+ allDocuments
151181 ) ;
152182 } else {
153- // Fallback: show backend-only results when Fuse finds none
154183 mergedResults = backendResults
155- . map ( ( r ) => filesData . find ( ( d ) => d . docKey === r . fileKey ) )
156- . filter ( ( d ) : d is DocMetadata => Boolean ( d ) ) ;
184+ . map ( ( result ) =>
185+ allDocuments . find (
186+ ( doc ) =>
187+ doc . docKey === result . fileKey ||
188+ doc . url . endsWith ( result . fileKey ) ||
189+ doc . url . includes ( result . fileKey )
190+ )
191+ )
192+ . filter ( ( doc ) : doc is DocMetadata => Boolean ( doc ) ) ;
157193 }
158194
159195 setResults ( mergedResults ) ;
160196 }
161- } , [ backendData , currentQuery , allDocs , fuse ] ) ;
197+ } , [ backendData , search , allDocuments , fuse ] ) ;
162198
163199 const isNoResult =
164200 ! isFetching &&
@@ -187,9 +223,9 @@ export const SearchView: FC<{
187223 ) }
188224 { results . length > 0 && (
189225 < ul className = "flex flex-col gap-10" >
190- { results . map ( ( r ) => (
191- < li key = { r . url } >
192- < SearchResultItem doc = { r } onClickLink = { onClickLink } />
226+ { results . map ( ( result ) => (
227+ < li key = { result . url } >
228+ < SearchResultItem doc = { result } onClickLink = { onClickLink } />
193229 </ li >
194230 ) ) }
195231 </ ul >
0 commit comments