From 2a8dfd69947f553bc1cab279124a9f1dce06f8b3 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:21:27 +0100 Subject: [PATCH] perf: opening a chat no longer loads the whole conversation to mark it read Every time a chat is opened, the server marks it as read, and to do so it loaded and parsed the full chat, images included, just to save the last-read time. Marking a chat as unread and removing a tag from a chat did the same. These now touch only the few chat fields they change, which makes them 4 to 6 times faster on large chats. Measured on SQLite, best of 7 runs, sizes are the chat as stored in the database: | Chat | Action | Before | After | | --- | --- | --- | --- | | 7 MB, text only | open (mark read) | 19.8 ms | 3.5 ms | | 7 MB, text only | mark as unread | 20.7 ms | 3.5 ms | | 7 MB, text only | remove tag | 21.2 ms | 3.4 ms | | 49 MB, with images | open (mark read) | 122.2 ms | 30.0 ms | | 49 MB, with images | mark as unread | 124.2 ms | 29.1 ms | | 49 MB, with images | remove tag | 120.5 ms | 28.6 ms | --- backend/open_webui/models/chats.py | 59 +++++++++++++++++------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 57f44331e3..9a7d186a84 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -942,14 +942,17 @@ class ChatTable: ) -> tuple[int, bool] | None: try: async with get_async_db_context(db) as session: - chat = await session.get(Chat, id) - if chat and chat.user_id == user_id: - last_read_at = int(time.time()) - was_unread = chat.last_read_at is None or chat.updated_at > chat.last_read_at - chat.last_read_at = last_read_at - await session.commit() - return last_read_at, was_unread - return None + row = ( + await session.execute(select(Chat.updated_at, Chat.last_read_at).filter_by(id=id, user_id=user_id)) + ).one_or_none() + if row is None: + return None + + last_read_at = int(time.time()) + was_unread = row.last_read_at is None or row.updated_at > row.last_read_at + await session.execute(update(Chat).filter_by(id=id).values(last_read_at=last_read_at)) + await session.commit() + return last_read_at, was_unread except Exception: return None @@ -958,18 +961,23 @@ class ChatTable: ) -> ChatTitleIdResponse | None: try: async with get_async_db_context(db) as session: - chat = await session.get(Chat, id) - if chat and chat.user_id == user_id: - chat.last_read_at = 0 - await session.commit() - return ChatTitleIdResponse( - id=chat.id, - title=chat.title, - updated_at=chat.updated_at, - created_at=chat.created_at, - last_read_at=chat.last_read_at, + row = ( + await session.execute( + select(Chat.title, Chat.updated_at, Chat.created_at).filter_by(id=id, user_id=user_id) ) - return None + ).one_or_none() + if row is None: + return None + + await session.execute(update(Chat).filter_by(id=id).values(last_read_at=0)) + await session.commit() + return ChatTitleIdResponse( + id=id, + title=row.title, + updated_at=row.updated_at, + created_at=row.created_at, + last_read_at=0, + ) except Exception: return None @@ -2796,15 +2804,16 @@ class ChatTable: ) -> bool: try: async with get_async_db_context(db) as session: - chat = await session.get(Chat, id) - tags = chat.meta.get('tags', []) + row = (await session.execute(select(Chat.meta).filter_by(id=id))).one_or_none() + if row is None: + return False + + meta = row[0] + tags = meta.get('tags', []) tag_id = tag_name.replace(' ', '_').lower() tags = [tag for tag in tags if tag != tag_id] - chat.meta = { - **chat.meta, - 'tags': list(set(tags)), - } + await session.execute(update(Chat).filter_by(id=id).values(meta={**meta, 'tags': list(set(tags))})) await session.commit() return True except Exception: