mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 02:40:14 +01:00
🕵️ 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
This commit is contained in:
parent
f9aebeba92
commit
57f8b333bc
9 changed files with 263 additions and 31 deletions
|
|
@ -427,13 +427,14 @@ describe('Share Methods', () => {
|
|||
expect(privateResults.links[0].title).toBe('Private Share');
|
||||
});
|
||||
|
||||
test('should handle search with mocked meiliSearch', async () => {
|
||||
test('should handle search with mocked meiliSearch and user filter', async () => {
|
||||
const userId = new mongoose.Types.ObjectId().toString();
|
||||
|
||||
// Mock meiliSearch method
|
||||
Conversation.meiliSearch = jest.fn().mockResolvedValue({
|
||||
const meiliSearchMock = jest.fn().mockResolvedValue({
|
||||
hits: [{ conversationId: 'conv1' }],
|
||||
});
|
||||
Conversation.meiliSearch = meiliSearchMock;
|
||||
|
||||
await SharedLink.create([
|
||||
{
|
||||
|
|
@ -464,6 +465,9 @@ describe('Share Methods', () => {
|
|||
|
||||
expect(result.links).toHaveLength(1);
|
||||
expect(result.links[0].title).toBe('Matching Share');
|
||||
|
||||
// Verify that meiliSearch was called with the correct user filter
|
||||
expect(meiliSearchMock).toHaveBeenCalledWith('search term', { filter: `user = "${userId}"` });
|
||||
});
|
||||
|
||||
test('should handle empty results', async () => {
|
||||
|
|
@ -475,6 +479,98 @@ describe('Share Methods', () => {
|
|||
expect(result.nextCursor).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should only return shares from search results for the current user', async () => {
|
||||
const userId1 = new mongoose.Types.ObjectId().toString();
|
||||
const userId2 = new mongoose.Types.ObjectId().toString();
|
||||
|
||||
// Mock meiliSearch to simulate finding conversations from both users
|
||||
const meiliSearchMock = jest.fn().mockImplementation((searchTerm, params) => {
|
||||
// Simulate MeiliSearch filtering by user
|
||||
const filter = params?.filter;
|
||||
if (filter && filter.includes(userId1)) {
|
||||
return Promise.resolve({
|
||||
hits: [{ conversationId: 'conv1' }, { conversationId: 'conv3' }],
|
||||
});
|
||||
} else if (filter && filter.includes(userId2)) {
|
||||
return Promise.resolve({ hits: [{ conversationId: 'conv2' }] });
|
||||
}
|
||||
// Without filter, would return all conversations (security issue)
|
||||
return Promise.resolve({
|
||||
hits: [
|
||||
{ conversationId: 'conv1' },
|
||||
{ conversationId: 'conv2' },
|
||||
{ conversationId: 'conv3' },
|
||||
],
|
||||
});
|
||||
});
|
||||
Conversation.meiliSearch = meiliSearchMock;
|
||||
|
||||
// Create shares for different users
|
||||
await SharedLink.create([
|
||||
{
|
||||
shareId: 'share1',
|
||||
conversationId: 'conv1',
|
||||
user: userId1,
|
||||
title: 'User 1 Share',
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
shareId: 'share2',
|
||||
conversationId: 'conv2',
|
||||
user: userId2,
|
||||
title: 'User 2 Share',
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
shareId: 'share3',
|
||||
conversationId: 'conv3',
|
||||
user: userId1,
|
||||
title: 'Another User 1 Share',
|
||||
isPublic: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// Search as userId1
|
||||
const result1 = await shareMethods.getSharedLinks(
|
||||
userId1,
|
||||
undefined,
|
||||
10,
|
||||
true,
|
||||
'createdAt',
|
||||
'desc',
|
||||
'search term',
|
||||
);
|
||||
|
||||
// Should only get shares from conversations belonging to userId1
|
||||
expect(result1.links).toHaveLength(2);
|
||||
expect(result1.links.every((link) => link.title.includes('User 1'))).toBe(true);
|
||||
|
||||
// Verify correct filter was used
|
||||
expect(meiliSearchMock).toHaveBeenCalledWith('search term', {
|
||||
filter: `user = "${userId1}"`,
|
||||
});
|
||||
|
||||
// Search as userId2
|
||||
const result2 = await shareMethods.getSharedLinks(
|
||||
userId2,
|
||||
undefined,
|
||||
10,
|
||||
true,
|
||||
'createdAt',
|
||||
'desc',
|
||||
'search term',
|
||||
);
|
||||
|
||||
// Should only get shares from conversations belonging to userId2
|
||||
expect(result2.links).toHaveLength(1);
|
||||
expect(result2.links[0].title).toBe('User 2 Share');
|
||||
|
||||
// Verify correct filter was used for second user
|
||||
expect(meiliSearchMock).toHaveBeenCalledWith('search term', {
|
||||
filter: `user = "${userId2}"`,
|
||||
});
|
||||
});
|
||||
|
||||
test('should only return shares for the specified user', async () => {
|
||||
const userId1 = new mongoose.Types.ObjectId().toString();
|
||||
const userId2 = new mongoose.Types.ObjectId().toString();
|
||||
|
|
|
|||
|
|
@ -150,7 +150,9 @@ export function createShareMethods(mongoose: typeof import('mongoose')) {
|
|||
|
||||
if (search && search.trim()) {
|
||||
try {
|
||||
const searchResults = await Conversation.meiliSearch(search);
|
||||
const searchResults = await Conversation.meiliSearch(search, {
|
||||
filter: `user = "${user}"`,
|
||||
});
|
||||
|
||||
if (!searchResults?.hits?.length) {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue