mruwnik 7e1770f384 Enable query analysis by default (runs in parallel with HyDE)
Query analysis and HyDE are both LLM-based operations that run in
parallel via asyncio.gather, so enabling query analysis adds no
extra latency when HyDE is also enabled.

Query analysis provides:
- Modality detection from query content
- Query cleaning and reformulation
- Query variants for better recall

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 16:29:28 +00:00

231 lines
8.6 KiB
TypeScript

import { useMCP } from '@/hooks/useMCP'
import { useEffect, useState } from 'react'
import { DynamicFilters } from './DynamicFilters'
import { SelectableTags } from './SelectableTags'
import { CollectionMetadata } from '@/types/mcp'
type Filter = {
tags?: string[]
source_ids?: string[]
[key: string]: any
}
type SearchConfig = {
previews: boolean
useScores: boolean
limit: number
// Search enhancement options
useBm25?: boolean
useHyde?: boolean
useReranking?: boolean
useQueryAnalysis?: boolean
}
export interface SearchParams {
query: string
modalities: string[]
filters: Filter
config: SearchConfig
}
interface SearchFormProps {
isLoading: boolean
onSearch: (params: SearchParams) => void
}
// Pure helper functions for SearchForm
const createFlags = (items: string[], defaultValue = false): Record<string, boolean> =>
items.reduce((acc, item) => ({ ...acc, [item]: defaultValue }), {})
const getSelectedItems = (items: Record<string, boolean>): string[] =>
Object.entries(items).filter(([_, selected]) => selected).map(([key]) => key)
const cleanFilters = (filters: Record<string, any>): Record<string, any> =>
Object.entries(filters)
.filter(([_, value]) => value !== null && value !== '' && value !== undefined)
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
export const SearchForm = ({ isLoading, onSearch }: SearchFormProps) => {
const [query, setQuery] = useState('')
const [previews, setPreviews] = useState(false)
const [useScores, setUseScores] = useState(false)
const [modalities, setModalities] = useState<Record<string, boolean>>({})
const [schemas, setSchemas] = useState<Record<string, CollectionMetadata>>({})
const [tags, setTags] = useState<Record<string, boolean>>({})
const [dynamicFilters, setDynamicFilters] = useState<Record<string, any>>({})
const [limit, setLimit] = useState(10)
// Search enhancement options - initialize to match server defaults
// All enabled by default (query analysis runs in parallel with HyDE, no extra latency)
const [useBm25, setUseBm25] = useState<boolean | undefined>(true)
const [useHyde, setUseHyde] = useState<boolean | undefined>(true)
const [useReranking, setUseReranking] = useState<boolean | undefined>(true)
const [useQueryAnalysis, setUseQueryAnalysis] = useState<boolean | undefined>(true)
const { getMetadataSchemas, getTags } = useMCP()
useEffect(() => {
const setupFilters = async () => {
const [schemas, tags] = await Promise.all([
getMetadataSchemas(),
getTags()
])
setSchemas(schemas)
setModalities(createFlags(Object.keys(schemas), true))
setTags(createFlags(tags))
}
setupFilters()
}, [getMetadataSchemas, getTags])
const handleFilterChange = (field: string, value: any) =>
setDynamicFilters(prev => ({ ...prev, [field]: value }))
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSearch({
query,
modalities: getSelectedItems(modalities),
config: {
previews,
useScores,
limit,
useBm25,
useHyde,
useReranking,
useQueryAnalysis,
},
filters: {
tags: getSelectedItems(tags),
...cleanFilters(dynamicFilters)
},
})
}
return (
<form onSubmit={handleSubmit} className="search-form">
<div className="search-input-group">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search your knowledge base..."
className="search-input"
required
/>
<button type="submit" disabled={isLoading} className="search-btn">
{isLoading ? 'Searching...' : 'Search'}
</button>
</div>
<div className="search-options">
<div className="search-option">
<label>
<input
type="checkbox"
checked={previews}
onChange={(e) => setPreviews(e.target.checked)}
/>
Include content previews
</label>
</div>
<div className="search-option">
<label>
<input
type="checkbox"
checked={useScores}
onChange={(e) => setUseScores(e.target.checked)}
/>
Score results with a LLM before returning
</label>
</div>
<details className="search-enhancements">
<summary>Search Enhancements</summary>
<div className="enhancement-options">
<div className="search-option">
<label>
<input
type="checkbox"
checked={useBm25}
onChange={(e) => setUseBm25(e.target.checked)}
/>
BM25 keyword search
</label>
</div>
<div className="search-option">
<label>
<input
type="checkbox"
checked={useHyde}
onChange={(e) => setUseHyde(e.target.checked)}
/>
HyDE (hypothetical document expansion)
</label>
</div>
<div className="search-option">
<label>
<input
type="checkbox"
checked={useReranking}
onChange={(e) => setUseReranking(e.target.checked)}
/>
Reranking (cross-encoder)
</label>
</div>
<div className="search-option">
<label>
<input
type="checkbox"
checked={useQueryAnalysis}
onChange={(e) => setUseQueryAnalysis(e.target.checked)}
/>
Query analysis (LLM-based)
</label>
</div>
</div>
</details>
<SelectableTags
title="Modalities"
className="modality-checkboxes"
tags={modalities}
onSelect={(tag, selected) => setModalities({ ...modalities, [tag]: selected })}
onBatchUpdate={(updates) => setModalities(updates)}
/>
<SelectableTags
title="Tags"
className="tags-container"
tags={tags}
onSelect={(tag, selected) => setTags({ ...tags, [tag]: selected })}
onBatchUpdate={(updates) => setTags(updates)}
searchable={true}
/>
<DynamicFilters
schemas={schemas}
selectedModalities={getSelectedItems(modalities)}
filters={dynamicFilters}
onFilterChange={handleFilterChange}
/>
<div className="search-option">
<label>
Max Results:
<input
type="number"
value={limit}
onChange={(e) => setLimit(parseInt(e.target.value) || 10)}
min={1}
max={100}
className="limit-input"
/>
</label>
</div>
</div>
</form>
)
}
export default SearchForm