Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion locales/en-US/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,16 @@
},
"title": "Online Search"
},
"searchAgentPlaceholder": "Search assistants...",
"searchAgentPlaceholder": "Search assistants/chat logs...",
"searchMessages": {
"count": "{{count}} results",
"empty": "No chat logs found",
"loadMore": "Load more chat logs",
"title": "Chat logs"
},
"searchSessions": {
"title": "Assistants"
},
"sendPlaceholder": "Type your message here...",
"sessionGroup": {
"config": "Group Management",
Expand Down
11 changes: 10 additions & 1 deletion locales/zh-CN/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,16 @@
},
"title": "联网搜索"
},
"searchAgentPlaceholder": "搜索助手...",
"searchAgentPlaceholder": "搜索助手/聊天记录...",
"searchMessages": {
"count": "共找到 {{count}} 条记录",
"empty": "未找到相关聊天记录",
"loadMore": "加载更多聊天记录",
"title": "聊天记录"
},
"searchSessions": {
"title": "助手"
},
"sendPlaceholder": "输入聊天内容...",
"sessionGroup": {
"config": "分组管理",
Expand Down
44 changes: 25 additions & 19 deletions packages/database/src/models/__tests__/message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,38 +561,44 @@ describe('MessageModel', () => {
});

describe('queryByKeyWord', () => {
it('should query messages by keyword', async () => {
// 创建测试数据
beforeEach(async () => {
await serverDB.insert(messages).values([
{ id: '1', userId, role: 'user', content: 'apple', createdAt: new Date('2022-02-01') },
{ id: '2', userId, role: 'user', content: 'banana' },
{ id: '3', userId, role: 'user', content: 'pear' },
{ id: '4', userId, role: 'user', content: 'apple pie', createdAt: new Date('2024-02-01') },
]);
});

// 测试查询包含特定关键字的消息
it('should query messages by keyword with default pagination', async () => {
const result = await messageModel.queryByKeyword('apple');

// 断言结果
expect(result).toHaveLength(2);
expect(result[0].id).toBe('4');
expect(result[1].id).toBe('1');
expect(result.data).toHaveLength(2);
expect(result.data[0].id).toBe('4');
expect(result.data[1].id).toBe('1');
expect(result.pagination.current).toBe(0);
expect(result.pagination.pageSize).toBe(20);
expect(result.pagination.total).toBe(2);
});

it('should return empty array when keyword is empty', async () => {
// 创建测试数据
await serverDB.insert(messages).values([
{ id: '1', userId, role: 'user', content: 'apple' },
{ id: '2', userId, role: 'user', content: 'banana' },
{ id: '3', userId, role: 'user', content: 'pear' },
{ id: '4', userId, role: 'user', content: 'apple pie' },
]);
it('should support pagination options', async () => {
const firstPage = await messageModel.queryByKeyword('apple', { current: 0, pageSize: 1 });
const secondPage = await messageModel.queryByKeyword('apple', { current: 1, pageSize: 1 });

// 测试当关键字为空时返回空数组
const result = await messageModel.queryByKeyword('');
expect(firstPage.data).toHaveLength(1);
expect(firstPage.data[0].id).toBe('4');
expect(firstPage.pagination).toMatchObject({ current: 0, pageSize: 1, total: 2 });

// 断言结果
expect(result).toHaveLength(0);
expect(secondPage.data).toHaveLength(1);
expect(secondPage.data[0].id).toBe('1');
expect(secondPage.pagination).toMatchObject({ current: 1, pageSize: 1, total: 2 });
});

it('should return empty result when keyword is empty after trim', async () => {
const result = await messageModel.queryByKeyword(' ');

expect(result.data).toHaveLength(0);
expect(result.pagination.total).toBe(0);
});
});

Expand Down
57 changes: 49 additions & 8 deletions packages/database/src/models/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
ChatVideoItem,
CreateMessageParams,
MessageItem,
MessageKeywordSearchResult,
ModelRankItem,
NewMessageQueryParams,
UpdateMessageParams,
UpdateMessageRAGParams,
} from '@lobechat/types';
import type { HeatmapsProps } from '@lobehub/charts';
import dayjs from 'dayjs';
import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, like, sql } from 'drizzle-orm';
import { and, asc, count, desc, eq, gt, ilike, inArray, isNotNull, isNull, sql } from 'drizzle-orm';

import { merge } from '@/utils/merge';
import { today } from '@/utils/time';
Expand Down Expand Up @@ -307,14 +308,54 @@ export class MessageModel {
return result as MessageItem[];
};

queryByKeyword = async (keyword: string) => {
if (!keyword) return [];
const result = await this.db.query.messages.findMany({
orderBy: [desc(messages.createdAt)],
where: and(eq(messages.userId, this.userId), like(messages.content, `%${keyword}%`)),
});
queryByKeyword = async (
keyword: string,
{ current = 0, pageSize = 20 }: { current?: number; pageSize?: number } = {},
): Promise<MessageKeywordSearchResult> => {
const sanitizedKeyword = keyword.trim();
const safePageSize = pageSize > 0 ? pageSize : 20;
const safeCurrent = current >= 0 ? current : 0;

if (!sanitizedKeyword) {
return {
data: [],
pagination: {
current: safeCurrent,
pageSize: safePageSize,
total: 0,
},
};
}

return result as MessageItem[];
const offset = safeCurrent * safePageSize;
const whereClause = and(
eq(messages.userId, this.userId),
ilike(messages.content, `%${sanitizedKeyword}%`),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Using ilike for keyword search may have performance implications on large datasets.

Consider full-text search indexes or other optimized strategies if query speed is a concern.

Suggested implementation:

    // Use full-text search for better performance on large datasets
    const whereClause = and(
      eq(messages.userId, this.userId),
      sql`to_tsvector('english', ${messages.content}) @@ plainto_tsquery('english', ${sanitizedKeyword})`,
    );
  • Ensure that your messages.content column is indexed with a full-text search index in your database (e.g., a GIN index in PostgreSQL).
  • If you are using a query builder or ORM, you may need to adapt the raw SQL to its full-text search API.
  • You may want to parameterize the language ('english') or make it configurable.

);

const [data, totalResult] = await Promise.all([
this.db.query.messages.findMany({
limit: safePageSize,
offset,
orderBy: [desc(messages.createdAt)],
where: whereClause,
}),
this.db
.select({ value: count(messages.id) })
.from(messages)
.where(whereClause),
]);

const total = Number(totalResult[0]?.value ?? 0);

return {
data: data as MessageItem[],
pagination: {
current: safeCurrent,
pageSize: safePageSize,
total,
},
};
};

count = async (params?: {
Expand Down
11 changes: 11 additions & 0 deletions packages/types/src/message/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ export interface NewMessage {
userId: string; // optional because it's generated
}

export interface MessageKeywordSearchPagination {
current: number;
pageSize: number;
total: number;
}

export interface MessageKeywordSearchResult {
data: MessageItem[];
pagination: MessageKeywordSearchPagination;
}

export interface UpdateMessageParams {
content?: string;
error?: ChatMessageError | null;
Expand Down
Loading