async fn search_bm25( State(state): State, Json(req): Json, ) -> Result, StatusCode> { let limit = req.limit.unwrap_or(10); let query_hash = generate_query_hash(&req.query, req.uuid.as_deref(), limit); let cache_key = keys::bm25_search(&query_hash); let ttl = state.mongo_cache.ttl_search(); let response = state .mongo_cache .get_or_fetch(&cache_key, ttl, keys::CATEGORY_SEARCH, || async { let pg = PostgresDb::init() .await .map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?; let bm25_results = pg .search_bm25(&req.query, req.uuid.as_deref(), limit) .await?; let results: Vec = bm25_results .into_iter() .map(|r| SearchResult { uuid: r.uuid, chunk_id: r.chunk_id, chunk_type: r.chunk_type, start_time: r.start_time, end_time: r.end_time, text: r.text, score: r.bm25_score, }) .collect(); Ok::(SearchResponse { results, query: req.query.clone(), }) }) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(response)) } async fn n8n_search_bm25( State(state): State, Json(req): Json, ) -> Result, StatusCode> { let limit = req.limit.unwrap_or(10); let query_hash = generate_query_hash(&req.query, req.uuid.as_deref(), limit); let cache_key = keys::n8n_bm25_search(&query_hash); let ttl = state.mongo_cache.ttl_search(); let response = state .mongo_cache .get_or_fetch(&cache_key, ttl, keys::CATEGORY_N8N_SEARCH, || async { let pg = PostgresDb::init() .await .map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?; let bm25_results = pg .search_bm25(&req.query, req.uuid.as_deref(), limit) .await?; let mut hits = Vec::new(); for r in bm25_results { if let Some(chunk) = pg .get_chunk_by_chunk_id_and_uuid(&r.chunk_id, &r.uuid) .await .ok() .flatten() { let text = r.text; // Use text from BM25 result let title = extract_title_from_content(&chunk.content); let file_path = if chunk.uuid.is_empty() { None } else { let video = pg.get_video_by_uuid(&chunk.uuid).await.ok().flatten(); video.map(|v| v.file_path) }; hits.push(N8nSearchHit { id: chunk.chunk_id.clone(), vid: chunk.uuid.clone(), start: chunk.start_time().seconds(), end: chunk.end_time().seconds(), title: if title.is_empty() { format!("Chunk {}", chunk.chunk_id) } else { title }, text, score: r.bm25_score, file_path, }); } } Ok::(N8nSearchResponse { query: req.query.clone(), count: hits.len(), hits, }) }) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(response)) }