Query history speed fix (#109)

This commit is contained in:
rashad-danswer 2024-06-20 18:35:28 -07:00 committed by Chris Weaver
parent 733d4e666b
commit 98a58337a7
5 changed files with 148 additions and 39 deletions

View File

@ -3,30 +3,48 @@ from typing import Literal
from sqlalchemy import asc from sqlalchemy import asc
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy.orm import contains_eager
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from danswer.db.models import ChatMessage
from danswer.db.models import ChatSession from danswer.db.models import ChatSession
SortByOptions = Literal["time_sent"] SortByOptions = Literal["time_sent"]
def fetch_chat_sessions_by_time( def fetch_chat_sessions_eagerly_by_time(
start: datetime.datetime, start: datetime.datetime,
end: datetime.datetime, end: datetime.datetime,
db_session: Session, db_session: Session,
ascending: bool = False, ascending: bool = False,
limit: int | None = 500, limit: int | None = 500,
) -> list[ChatSession]: ) -> list[ChatSession]:
order = asc(ChatSession.time_created) if ascending else desc(ChatSession.time_created) # type: ignore time_order = asc(ChatSession.time_created) if ascending else desc(ChatSession.time_created) # type: ignore
message_order = asc(ChatMessage.id) # type: ignore
subquery = (
db_session.query(ChatSession.id, ChatSession.time_created)
.filter(ChatSession.time_created.between(start, end))
.order_by(desc(ChatSession.id), time_order)
.distinct(ChatSession.id)
.limit(limit)
.subquery()
)
query = ( query = (
db_session.query(ChatSession) db_session.query(ChatSession)
.filter(ChatSession.time_created >= start, ChatSession.time_created <= end) .join(subquery, ChatSession.id == subquery.c.id)
.order_by(order) .outerjoin(ChatMessage, ChatSession.id == ChatMessage.chat_session_id)
.options(
joinedload(ChatSession.user),
joinedload(ChatSession.persona),
contains_eager(ChatSession.messages).joinedload(
ChatMessage.chat_message_feedbacks
),
)
.order_by(time_order, message_order)
) )
if limit is not None:
query = query.limit(limit)
chat_sessions = query.all() chat_sessions = query.all()

View File

@ -3,6 +3,7 @@ import io
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from datetime import timezone from datetime import timezone
from typing import Literal
from fastapi import APIRouter from fastapi import APIRouter
from fastapi import Depends from fastapi import Depends
@ -21,7 +22,7 @@ from danswer.db.chat import get_chat_session_by_id
from danswer.db.engine import get_session from danswer.db.engine import get_session
from danswer.db.models import ChatMessage from danswer.db.models import ChatMessage
from danswer.db.models import ChatSession from danswer.db.models import ChatSession
from ee.danswer.db.query_history import fetch_chat_sessions_by_time from ee.danswer.db.query_history import fetch_chat_sessions_eagerly_by_time
router = APIRouter() router = APIRouter()
@ -81,6 +82,17 @@ class MessageSnapshot(BaseModel):
) )
class ChatSessionMinimal(BaseModel):
id: int
user_email: str
name: str | None
first_user_message: str
first_ai_message: str
persona_name: str
time_created: datetime
feedback_type: QAFeedbackType | Literal["mixed"] | None
class ChatSessionSnapshot(BaseModel): class ChatSessionSnapshot(BaseModel):
id: int id: int
user_email: str user_email: str
@ -146,6 +158,85 @@ class QuestionAnswerPairSnapshot(BaseModel):
} }
def fetch_and_process_chat_session_history_minimal(
db_session: Session,
start: datetime,
end: datetime,
feedback_filter: QAFeedbackType | None = None,
limit: int | None = 500,
) -> list[ChatSessionMinimal]:
chat_sessions = fetch_chat_sessions_eagerly_by_time(
start=start, end=end, db_session=db_session, limit=limit
)
minimal_sessions = []
for chat_session in chat_sessions:
if not chat_session.messages:
continue
first_user_message = next(
(
message.message
for message in chat_session.messages
if message.message_type == MessageType.USER
),
"",
)
first_ai_message = next(
(
message.message
for message in chat_session.messages
if message.message_type == MessageType.ASSISTANT
),
"",
)
has_positive_feedback = any(
feedback.is_positive
for message in chat_session.messages
for feedback in message.chat_message_feedbacks
)
has_negative_feedback = any(
not feedback.is_positive
for message in chat_session.messages
for feedback in message.chat_message_feedbacks
)
feedback_type: QAFeedbackType | Literal["mixed"] | None = (
"mixed"
if has_positive_feedback and has_negative_feedback
else QAFeedbackType.LIKE
if has_positive_feedback
else QAFeedbackType.DISLIKE
if has_negative_feedback
else None
)
if feedback_filter:
if feedback_filter == QAFeedbackType.LIKE and not has_positive_feedback:
continue
if feedback_filter == QAFeedbackType.DISLIKE and not has_negative_feedback:
continue
minimal_sessions.append(
ChatSessionMinimal(
id=chat_session.id,
user_email=get_display_email(
chat_session.user.email if chat_session.user else None
),
name=chat_session.description,
first_user_message=first_user_message,
first_ai_message=first_ai_message,
persona_name=chat_session.persona.name,
time_created=chat_session.time_created,
feedback_type=feedback_type,
)
)
return minimal_sessions
def fetch_and_process_chat_session_history( def fetch_and_process_chat_session_history(
db_session: Session, db_session: Session,
start: datetime, start: datetime,
@ -153,7 +244,7 @@ def fetch_and_process_chat_session_history(
feedback_type: QAFeedbackType | None, feedback_type: QAFeedbackType | None,
limit: int | None = 500, limit: int | None = 500,
) -> list[ChatSessionSnapshot]: ) -> list[ChatSessionSnapshot]:
chat_sessions = fetch_chat_sessions_by_time( chat_sessions = fetch_chat_sessions_eagerly_by_time(
start=start, end=end, db_session=db_session, limit=limit start=start, end=end, db_session=db_session, limit=limit
) )
@ -214,15 +305,15 @@ def get_chat_session_history(
end: datetime | None = None, end: datetime | None = None,
_: db_models.User | None = Depends(current_admin_user), _: db_models.User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
) -> list[ChatSessionSnapshot]: ) -> list[ChatSessionMinimal]:
return fetch_and_process_chat_session_history( return fetch_and_process_chat_session_history_minimal(
db_session=db_session, db_session=db_session,
start=start start=start
or ( or (
datetime.now(tz=timezone.utc) - timedelta(days=30) datetime.now(tz=timezone.utc) - timedelta(days=30)
), # default is 30d lookback ), # default is 30d lookback
end=end or datetime.now(tz=timezone.utc), end=end or datetime.now(tz=timezone.utc),
feedback_type=feedback_type, feedback_filter=feedback_type,
) )

View File

@ -1,7 +1,7 @@
import { errorHandlingFetcher } from "@/lib/fetcher"; import { errorHandlingFetcher } from "@/lib/fetcher";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { import {
ChatSessionSnapshot, ChatSessionMinimal,
DanswerBotAnalytics, DanswerBotAnalytics,
QueryAnalytics, QueryAnalytics,
UserAnalytics, UserAnalytics,
@ -74,7 +74,7 @@ export const useQueryHistory = () => {
start: convertDateToStartOfDay(timeRange.from)?.toISOString(), start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange.to)?.toISOString(), end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
}); });
const swrResponse = useSWR<ChatSessionSnapshot[]>(url, errorHandlingFetcher); const swrResponse = useSWR<ChatSessionMinimal[]>(url, errorHandlingFetcher);
return { return {
...swrResponse, ...swrResponse,

View File

@ -13,7 +13,7 @@ import {
import { Divider } from "@tremor/react"; import { Divider } from "@tremor/react";
import { Select, SelectItem } from "@tremor/react"; import { Select, SelectItem } from "@tremor/react";
import { ThreeDotsLoader } from "@/components/Loading"; import { ThreeDotsLoader } from "@/components/Loading";
import { ChatSessionSnapshot } from "../usage/types"; import { ChatSessionMinimal } from "../usage/types";
import { timestampToReadableDate } from "@/lib/dateUtils"; import { timestampToReadableDate } from "@/lib/dateUtils";
import { FiFrown, FiMinus, FiSmile } from "react-icons/fi"; import { FiFrown, FiMinus, FiSmile } from "react-icons/fi";
import { useState } from "react"; import { useState } from "react";
@ -27,48 +27,37 @@ import { DownloadAsCSV } from "./DownloadAsCSV";
const NUM_IN_PAGE = 20; const NUM_IN_PAGE = 20;
function QueryHistoryTableRow({ function QueryHistoryTableRow({
chatSessionSnapshot, chatSessionMinimal,
}: { }: {
chatSessionSnapshot: ChatSessionSnapshot; chatSessionMinimal: ChatSessionMinimal;
}) { }) {
let finalFeedback: Feedback | "mixed" | null = null;
for (const message of chatSessionSnapshot.messages) {
if (message.feedback_type) {
if (finalFeedback === null) {
finalFeedback = message.feedback_type;
} else if (finalFeedback !== message.feedback_type) {
finalFeedback = "mixed";
}
}
}
return ( return (
<TableRow <TableRow
key={chatSessionSnapshot.id} key={chatSessionMinimal.id}
className="hover:bg-hover-light cursor-pointer relative" className="hover:bg-hover-light cursor-pointer relative"
> >
<TableCell> <TableCell>
<Text className="whitespace-normal line-clamp-5"> <Text className="whitespace-normal line-clamp-5">
{chatSessionSnapshot.messages[0]?.message || "-"} {chatSessionMinimal.first_user_message || "-"}
</Text> </Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text className="whitespace-normal line-clamp-5"> <Text className="whitespace-normal line-clamp-5">
{chatSessionSnapshot.messages[1]?.message || "-"} {chatSessionMinimal.first_ai_message || "-"}
</Text> </Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<FeedbackBadge feedback={finalFeedback} /> <FeedbackBadge feedback={chatSessionMinimal.feedback_type} />
</TableCell> </TableCell>
<TableCell>{chatSessionSnapshot.user_email || "-"}</TableCell> <TableCell>{chatSessionMinimal.user_email || "-"}</TableCell>
<TableCell>{chatSessionSnapshot.persona_name || "Unknown"}</TableCell> <TableCell>{chatSessionMinimal.persona_name || "Unknown"}</TableCell>
<TableCell> <TableCell>
{timestampToReadableDate(chatSessionSnapshot.time_created)} {timestampToReadableDate(chatSessionMinimal.time_created)}
</TableCell> </TableCell>
{/* Wrapping in <td> to avoid console warnings */} {/* Wrapping in <td> to avoid console warnings */}
<td className="w-0 p-0"> <td className="w-0 p-0">
<Link <Link
href={`/admin/performance/query-history/${chatSessionSnapshot.id}`} href={`/admin/performance/query-history/${chatSessionMinimal.id}`}
className="absolute w-full h-full left-0" className="absolute w-full h-full left-0"
></Link> ></Link>
</td> </td>
@ -152,10 +141,10 @@ export function QueryHistoryTable() {
<TableBody> <TableBody>
{chatSessionData {chatSessionData
.slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page) .slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page)
.map((chatSessionSnapshot) => ( .map((chatSessionMinimal) => (
<QueryHistoryTableRow <QueryHistoryTableRow
key={chatSessionSnapshot.id} key={chatSessionMinimal.id}
chatSessionSnapshot={chatSessionSnapshot} chatSessionMinimal={chatSessionMinimal}
/> />
))} ))}
</TableBody> </TableBody>

View File

@ -41,3 +41,14 @@ export interface ChatSessionSnapshot {
persona_name: string | null; persona_name: string | null;
time_created: string; time_created: string;
} }
export interface ChatSessionMinimal {
id: number;
user_email: string | null;
name: string | null;
first_user_message: string;
first_ai_message: string;
persona_name: string | null;
time_created: string;
feedback_type: Feedback | "mixed" | null;
}