Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
683050f271
commit
8d19ff39a7
15
package-lock.json
generated
15
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"firebase": "^11.10.0",
|
"firebase": "^11.10.0",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
@ -3336,6 +3337,15 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuse.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
@ -6841,6 +6851,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"fuse.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="
|
||||||
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"firebase": "^11.10.0",
|
"firebase": "^11.10.0",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useAppSelector, selectArtistsArray, selectSongsArray } from '../redux';
|
import { useAppSelector, selectArtistsArray, selectSongsArray } from '../redux';
|
||||||
import { useActions } from './useActions';
|
import { useActions } from './useActions';
|
||||||
import { usePaginatedData } from './index';
|
import { usePaginatedData } from './index';
|
||||||
|
import { filterArtists } from '../utils/dataProcessing';
|
||||||
import type { Song } from '../types';
|
import type { Song } from '../types';
|
||||||
|
|
||||||
export const useArtists = () => {
|
export const useArtists = () => {
|
||||||
@ -9,6 +10,9 @@ export const useArtists = () => {
|
|||||||
const allSongs = useAppSelector(selectSongsArray);
|
const allSongs = useAppSelector(selectSongsArray);
|
||||||
const { handleAddToQueue, handleToggleFavorite } = useActions();
|
const { handleAddToQueue, handleToggleFavorite } = useActions();
|
||||||
|
|
||||||
|
// Manage search term locally
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
// Pre-compute songs by artist and song counts for performance
|
// Pre-compute songs by artist and song counts for performance
|
||||||
const songsByArtist = useMemo(() => {
|
const songsByArtist = useMemo(() => {
|
||||||
const songsMap = new Map<string, Song[]>();
|
const songsMap = new Map<string, Song[]>();
|
||||||
@ -27,8 +31,13 @@ export const useArtists = () => {
|
|||||||
return { songsMap, countsMap };
|
return { songsMap, countsMap };
|
||||||
}, [allSongs]);
|
}, [allSongs]);
|
||||||
|
|
||||||
// Use the composable pagination hook
|
// Apply fuzzy search to artists
|
||||||
const pagination = usePaginatedData(allArtists, {
|
const filteredArtists = useMemo(() => {
|
||||||
|
return filterArtists(allArtists, searchTerm);
|
||||||
|
}, [allArtists, searchTerm]);
|
||||||
|
|
||||||
|
// Use the composable pagination hook with fuzzy-filtered results
|
||||||
|
const pagination = usePaginatedData(filteredArtists, {
|
||||||
itemsPerPage: 20 // Default pagination size
|
itemsPerPage: 20 // Default pagination size
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -42,15 +51,21 @@ export const useArtists = () => {
|
|||||||
return songsByArtist.countsMap.get((artistName || '').toLowerCase()) || 0;
|
return songsByArtist.countsMap.get((artistName || '').toLowerCase()) || 0;
|
||||||
}, [songsByArtist.countsMap]);
|
}, [songsByArtist.countsMap]);
|
||||||
|
|
||||||
|
// Handle search term changes
|
||||||
|
const handleSearchChange = useCallback((value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
pagination.resetPage && pagination.resetPage(); // Reset to first page on new search
|
||||||
|
}, [pagination]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
artists: pagination.items,
|
artists: pagination.items,
|
||||||
allArtists: pagination.searchTerm ? pagination.items : allArtists,
|
allArtists: searchTerm ? pagination.items : allArtists,
|
||||||
searchTerm: pagination.searchTerm,
|
searchTerm,
|
||||||
hasMore: pagination.hasMore,
|
hasMore: pagination.hasMore,
|
||||||
loadMore: pagination.loadMore,
|
loadMore: pagination.loadMore,
|
||||||
currentPage: pagination.currentPage,
|
currentPage: pagination.currentPage,
|
||||||
totalPages: pagination.totalPages,
|
totalPages: pagination.totalPages,
|
||||||
handleSearchChange: pagination.setSearchTerm,
|
handleSearchChange,
|
||||||
getSongsByArtist,
|
getSongsByArtist,
|
||||||
getSongCountByArtist,
|
getSongCountByArtist,
|
||||||
handleAddToQueue,
|
handleAddToQueue,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useActions } from './useActions';
|
import { useActions } from './useActions';
|
||||||
import { useFilteredSongs, usePaginatedData } from './index';
|
import { useFilteredSongs, usePaginatedData } from './index';
|
||||||
import { UI_CONSTANTS } from '../constants';
|
import { UI_CONSTANTS } from '../constants';
|
||||||
@ -6,20 +6,25 @@ import { UI_CONSTANTS } from '../constants';
|
|||||||
export const useSearch = () => {
|
export const useSearch = () => {
|
||||||
const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActions();
|
const { handleAddToQueue, handleToggleFavorite, handleToggleDisabled, isSongDisabled } = useActions();
|
||||||
|
|
||||||
// Use the composable filtered songs hook
|
// Manage search term locally
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Use the composable filtered songs hook, passing the search term
|
||||||
const { songs: filteredSongs, disabledSongsLoading } = useFilteredSongs({
|
const { songs: filteredSongs, disabledSongsLoading } = useFilteredSongs({
|
||||||
|
searchTerm,
|
||||||
context: 'useSearch'
|
context: 'useSearch'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the composable pagination hook
|
// Use the composable pagination hook (no search term here, just paginates filtered results)
|
||||||
const pagination = usePaginatedData(filteredSongs, {
|
const pagination = usePaginatedData(filteredSongs, {
|
||||||
itemsPerPage: UI_CONSTANTS.PAGINATION.ITEMS_PER_PAGE
|
itemsPerPage: UI_CONSTANTS.PAGINATION.ITEMS_PER_PAGE
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update search term and reset pagination when search changes
|
||||||
const handleSearchChange = useCallback((value: string) => {
|
const handleSearchChange = useCallback((value: string) => {
|
||||||
// Only search if the term meets minimum length requirement
|
|
||||||
if (value.length >= UI_CONSTANTS.SEARCH.MIN_SEARCH_LENGTH || value.length === 0) {
|
if (value.length >= UI_CONSTANTS.SEARCH.MIN_SEARCH_LENGTH || value.length === 0) {
|
||||||
pagination.setSearchTerm(value);
|
setSearchTerm(value);
|
||||||
|
pagination.resetPage && pagination.resetPage(); // Optional: reset to first page on new search
|
||||||
}
|
}
|
||||||
}, [pagination]);
|
}, [pagination]);
|
||||||
|
|
||||||
@ -33,7 +38,7 @@ export const useSearch = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchTerm: pagination.searchTerm,
|
searchTerm,
|
||||||
searchResults,
|
searchResults,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
handleAddToQueue,
|
handleAddToQueue,
|
||||||
|
|||||||
@ -175,7 +175,6 @@ const queueSlice = createSlice({
|
|||||||
.addCase(removeFromQueue.fulfilled, (state, action) => {
|
.addCase(removeFromQueue.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
const { key } = action.payload;
|
const { key } = action.payload;
|
||||||
console.log('removeFromQueue.fulfilled - removing key:', key);
|
|
||||||
|
|
||||||
// Clear the queue state - the real-time sync will update it with the new data
|
// Clear the queue state - the real-time sync will update it with the new data
|
||||||
state.data = {};
|
state.data = {};
|
||||||
@ -212,7 +211,6 @@ const queueSlice = createSlice({
|
|||||||
.addCase(reorderQueueAsync.fulfilled, (state, action) => {
|
.addCase(reorderQueueAsync.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
const { updates } = action.payload;
|
const { updates } = action.payload;
|
||||||
console.log('reorderQueueAsync.fulfilled - updates:', updates);
|
|
||||||
|
|
||||||
// Clear the queue state - the real-time sync will update it with the new data
|
// Clear the queue state - the real-time sync will update it with the new data
|
||||||
state.data = {};
|
state.data = {};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { debugLog } from './logger';
|
import { debugLog } from './logger';
|
||||||
import type { Song, QueueItem, TopPlayed } from '../types';
|
import type { Song, QueueItem, TopPlayed } from '../types';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
// Convert Firebase object to array with keys
|
// Convert Firebase object to array with keys
|
||||||
export const objectToArray = <T extends { key?: string }>(
|
export const objectToArray = <T extends { key?: string }>(
|
||||||
@ -18,8 +19,11 @@ export const filterDisabledSongs = (songs: Song[], disabledSongPaths: Set<string
|
|||||||
return songs.filter(song => !disabledSongPaths.has(song.path));
|
return songs.filter(song => !disabledSongPaths.has(song.path));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter songs by search term with intelligent multi-word handling
|
// Filter songs by search term with FUZZY MATCHING using Fuse.js (90% threshold)
|
||||||
|
// TODO: REVERT - This is the new fuzzy matching implementation. To revert, replace with the old filterSongs function below
|
||||||
export const filterSongs = (songs: Song[], searchTerm: string, disabledSongPaths?: Set<string>): Song[] => {
|
export const filterSongs = (songs: Song[], searchTerm: string, disabledSongPaths?: Set<string>): Song[] => {
|
||||||
|
debugLog('🚀 FILTER SONGS CALLED with:', { searchTerm, songsCount: songs.length });
|
||||||
|
|
||||||
let filteredSongs = songs;
|
let filteredSongs = songs;
|
||||||
|
|
||||||
// First filter out disabled songs if disabledSongPaths is provided
|
// First filter out disabled songs if disabledSongPaths is provided
|
||||||
@ -27,26 +31,273 @@ export const filterSongs = (songs: Song[], searchTerm: string, disabledSongPaths
|
|||||||
filteredSongs = filterDisabledSongs(songs, disabledSongPaths);
|
filteredSongs = filterDisabledSongs(songs, disabledSongPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!searchTerm.trim()) return filteredSongs;
|
if (!searchTerm.trim()) {
|
||||||
|
debugLog('📝 No search term, returning all songs');
|
||||||
|
return filteredSongs;
|
||||||
|
}
|
||||||
|
|
||||||
const terms = (searchTerm || '').toLowerCase().split(/\s+/).filter(term => term.length > 0);
|
// Configure Fuse.js for fuzzy matching with 90% threshold
|
||||||
|
// Note: Fuse.js threshold is 0.0 (exact) to 1.0 (very loose), so 0.1 = 90% similarity
|
||||||
|
const fuseOptions = {
|
||||||
|
keys: ['title', 'artist'],
|
||||||
|
threshold: 0.2, // 80% similarity threshold (more reasonable)
|
||||||
|
includeScore: true,
|
||||||
|
includeMatches: false,
|
||||||
|
minMatchCharLength: 2, // Allow shorter matches
|
||||||
|
shouldSort: true,
|
||||||
|
findAllMatches: true,
|
||||||
|
location: 0,
|
||||||
|
distance: 100, // More reasonable distance
|
||||||
|
useExtendedSearch: false,
|
||||||
|
ignoreLocation: true, // Allow words anywhere in the text
|
||||||
|
ignoreFieldNorm: false,
|
||||||
|
};
|
||||||
|
|
||||||
if (terms.length === 0) return filteredSongs;
|
// Split search term into individual words for better matching
|
||||||
|
const searchWords = searchTerm.toLowerCase().split(/\s+/).filter(word => word.length >= 2);
|
||||||
|
|
||||||
return filteredSongs.filter(song => {
|
debugLog('🔍 FUZZY SEARCH DEBUG:', {
|
||||||
const songTitle = (song.title || '').toLowerCase();
|
originalSearchTerm: searchTerm,
|
||||||
const songArtist = (song.artist || '').toLowerCase();
|
searchWords: searchWords,
|
||||||
|
totalSongsToSearch: filteredSongs.length,
|
||||||
|
firstFewSongs: filteredSongs.slice(0, 3).map(s => `${s.artist} - ${s.title}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchWords.length === 0) {
|
||||||
|
debugLog('❌ No search words found, returning all songs');
|
||||||
|
return filteredSongs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for each word individually and find songs that contain ALL words
|
||||||
|
const songsWithAllWords = new Map<Song, number[]>(); // Song -> array of scores for each word
|
||||||
|
|
||||||
|
searchWords.forEach((word, wordIndex) => {
|
||||||
|
debugLog(`\n🔤 Searching for word ${wordIndex + 1}: "${word}"`);
|
||||||
|
|
||||||
// If only one term, use OR logic (title OR artist)
|
const fuse = new Fuse(filteredSongs, fuseOptions);
|
||||||
if (terms.length === 1) {
|
const wordResults = fuse.search(word);
|
||||||
return songTitle.includes(terms[0]) || songArtist.includes(terms[0]);
|
|
||||||
|
debugLog(` Found ${wordResults.length} matches for "${word}":`);
|
||||||
|
|
||||||
|
if (wordResults.length === 0) {
|
||||||
|
debugLog(` ❌ No matches found for "${word}"`);
|
||||||
|
} else {
|
||||||
|
wordResults.slice(0, 5).forEach((result, resultIndex) => {
|
||||||
|
const song = result.item;
|
||||||
|
const score = result.score || 1;
|
||||||
|
const similarity = Math.round((1 - score) * 100);
|
||||||
|
|
||||||
|
debugLog(` ${resultIndex + 1}. "${song.artist} - ${song.title}" (Score: ${score.toFixed(3)}, ${similarity}% match)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wordResults.length > 5) {
|
||||||
|
debugLog(` ... and ${wordResults.length - 5} more results`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If multiple terms, use AND logic (all terms must match somewhere)
|
// Add songs that match this word to our tracking map
|
||||||
return terms.every(term =>
|
wordResults.forEach(result => {
|
||||||
songTitle.includes(term) || songArtist.includes(term)
|
const song = result.item;
|
||||||
);
|
const score = result.score || 1;
|
||||||
|
|
||||||
|
if (!songsWithAllWords.has(song)) {
|
||||||
|
songsWithAllWords.set(song, new Array(searchWords.length).fill(1)); // Initialize with worst scores
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the score for this word
|
||||||
|
songsWithAllWords.get(song)![wordIndex] = score;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Only keep songs that have ALL words (no missing words)
|
||||||
|
const allResults = new Map<Song, number>(); // Song -> best score
|
||||||
|
|
||||||
|
songsWithAllWords.forEach((scores, song) => {
|
||||||
|
// Check if this song has all words (no missing words with score 1)
|
||||||
|
const hasAllWords = scores.every(score => score < 1);
|
||||||
|
|
||||||
|
if (hasAllWords) {
|
||||||
|
// Use the best (lowest) score for ranking
|
||||||
|
const bestScore = Math.min(...scores);
|
||||||
|
allResults.set(song, bestScore);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert back to array and sort by score
|
||||||
|
const fuzzyFilteredSongs = Array.from(allResults.entries())
|
||||||
|
.sort(([, scoreA], [, scoreB]) => scoreA - scoreB)
|
||||||
|
.map(([song]) => song);
|
||||||
|
|
||||||
|
debugLog('\n🎯 FINAL COMBINED RESULTS:');
|
||||||
|
debugLog(` Total unique songs found: ${fuzzyFilteredSongs.length}`);
|
||||||
|
debugLog(' Top 10 results:');
|
||||||
|
|
||||||
|
fuzzyFilteredSongs.slice(0, 10).forEach((song, index) => {
|
||||||
|
const score = allResults.get(song) || 1;
|
||||||
|
const similarity = Math.round((1 - score) * 100);
|
||||||
|
debugLog(` ${index + 1}. "${song.artist} - ${song.title}" (Best score: ${score.toFixed(3)}, ${similarity}% match)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fuzzyFilteredSongs.length > 10) {
|
||||||
|
debugLog(` ... and ${fuzzyFilteredSongs.length - 10} more results`);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('Fuzzy search results:', {
|
||||||
|
searchTerm,
|
||||||
|
searchWords,
|
||||||
|
totalSongs: filteredSongs.length,
|
||||||
|
fuzzyResults: fuzzyFilteredSongs.length,
|
||||||
|
firstFewResults: fuzzyFilteredSongs.slice(0, 3).map(s => `${s.artist} - ${s.title}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
return fuzzyFilteredSongs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// OLD IMPLEMENTATION (for easy revert):
|
||||||
|
// export const filterSongs = (songs: Song[], searchTerm: string, disabledSongPaths?: Set<string>): Song[] => {
|
||||||
|
// let filteredSongs = songs;
|
||||||
|
//
|
||||||
|
// // First filter out disabled songs if disabledSongPaths is provided
|
||||||
|
// if (disabledSongPaths) {
|
||||||
|
// filteredSongs = filterDisabledSongs(songs, disabledSongPaths);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!searchTerm.trim()) return filteredSongs;
|
||||||
|
//
|
||||||
|
// const terms = (searchTerm || '').toLowerCase().split(/\s+/).filter(term => term.length > 0);
|
||||||
|
//
|
||||||
|
// if (terms.length === 0) return filteredSongs;
|
||||||
|
//
|
||||||
|
// return filteredSongs.filter(song => {
|
||||||
|
// const songTitle = (song.title || '').toLowerCase();
|
||||||
|
// const songArtist = (song.artist || '').toLowerCase();
|
||||||
|
//
|
||||||
|
// // If only one term, use OR logic (title OR artist)
|
||||||
|
// if (terms.length === 1) {
|
||||||
|
// return songTitle.includes(terms[0]) || songArtist.includes(terms[0]);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // If multiple terms, use AND logic (all terms must match somewhere)
|
||||||
|
// return terms.every(term =>
|
||||||
|
// songTitle.includes(term) || songArtist.includes(term)
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Filter artists by search term with FUZZY MATCHING using Fuse.js
|
||||||
|
export const filterArtists = (artists: string[], searchTerm: string): string[] => {
|
||||||
|
debugLog('🎤 ARTIST SEARCH CALLED with:', { searchTerm, artistsCount: artists.length });
|
||||||
|
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
debugLog('📝 No search term, returning all artists');
|
||||||
|
return artists;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure Fuse.js for fuzzy matching artists
|
||||||
|
const fuseOptions = {
|
||||||
|
threshold: 0.2, // 80% similarity threshold
|
||||||
|
includeScore: true,
|
||||||
|
includeMatches: false,
|
||||||
|
minMatchCharLength: 2,
|
||||||
|
shouldSort: true,
|
||||||
|
findAllMatches: true,
|
||||||
|
location: 0,
|
||||||
|
distance: 100,
|
||||||
|
useExtendedSearch: false,
|
||||||
|
ignoreLocation: true,
|
||||||
|
ignoreFieldNorm: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split search term into individual words
|
||||||
|
const searchWords = searchTerm.toLowerCase().split(/\s+/).filter(word => word.length >= 2);
|
||||||
|
|
||||||
|
debugLog('🔍 ARTIST FUZZY SEARCH DEBUG:', {
|
||||||
|
originalSearchTerm: searchTerm,
|
||||||
|
searchWords: searchWords,
|
||||||
|
totalArtistsToSearch: artists.length,
|
||||||
|
firstFewArtists: artists.slice(0, 3)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchWords.length === 0) {
|
||||||
|
debugLog('❌ No search words found, returning all artists');
|
||||||
|
return artists;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for each word individually and find artists that contain ALL words
|
||||||
|
const artistsWithAllWords = new Map<string, number[]>(); // Artist -> array of scores for each word
|
||||||
|
|
||||||
|
searchWords.forEach((word, wordIndex) => {
|
||||||
|
debugLog(`\n🔤 Searching for artist word ${wordIndex + 1}: "${word}"`);
|
||||||
|
|
||||||
|
const fuse = new Fuse(artists, fuseOptions);
|
||||||
|
const wordResults = fuse.search(word);
|
||||||
|
|
||||||
|
debugLog(` Found ${wordResults.length} artist matches for "${word}":`);
|
||||||
|
|
||||||
|
if (wordResults.length === 0) {
|
||||||
|
debugLog(` ❌ No artist matches found for "${word}"`);
|
||||||
|
} else {
|
||||||
|
wordResults.slice(0, 5).forEach((result, resultIndex) => {
|
||||||
|
const artist = result.item;
|
||||||
|
const score = result.score || 1;
|
||||||
|
const similarity = Math.round((1 - score) * 100);
|
||||||
|
|
||||||
|
debugLog(` ${resultIndex + 1}. "${artist}" (Score: ${score.toFixed(3)}, ${similarity}% match)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wordResults.length > 5) {
|
||||||
|
debugLog(` ... and ${wordResults.length - 5} more results`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add artists that match this word to our tracking map
|
||||||
|
wordResults.forEach(result => {
|
||||||
|
const artist = result.item;
|
||||||
|
const score = result.score || 1;
|
||||||
|
|
||||||
|
if (!artistsWithAllWords.has(artist)) {
|
||||||
|
artistsWithAllWords.set(artist, new Array(searchWords.length).fill(1)); // Initialize with worst scores
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the score for this word
|
||||||
|
artistsWithAllWords.get(artist)![wordIndex] = score;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only keep artists that have ALL words (no missing words)
|
||||||
|
const allResults = new Map<string, number>(); // Artist -> best score
|
||||||
|
|
||||||
|
artistsWithAllWords.forEach((scores, artist) => {
|
||||||
|
// Check if this artist has all words (no missing words with score 1)
|
||||||
|
const hasAllWords = scores.every(score => score < 1);
|
||||||
|
|
||||||
|
if (hasAllWords) {
|
||||||
|
// Use the best (lowest) score for ranking
|
||||||
|
const bestScore = Math.min(...scores);
|
||||||
|
allResults.set(artist, bestScore);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert back to array and sort by score
|
||||||
|
const fuzzyFilteredArtists = Array.from(allResults.entries())
|
||||||
|
.sort(([, scoreA], [, scoreB]) => scoreA - scoreB)
|
||||||
|
.map(([artist]) => artist);
|
||||||
|
|
||||||
|
debugLog('\n🎯 ARTIST FINAL COMBINED RESULTS:');
|
||||||
|
debugLog(` Total unique artists found: ${fuzzyFilteredArtists.length}`);
|
||||||
|
debugLog(' Top 10 results:');
|
||||||
|
|
||||||
|
fuzzyFilteredArtists.slice(0, 10).forEach((artist, index) => {
|
||||||
|
const score = allResults.get(artist) || 1;
|
||||||
|
const similarity = Math.round((1 - score) * 100);
|
||||||
|
debugLog(` ${index + 1}. "${artist}" (Best score: ${score.toFixed(3)}, ${similarity}% match)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fuzzyFilteredArtists.length > 10) {
|
||||||
|
debugLog(` ... and ${fuzzyFilteredArtists.length - 10} more results`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuzzyFilteredArtists;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort queue items by order
|
// Sort queue items by order
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user