fix(conversations): eliminate N+1 photo queries from list endpoints#5271
fix(conversations): eliminate N+1 photo queries from list endpoints#5271
Conversation
Greptile SummaryThis PR introduces Key changes:
Implementation quality:
Known tradeoff: Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant API as List Endpoint
participant DB as conversations.py
participant FS as Firestore
Note over Client,FS: Before (N+1 queries for limit=3)
Client->>API: GET /v1/conversations?limit=3
API->>DB: get_conversations()
DB->>FS: Query conversations collection
FS-->>DB: 3 conversations (encrypted)
Note over DB: @prepare_for_read decorator<br/>deepcopy + decrypt + decompress<br/>for each conversation
Note over DB: @with_photos decorator<br/>fetches photos serially
DB->>FS: Query photos subcollection (conv 1)
FS-->>DB: photos for conv 1
DB->>FS: Query photos subcollection (conv 2)
FS-->>DB: photos for conv 2
DB->>FS: Query photos subcollection (conv 3)
FS-->>DB: photos for conv 3
DB-->>API: 3 conversations with full data
API-->>Client: Full conversations (4 Firestore queries)
Note over Client,FS: After (single query)
Client->>API: GET /v1/conversations?limit=3
API->>DB: get_conversations_lite()
DB->>FS: Query conversations collection
FS-->>DB: 3 conversations (raw)
Note over DB: No decorators<br/>Set transcript_segments=[]<br/>Set photos=[]
DB-->>API: 3 conversations (list data only)
API-->>Client: List view data (1 Firestore query)
Last reviewed commit: 1cdf95e |
PR ready for reviewAll checkpoints complete (CP0–CP8):
Impact
Awaiting dev deploy verification and external review. by AI for @beastoin |
5d304de to
e696018
Compare
…hen transcript not requested
…overy capture_provider.dart calls GET /v1/conversations?limit=1&statuses=in_progress and needs transcript_segments for the capture UI. Only apply the lite optimization (skip N+1 photo queries and transcript decryption) for multi-item list fetches (limit > 1).
e696018 to
947924e
Compare
Summary
get_conversations_lite()— a lightweight DB function that skips the@with_photosdecorator (eliminates N serial Firestore subcollection queries) and@prepare_for_readdecorator (eliminates deepcopy + decrypt/decompress of transcript data)GET /v1/conversations,GET /v1/mcp/conversations, MCP SSElist_conversations, andGET /v1/dev/user/conversations(wheninclude_transcript=False)transcript_segments=[]andphotos=[]in list responses — full data available via detail endpointGET /v1/conversations/{id}limit=1calls (in-progress conversation recovery incapture_provider.dart:1388needstranscriptSegments)Root cause investigation (prod evidence)
Verified via
gcloud logging readon Cloud Runbackendservice:GET /v1/conversationsin last 7 dayslimit=100 offset=0)offset=0— the N+1 photo queries are the dominant bottleneck, not paginationBefore (limit=100): 1 main Firestore query + 100 serial photo subcollection queries + 100x deepcopy + decrypt/decompress = 101 Firestore round-trips
After (limit=100): 1 main Firestore query only = 1 Firestore round-trip
Code-level trace confirmed:
helpers.py:240—@with_photositerates list, callsget_conversation_photos()per item (serial)conversations.py:99—_prepare_conversation_for_read()doescopy.deepcopy()on every conversationconversation_list_item.dart) does NOT display transcript_segmentsReview cycle fixes
capture_provider.dart:1388callsGET /v1/conversations?limit=1&statuses=in_progressand accessestranscriptSegmentsdirectly. Fixed by usingget_conversations_lite()only whenlimit > 1;limit <= 1calls use the originalget_conversations()with full hydration (already fast — only 1-2 Firestore round-trips).Tests added (12 new, all passing)
test_conversations_lite_db.py— imports REALget_conversations_lite()via importlib; tests field stripping, all 7 filter parameters, include_discarded=True skiptest_conversations_router_branching.py— patches REALrouters.conversations.get_conversations; tests limit=1 full hydration, limit=2/100 lite, empty statuses default, locked field strippingtest_mcp_and_developer_conversations_lite.py— patches REALrouters.mcpandrouters.developer; tests MCP always uses lite, developer conditional on include_transcript, speaker enrichment only on include_transcript=Truetest.shKnown edge case
Discarded conversations use
getTranscript()for list title display (conversation_list_item.dart:289). Withtranscript_segments=[], discarded cards show blank title text. This only affects users who enable "show discarded" — the app's default call usesinclude_discarded=false. Full transcript remains available via the detail endpoint.Test plan
backend/test.sh— 20 pass (8 original + 12 new), 5 pre-existing failures (unrelatedtest_process_conversation_usage_context.py)Closes #4904
by AI for @beastoin