LibreChat/client/src/routes/Search.tsx
Danny Avila 57f8b333bc
🕵️ refactor: Optimize Message Search Performance (#9818)
* 🕵️ feat: Enhance Index Sync and MeiliSearch filtering for User Field

- Implemented `ensureFilterableAttributes` function to configure MeiliSearch indexes for messages and conversations to filter by user.
- Updated sync logic to trigger a full re-sync if the user field is missing or index settings are modified.
- Adjusted search queries in Conversation and Message models to include user filtering.
- Ensured 'user' field is marked as filterable in MongoDB schema for both messages and conversations.

This update improves data integrity and search capabilities by ensuring user-related data is properly indexed and retrievable.

* fix: message processing in Search component to use linear list and not tree

* feat: Implement user filtering in MeiliSearch for shared links

* refactor: Optimize message search retrieval by batching database calls

* chore: Update MeiliSearch parameters type to use SearchParams for improved type safety
2025-09-24 16:27:34 -04:00

104 lines
3.2 KiB
TypeScript

import { useEffect, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { Spinner, useToastContext } from '@librechat/client';
import MinimalMessagesWrapper from '~/components/Chat/Messages/MinimalMessages';
import { useNavScrolling, useLocalize, useAuthContext } from '~/hooks';
import SearchMessage from '~/components/Chat/Messages/SearchMessage';
import { useMessagesInfiniteQuery } from '~/data-provider';
import { useFileMapContext } from '~/Providers';
import store from '~/store';
export default function Search() {
const localize = useLocalize();
const fileMap = useFileMapContext();
const { showToast } = useToastContext();
const { isAuthenticated } = useAuthContext();
const search = useRecoilValue(store.search);
const searchQuery = search.debouncedQuery;
const {
data: searchMessages,
isLoading,
isError,
fetchNextPage,
isFetchingNextPage,
hasNextPage: _hasNextPage,
} = useMessagesInfiniteQuery(
{
search: searchQuery || undefined,
},
{
enabled: isAuthenticated && !!searchQuery,
staleTime: 30000,
cacheTime: 300000,
},
);
const { containerRef } = useNavScrolling({
nextCursor: searchMessages?.pages[searchMessages.pages.length - 1]?.nextCursor,
setShowLoading: () => ({}),
fetchNextPage: fetchNextPage,
isFetchingNext: isFetchingNextPage,
});
const messages = useMemo(() => {
const msgs =
searchMessages?.pages.flatMap((page) =>
page.messages.map((message) => {
if (!message.files || !fileMap) {
return message;
}
return {
...message,
files: message.files.map((file) => fileMap[file.file_id ?? ''] ?? file),
};
}),
) || [];
return msgs.length === 0 ? null : msgs;
}, [fileMap, searchMessages?.pages]);
useEffect(() => {
if (isError && searchQuery) {
showToast({ message: 'An error occurred during search', status: 'error' });
}
}, [isError, searchQuery, showToast]);
const isSearchLoading = search.isTyping || isLoading || isFetchingNextPage;
if (isSearchLoading) {
return (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner className="text-text-primary" />
</div>
);
}
if (!searchQuery) {
return null;
}
return (
<MinimalMessagesWrapper ref={containerRef} className="relative flex h-full pt-4">
{(messages && messages.length === 0) || messages == null ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-lg bg-white p-6 text-lg text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
{localize('com_ui_nothing_found')}
</div>
</div>
) : (
<>
{messages.map((msg) => (
<SearchMessage key={msg.messageId} message={msg} />
))}
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<Spinner className="text-text-primary" />
</div>
)}
</>
)}
<div className="absolute bottom-0 left-0 right-0 h-[5%] bg-gradient-to-t from-gray-50 to-transparent dark:from-gray-800" />
</MinimalMessagesWrapper>
);
}