mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-15 00:33:44 +08:00
feat: Enhance relevance scoring in search functionality; add scoring for actor, director, and content matches; improve query handling and penalties for long titles
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { getSourceName, SOURCE_IDS } from '@/lib/utils/source-names';
|
||||
import { checkVideoAvailability } from '@/lib/utils/source-checker';
|
||||
import { calculateRelevanceScore } from '@/lib/utils/search';
|
||||
|
||||
interface Video {
|
||||
vod_id: string;
|
||||
@@ -16,6 +17,9 @@ interface Video {
|
||||
isNew?: boolean;
|
||||
isVerifying?: boolean;
|
||||
vod_play_url?: string;
|
||||
vod_actor?: string;
|
||||
vod_director?: string;
|
||||
relevanceScore?: number;
|
||||
}
|
||||
|
||||
export interface ParallelSearchResult {
|
||||
@@ -40,6 +44,7 @@ export function useParallelSearch(
|
||||
const [completedSources, setCompletedSources] = useState(0);
|
||||
const [totalSources, setTotalSources] = useState(0);
|
||||
const [totalVideosFound, setTotalVideosFound] = useState(0);
|
||||
const currentQueryRef = useRef<string>('');
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const verificationQueueRef = useRef<Video[]>([]);
|
||||
@@ -65,12 +70,14 @@ export function useParallelSearch(
|
||||
|
||||
setResults((prev) => {
|
||||
if (isValid) {
|
||||
// Remove verifying badge
|
||||
return prev.map((v) =>
|
||||
// Remove verifying badge, maintain sort order
|
||||
const updated = prev.map((v) =>
|
||||
v.vod_id === video.vod_id && v.source === video.source
|
||||
? { ...v, isVerifying: false }
|
||||
: v
|
||||
);
|
||||
// Re-sort by relevance to maintain order
|
||||
return updated.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
||||
} else {
|
||||
// Remove invalid video
|
||||
const removedVideo = prev.find(
|
||||
@@ -88,9 +95,11 @@ export function useParallelSearch(
|
||||
);
|
||||
}
|
||||
|
||||
return prev.filter(
|
||||
const filtered = prev.filter(
|
||||
(v) => !(v.vod_id === video.vod_id && v.source === video.source)
|
||||
);
|
||||
// Maintain sort order
|
||||
return filtered.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -118,9 +127,11 @@ export function useParallelSearch(
|
||||
);
|
||||
}
|
||||
|
||||
return prev.filter(
|
||||
const filtered = prev.filter(
|
||||
(v) => !(v.vod_id === video.vod_id && v.source === video.source)
|
||||
);
|
||||
// Maintain sort order
|
||||
return filtered.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
||||
});
|
||||
|
||||
// Continue processing queue
|
||||
@@ -159,6 +170,7 @@ export function useParallelSearch(
|
||||
verificationQueueRef.current = [];
|
||||
verifyingCountRef.current = 0;
|
||||
allVideosReceivedRef.current = false;
|
||||
currentQueryRef.current = searchQuery.trim();
|
||||
|
||||
// Update URL
|
||||
onUrlUpdate(searchQuery);
|
||||
@@ -199,17 +211,23 @@ export function useParallelSearch(
|
||||
console.log(`[useParallelSearch] Started search for ${data.totalSources} sources`);
|
||||
}
|
||||
else if (data.type === 'videos') {
|
||||
const currentQuery = currentQueryRef.current;
|
||||
const newVideos: Video[] = data.videos.map((video: any) => ({
|
||||
...video,
|
||||
sourceName: video.sourceDisplayName || getSourceName(video.source),
|
||||
isNew: true,
|
||||
isVerifying: true, // All videos start as verifying
|
||||
relevanceScore: calculateRelevanceScore(video, currentQuery),
|
||||
}));
|
||||
|
||||
console.log(`[useParallelSearch] Received ${newVideos.length} videos from source ${data.source}`);
|
||||
|
||||
// Add videos to display immediately
|
||||
setResults((prev) => [...prev, ...newVideos]);
|
||||
// Add videos and sort by relevance
|
||||
setResults((prev) => {
|
||||
const combined = [...prev, ...newVideos];
|
||||
// Sort by relevance score (highest first)
|
||||
return combined.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
||||
});
|
||||
|
||||
// Queue videos for verification
|
||||
queueVideosForVerification(newVideos);
|
||||
@@ -286,6 +304,7 @@ export function useParallelSearch(
|
||||
verificationQueueRef.current = [];
|
||||
verifyingCountRef.current = 0;
|
||||
allVideosReceivedRef.current = false;
|
||||
currentQueryRef.current = '';
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
+101
-20
@@ -261,40 +261,121 @@ export function clearSearchHistory(): void {
|
||||
|
||||
/**
|
||||
* Calculate search relevance score
|
||||
* Higher score = more relevant to the search query
|
||||
*/
|
||||
export function calculateRelevanceScore(item: VideoItem, query: string): number {
|
||||
let score = 0;
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
const normalizedTitle = item.vod_name.toLowerCase();
|
||||
|
||||
// Exact title match
|
||||
if (item.vod_name.toLowerCase() === normalizedQuery) {
|
||||
// Split query into words for partial matching
|
||||
const queryWords = normalizedQuery.split(/\s+/);
|
||||
|
||||
// 1. Exact title match (highest priority)
|
||||
if (normalizedTitle === normalizedQuery) {
|
||||
score += 1000;
|
||||
return score; // Early return for perfect match
|
||||
}
|
||||
|
||||
// 2. Title starts with query (very high priority)
|
||||
if (normalizedTitle.startsWith(normalizedQuery)) {
|
||||
score += 500;
|
||||
}
|
||||
|
||||
// 3. Title contains full query as substring
|
||||
if (normalizedTitle.includes(normalizedQuery)) {
|
||||
score += 200;
|
||||
|
||||
// Bonus for query appearing earlier in title
|
||||
const position = normalizedTitle.indexOf(normalizedQuery);
|
||||
score += Math.max(0, 50 - position * 2);
|
||||
}
|
||||
|
||||
// 4. All query words present in title
|
||||
const allWordsPresent = queryWords.every(word =>
|
||||
normalizedTitle.includes(word)
|
||||
);
|
||||
if (allWordsPresent && queryWords.length > 1) {
|
||||
score += 100;
|
||||
}
|
||||
// Title starts with query
|
||||
else if (item.vod_name.toLowerCase().startsWith(normalizedQuery)) {
|
||||
score += 50;
|
||||
}
|
||||
// Title contains query
|
||||
else if (item.vod_name.toLowerCase().includes(normalizedQuery)) {
|
||||
score += 25;
|
||||
|
||||
// 5. Individual word matches
|
||||
queryWords.forEach(word => {
|
||||
if (word.length < 2) return; // Skip very short words
|
||||
|
||||
if (normalizedTitle.includes(word)) {
|
||||
score += 30;
|
||||
|
||||
// Bonus if word is at the start
|
||||
if (normalizedTitle.startsWith(word)) {
|
||||
score += 20;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 6. Actor match
|
||||
if (item.vod_actor) {
|
||||
const normalizedActor = item.vod_actor.toLowerCase();
|
||||
if (normalizedActor.includes(normalizedQuery)) {
|
||||
score += 80;
|
||||
}
|
||||
queryWords.forEach(word => {
|
||||
if (word.length >= 2 && normalizedActor.includes(word)) {
|
||||
score += 15;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Actor match
|
||||
if (item.vod_actor?.toLowerCase().includes(normalizedQuery)) {
|
||||
score += 10;
|
||||
// 7. Director match
|
||||
if (item.vod_director) {
|
||||
const normalizedDirector = item.vod_director.toLowerCase();
|
||||
if (normalizedDirector.includes(normalizedQuery)) {
|
||||
score += 60;
|
||||
}
|
||||
queryWords.forEach(word => {
|
||||
if (word.length >= 2 && normalizedDirector.includes(word)) {
|
||||
score += 10;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Director match
|
||||
if (item.vod_director?.toLowerCase().includes(normalizedQuery)) {
|
||||
score += 10;
|
||||
// 8. Content/description match (if available)
|
||||
if (item.vod_content) {
|
||||
const normalizedContent = item.vod_content.toLowerCase();
|
||||
if (normalizedContent.includes(normalizedQuery)) {
|
||||
score += 20;
|
||||
}
|
||||
}
|
||||
|
||||
// Recent year bonus
|
||||
// 9. Recent year bonus (favor newer content)
|
||||
const currentYear = new Date().getFullYear();
|
||||
const itemYear = parseInt(item.vod_year || '0');
|
||||
if (itemYear >= currentYear - 2) {
|
||||
score += 5;
|
||||
if (itemYear > 0) {
|
||||
const yearDiff = currentYear - itemYear;
|
||||
if (yearDiff === 0) {
|
||||
score += 15; // Current year
|
||||
} else if (yearDiff === 1) {
|
||||
score += 10; // Last year
|
||||
} else if (yearDiff <= 3) {
|
||||
score += 5; // Within 3 years
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
// 10. Penalty for very long titles (might be less relevant)
|
||||
if (item.vod_name.length > 50) {
|
||||
score -= 5;
|
||||
}
|
||||
|
||||
// 11. Bonus for HD/quality indicators in remarks
|
||||
if (item.vod_remarks) {
|
||||
const remarks = item.vod_remarks.toLowerCase();
|
||||
if (remarks.includes('hd') || remarks.includes('1080') || remarks.includes('4k')) {
|
||||
score += 5;
|
||||
}
|
||||
if (remarks.includes('完结') || remarks.includes('全集')) {
|
||||
score += 3;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(0, score); // Ensure non-negative
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user