mirror of
https://github.com/mruwnik/memory.git
synced 2026-01-02 09:12:58 +01:00
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>
231 lines
8.6 KiB
TypeScript
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 |