|
import React, { useState } from 'react'; |
|
import { Search, Zap, Database, Loader2, ArrowRight } from 'lucide-react'; |
|
import { Button } from '@/components/ui/button'; |
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
|
import { Input } from '@/components/ui/input'; |
|
import { Label } from '@/components/ui/label'; |
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|
import { Badge } from '@/components/ui/badge'; |
|
import { Alert, AlertDescription } from '@/components/ui/alert'; |
|
import { Separator } from '@/components/ui/separator'; |
|
|
|
interface VectorSearchResult { |
|
id: string; |
|
title: string; |
|
content: string; |
|
source: string; |
|
relevanceScore: number; |
|
rank: number; |
|
snippet: string; |
|
} |
|
|
|
interface SearchResults { |
|
success: boolean; |
|
query: string; |
|
indexName: string; |
|
results: VectorSearchResult[]; |
|
totalFound: number; |
|
searchTime?: number; |
|
error?: string; |
|
} |
|
|
|
export default function VectorSearch() { |
|
const [query, setQuery] = useState(''); |
|
const [indexName, setIndexName] = useState('research_papers_clean_v2'); |
|
const [maxResults, setMaxResults] = useState(10); |
|
const [isSearching, setIsSearching] = useState(false); |
|
const [searchResults, setSearchResults] = useState<SearchResults | null>(null); |
|
const [comparisonMode, setComparisonMode] = useState(false); |
|
const [traditionalResults, setTraditionalResults] = useState<any>(null); |
|
|
|
const handleVectorSearch = async () => { |
|
if (!query.trim()) return; |
|
|
|
setIsSearching(true); |
|
try { |
|
const response = await fetch('/api/documents/search/vector', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ |
|
query: query.trim(), |
|
indexName, |
|
maxResults |
|
}) |
|
}); |
|
|
|
const result = await response.json(); |
|
setSearchResults(result); |
|
|
|
|
|
if (comparisonMode) { |
|
await runTraditionalSearch(); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Vector search error:', error); |
|
setSearchResults({ |
|
success: false, |
|
query: query.trim(), |
|
indexName, |
|
results: [], |
|
totalFound: 0, |
|
error: error instanceof Error ? error.message : 'Search failed' |
|
}); |
|
} finally { |
|
setIsSearching(false); |
|
} |
|
}; |
|
|
|
const runTraditionalSearch = async () => { |
|
try { |
|
const response = await fetch('/api/search', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ |
|
query: query.trim(), |
|
searchType: 'keyword', |
|
limit: maxResults, |
|
offset: 0 |
|
}) |
|
}); |
|
|
|
const result = await response.json(); |
|
setTraditionalResults(result); |
|
|
|
} catch (error) { |
|
console.error('Traditional search error:', error); |
|
setTraditionalResults(null); |
|
} |
|
}; |
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => { |
|
if (e.key === 'Enter' && !isSearching) { |
|
handleVectorSearch(); |
|
} |
|
}; |
|
|
|
const formatRelevanceScore = (score: number): string => { |
|
return (score * 100).toFixed(1) + '%'; |
|
}; |
|
|
|
const getScoreColor = (score: number): string => { |
|
if (score >= 0.8) return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; |
|
if (score >= 0.6) return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; |
|
if (score >= 0.4) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; |
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'; |
|
}; |
|
|
|
return ( |
|
<div className="space-y-6"> |
|
{/* Search Interface */} |
|
<Card> |
|
<CardHeader> |
|
<CardTitle className="flex items-center gap-2"> |
|
<Zap className="w-5 h-5 text-blue-500" /> |
|
Vector Search (Modal + FAISS) |
|
</CardTitle> |
|
</CardHeader> |
|
<CardContent className="space-y-4"> |
|
{/* Search Input */} |
|
<div className="space-y-2"> |
|
<Label htmlFor="vector-query">Search Query</Label> |
|
<div className="flex gap-2"> |
|
<Input |
|
id="vector-query" |
|
placeholder="Enter your search query for semantic similarity matching..." |
|
value={query} |
|
onChange={(e) => setQuery(e.target.value)} |
|
onKeyPress={handleKeyPress} |
|
className="flex-1" |
|
/> |
|
<Button |
|
onClick={handleVectorSearch} |
|
disabled={isSearching || !query.trim()} |
|
> |
|
{isSearching ? ( |
|
<Loader2 className="w-4 h-4 animate-spin" /> |
|
) : ( |
|
<Search className="w-4 h-4" /> |
|
)} |
|
</Button> |
|
</div> |
|
</div> |
|
|
|
{/* Search Options */} |
|
<div className="grid grid-cols-3 gap-4"> |
|
<div> |
|
<Label htmlFor="index-name">Vector Index</Label> |
|
<Select value={indexName} onValueChange={setIndexName}> |
|
<SelectTrigger> |
|
<SelectValue /> |
|
</SelectTrigger> |
|
<SelectContent> |
|
<SelectItem value="research_papers_clean_v2">Research Papers (Clean)</SelectItem> |
|
<SelectItem value="main_index">Main Index (Legacy - has uploaded docs)</SelectItem> |
|
<SelectItem value="academic_index">Academic Papers</SelectItem> |
|
</SelectContent> |
|
</Select> |
|
</div> |
|
|
|
<div> |
|
<Label htmlFor="max-results">Max Results</Label> |
|
<Select value={maxResults.toString()} onValueChange={(value) => setMaxResults(parseInt(value))}> |
|
<SelectTrigger> |
|
<SelectValue /> |
|
</SelectTrigger> |
|
<SelectContent> |
|
<SelectItem value="5">5 results</SelectItem> |
|
<SelectItem value="10">10 results</SelectItem> |
|
<SelectItem value="20">20 results</SelectItem> |
|
<SelectItem value="50">50 results</SelectItem> |
|
</SelectContent> |
|
</Select> |
|
</div> |
|
|
|
<div className="flex items-end"> |
|
<Button |
|
variant="outline" |
|
onClick={() => setComparisonMode(!comparisonMode)} |
|
className="w-full" |
|
> |
|
<Database className="w-4 h-4 mr-2" /> |
|
{comparisonMode ? 'Comparison: ON' : 'Compare with Keyword'} |
|
</Button> |
|
</div> |
|
</div> |
|
|
|
{/* Search Info */} |
|
<Alert> |
|
<Database className="w-4 h-4" /> |
|
<AlertDescription> |
|
Vector search uses Modal.com's distributed FAISS implementation for high-performance semantic similarity matching. |
|
Enable comparison mode to see differences between vector and traditional keyword search. |
|
</AlertDescription> |
|
</Alert> |
|
</CardContent> |
|
</Card> |
|
|
|
{/* Search Results */} |
|
{searchResults && ( |
|
<div className="grid grid-cols-1 gap-6"> |
|
{/* Vector Search Results */} |
|
<Card> |
|
<CardHeader> |
|
<div className="flex items-center justify-between"> |
|
<CardTitle className="flex items-center gap-2"> |
|
<Zap className="w-5 h-5 text-blue-500" /> |
|
Vector Search Results |
|
</CardTitle> |
|
<div className="flex items-center gap-4 text-sm text-gray-500"> |
|
{searchResults.searchTime && ( |
|
<span>Search time: {(searchResults.searchTime * 1000).toFixed(0)}ms</span> |
|
)} |
|
<Badge variant="outline"> |
|
{searchResults.totalFound} results found |
|
</Badge> |
|
</div> |
|
</div> |
|
</CardHeader> |
|
<CardContent> |
|
{searchResults.success ? ( |
|
<div className="space-y-4"> |
|
{searchResults.results.length === 0 ? ( |
|
<div className="text-center py-8 text-gray-500"> |
|
<Database className="w-12 h-12 mx-auto mb-4 opacity-50" /> |
|
<p>No results found in vector index.</p> |
|
<p className="text-sm">Try uploading and processing some documents first.</p> |
|
</div> |
|
) : ( |
|
searchResults.results.map((result, index) => ( |
|
<div key={result.id} className="border rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"> |
|
<div className="flex items-start justify-between mb-2"> |
|
<div className="flex items-center gap-3"> |
|
<Badge variant="outline" className="text-xs"> |
|
#{result.rank} |
|
</Badge> |
|
<Badge className={getScoreColor(result.relevanceScore)}> |
|
{formatRelevanceScore(result.relevanceScore)} |
|
</Badge> |
|
</div> |
|
<span className="text-xs text-gray-500">ID: {result.id}</span> |
|
</div> |
|
|
|
<h3 className="font-semibold text-lg mb-2">{result.title}</h3> |
|
|
|
<p className="text-gray-600 dark:text-gray-300 mb-3 leading-relaxed"> |
|
{result.snippet} |
|
</p> |
|
|
|
<div className="flex items-center justify-between text-sm text-gray-500"> |
|
<span>{result.source}</span> |
|
<ArrowRight className="w-4 h-4" /> |
|
</div> |
|
</div> |
|
)) |
|
)} |
|
</div> |
|
) : ( |
|
<Alert variant="destructive"> |
|
<AlertDescription> |
|
{searchResults.error || 'Vector search failed'} |
|
</AlertDescription> |
|
</Alert> |
|
)} |
|
</CardContent> |
|
</Card> |
|
|
|
{/* Traditional Search Results (if comparison mode) */} |
|
{comparisonMode && traditionalResults && ( |
|
<Card> |
|
<CardHeader> |
|
<CardTitle className="flex items-center gap-2"> |
|
<Search className="w-5 h-5 text-gray-500" /> |
|
Traditional Search Results (for comparison) |
|
</CardTitle> |
|
</CardHeader> |
|
<CardContent> |
|
{traditionalResults.results && traditionalResults.results.length > 0 ? ( |
|
<div className="space-y-4"> |
|
{traditionalResults.results.slice(0, maxResults).map((result: any, index: number) => ( |
|
<div key={result.id} className="border rounded-lg p-4 opacity-75"> |
|
<div className="flex items-start justify-between mb-2"> |
|
<Badge variant="outline" className="text-xs"> |
|
#{index + 1} |
|
</Badge> |
|
<Badge variant="secondary"> |
|
{formatRelevanceScore(result.relevanceScore || 0)} |
|
</Badge> |
|
</div> |
|
|
|
<h3 className="font-semibold text-lg mb-2">{result.title}</h3> |
|
|
|
<p className="text-gray-600 dark:text-gray-300 mb-3 leading-relaxed"> |
|
{result.snippet} |
|
</p> |
|
|
|
<div className="text-sm text-gray-500"> |
|
{result.source} |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
) : ( |
|
<div className="text-center py-4 text-gray-500"> |
|
<p>No traditional search results found.</p> |
|
</div> |
|
)} |
|
</CardContent> |
|
</Card> |
|
)} |
|
</div> |
|
)} |
|
|
|
{/* Help Text */} |
|
{!searchResults && ( |
|
<Card> |
|
<CardContent className="pt-6"> |
|
<div className="text-center text-gray-500 space-y-2"> |
|
<Database className="w-16 h-16 mx-auto opacity-50" /> |
|
<h3 className="text-lg font-medium">Advanced Vector Search</h3> |
|
<p className="text-sm max-w-md mx-auto"> |
|
Search through your documents using semantic similarity powered by Modal.com's distributed FAISS implementation. |
|
Upload documents first to build your vector index. |
|
</p> |
|
</div> |
|
</CardContent> |
|
</Card> |
|
)} |
|
</div> |
|
); |
|
} |