mirror of
https://github.com/open-webui/open-webui.git
synced 2026-02-24 04:00:31 +01:00
perf: eliminate 2 redundant full chat deserialization on every message send (#21596)
* perf: eliminate 2 redundant full chat deserialization on every message send (#162) Problem: Every message send triggered get_chat_by_id_and_user_id which loads the entire Chat row — including the potentially massive JSON blob containing the full conversation history — even when the caller only needed a simple yes/no ownership check or a single column value. Two call sites in the message-send hot path were doing this: 1. main.py ownership verification: loaded the entire chat object including all message history JSON, then checked `if chat is None`. The JSON blob was immediately discarded — only the existence of the row mattered. 2. middleware.py folder check: loaded the entire chat object including all message history JSON, then read only `chat.folder_id` — a plain column on the chat table that requires zero JSON parsing. Fix: - Added `chat_exists_by_id_and_user_id()`: uses SQL EXISTS subquery which returns a boolean without loading any row data. The database can satisfy this from the primary key index alone. - Added `get_chat_folder_id()`: queries only the `folder_id` column via `db.query(Chat.folder_id)`, which tells SQLAlchemy to SELECT only that single column instead of the entire row. Both new methods preserve the same error handling semantics (return False/None on exception) and user_id filtering (ownership check) as the original get_chat_by_id_and_user_id. Impact: - Best case (typical): eliminates deserializing 2 full chat JSON blobs per message send. For long conversations (hundreds of messages with tool calls, images, file attachments), this blob can be multiple megabytes. - Worst case: no regression — the new queries are strictly cheaper than the old ones (less data transferred, less Python object construction, no Pydantic model_validate overhead). - The 3 remaining full chat loads in process_chat_payload (load_messages_from_db, add_file_context, chat_image_generation_handler) are left untouched as they genuinely need the full history and require separate analysis. * Address maintainer feedback: rename method and inline call (#166) - Rename chat_exists_by_id_and_user_id -> is_chat_owner - Remove intermediate chat_owned variable; call is_chat_owner directly in if condition
This commit is contained in:
@@ -1760,9 +1760,9 @@ async def chat_completion(
|
||||
"local:"
|
||||
): # temporary chats are not stored
|
||||
|
||||
# Verify chat ownership
|
||||
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
|
||||
if chat is None and user.role != "admin": # admins can access any chat
|
||||
# Verify chat ownership — lightweight EXISTS check avoids
|
||||
# deserializing the full chat JSON blob just to confirm the row exists
|
||||
if not Chats.is_chat_owner(metadata["chat_id"], user.id) and user.role != "admin": # admins can access any chat
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
|
||||
@@ -974,6 +974,41 @@ class ChatTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def is_chat_owner(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Lightweight ownership check — uses EXISTS subquery instead of loading
|
||||
the full Chat row (which includes the potentially large JSON blob).
|
||||
"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
return db.query(
|
||||
exists().where(
|
||||
and_(Chat.id == id, Chat.user_id == user_id)
|
||||
)
|
||||
).scalar()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_chat_folder_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Fetch only the folder_id column for a chat, without loading the full
|
||||
JSON blob. Returns None if chat doesn't exist or doesn't belong to user.
|
||||
"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
result = (
|
||||
db.query(Chat.folder_id)
|
||||
.filter_by(id=id, user_id=user_id)
|
||||
.first()
|
||||
)
|
||||
return result[0] if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chats(
|
||||
self, skip: int = 0, limit: int = 50, db: Optional[Session] = None
|
||||
) -> list[ChatModel]:
|
||||
|
||||
@@ -2078,11 +2078,12 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
|
||||
# Folder "Project" handling
|
||||
# Check if the request has chat_id and is inside of a folder
|
||||
# Uses lightweight column query — only fetches folder_id, not the full chat JSON blob
|
||||
chat_id = metadata.get("chat_id", None)
|
||||
if chat_id and user:
|
||||
chat = Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
||||
if chat and chat.folder_id:
|
||||
folder = Folders.get_folder_by_id_and_user_id(chat.folder_id, user.id)
|
||||
folder_id = Chats.get_chat_folder_id(chat_id, user.id)
|
||||
if folder_id:
|
||||
folder = Folders.get_folder_by_id_and_user_id(folder_id, user.id)
|
||||
|
||||
if folder and folder.data:
|
||||
if "system_prompt" in folder.data:
|
||||
|
||||
Reference in New Issue
Block a user