feat: backup architecture docs, source code, and scripts
This commit is contained in:
195
src/api/search.rs
Normal file
195
src/api/search.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
//! Smart Search API
|
||||
//! Implements the 5W1H search capability using semantic vectors.
|
||||
|
||||
use axum::{extract::State, http::StatusCode, response::Json, routing::post, Router};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use tracing;
|
||||
|
||||
use crate::core::db::PostgresDb;
|
||||
|
||||
// --- Request / Response Structures ---
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SmartSearchRequest {
|
||||
pub uuid: String,
|
||||
pub query: String,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SearchResult {
|
||||
pub id: i32,
|
||||
pub parent_id: i32,
|
||||
pub scene_order: Option<i32>,
|
||||
|
||||
// Primary: frame-accurate position (authoritative unit)
|
||||
pub start_frame: i64,
|
||||
pub end_frame: i64,
|
||||
pub fps: f64,
|
||||
|
||||
// Reference: time derived from frames (subject to FPS variation, not precise)
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
|
||||
pub raw_text: Option<String>, // Text content of the child chunk
|
||||
pub summary: Option<String>, // Summary from parent context
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub similarity: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SmartSearchResponse {
|
||||
pub query: String,
|
||||
pub results: Vec<SearchResult>,
|
||||
pub strategy: String,
|
||||
}
|
||||
|
||||
// --- API Handler ---
|
||||
|
||||
pub async fn smart_search(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Json(req): Json<SmartSearchRequest>,
|
||||
) -> Result<Json<SmartSearchResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let db = &state.db;
|
||||
let limit = req.limit.unwrap_or(5);
|
||||
|
||||
// 1. Generate Embedding using Ollama
|
||||
let embedding = get_ollama_embedding(&req.query).await.map_err(
|
||||
|e| -> (StatusCode, Json<serde_json::Value>) {
|
||||
tracing::error!("Embedding failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
// 2. Search Database (Drill-Down: Find Parents First)
|
||||
let db_parents: Vec<crate::core::db::postgres_db::SemanticSearchResult> = db
|
||||
.search_parent_chunks_semantic(&req.uuid, &embedding, limit)
|
||||
.await
|
||||
.map_err(
|
||||
|e: anyhow::Error| -> (StatusCode, Json<serde_json::Value>) {
|
||||
tracing::error!("DB search failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
if db_parents.is_empty() {
|
||||
return Ok(Json(SmartSearchResponse {
|
||||
query: req.query,
|
||||
results: vec![],
|
||||
strategy: "semantic_vector_search".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect Parent IDs
|
||||
let parent_ids: Vec<i32> = db_parents.iter().map(|p| p.id).collect();
|
||||
|
||||
// 3. Fetch Children for these Parents (Drill Down)
|
||||
// We fetch all children for these parents (limit can be adjusted)
|
||||
let children: Vec<crate::core::db::postgres_db::ChildChunkResult> = db
|
||||
.get_children_for_parents(&parent_ids, 10) // Fetch top 10 children per parent
|
||||
.await
|
||||
.map_err(
|
||||
|e: anyhow::Error| -> (StatusCode, Json<serde_json::Value>) {
|
||||
tracing::error!("Fetching children failed: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": e.to_string() })),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
// 4. Map Parents to a lookup table
|
||||
let parent_map: std::collections::HashMap<
|
||||
i32,
|
||||
&crate::core::db::postgres_db::SemanticSearchResult,
|
||||
> = db_parents.iter().map(|p| (p.id, p)).collect();
|
||||
|
||||
// Map Children to API response struct
|
||||
let results: Vec<SearchResult> = children
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let parent = parent_map.get(&c.parent_id);
|
||||
SearchResult {
|
||||
id: c.id,
|
||||
parent_id: c.parent_id,
|
||||
scene_order: parent.map(|p| p.scene_order),
|
||||
|
||||
start_frame: c.start_frame,
|
||||
end_frame: c.end_frame,
|
||||
fps: c.fps,
|
||||
|
||||
start_time: c.start_time,
|
||||
end_time: c.end_time,
|
||||
raw_text: Some(c.raw_text),
|
||||
summary: parent.map(|p| p.summary.clone()),
|
||||
metadata: parent.map(|p| p.metadata.clone()),
|
||||
similarity: parent.and_then(|p| p.similarity),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 6. Sort results by similarity (descending)
|
||||
// Since all children of a parent have the same parent similarity, this groups relevant chunks together
|
||||
let mut results = results;
|
||||
results.sort_by(|a, b| {
|
||||
b.similarity
|
||||
.partial_cmp(&a.similarity)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// 7. Limit the final results (optional, but good for API consistency)
|
||||
let limit = req.limit.unwrap_or(5) * 5; // Allow more children per parent context
|
||||
results.truncate(limit);
|
||||
|
||||
// 8. Format Response
|
||||
let response = SmartSearchResponse {
|
||||
query: req.query,
|
||||
results,
|
||||
strategy: "drill_down_semantic_search".to_string(),
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
// --- Helper: Ollama Embedding ---
|
||||
|
||||
async fn get_ollama_embedding(
|
||||
text: &str,
|
||||
) -> Result<Vec<f32>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({
|
||||
"model": "nomic-embed-text",
|
||||
"prompt": text
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post("http://localhost:11434/api/embeddings")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
|
||||
// Parse embedding array from response
|
||||
let embedding = res["embedding"]
|
||||
.as_array()
|
||||
.ok_or("No embedding found in Ollama response")?
|
||||
.iter()
|
||||
.map(|v| v.as_f64().unwrap_or(0.0) as f32)
|
||||
.collect();
|
||||
|
||||
Ok(embedding)
|
||||
}
|
||||
|
||||
// --- Router Setup ---
|
||||
|
||||
pub fn search_routes() -> Router<crate::api::server::AppState> {
|
||||
Router::new().route("/smart", post(smart_search))
|
||||
}
|
||||
Reference in New Issue
Block a user