feat: trace-level matching, health watcher/worker status, timezone config
This commit is contained in:
@@ -88,9 +88,9 @@ pub enum SearchResult {
|
||||
},
|
||||
#[serde(rename = "person")]
|
||||
Person {
|
||||
person_id: String,
|
||||
identity_id: i32,
|
||||
identity_uuid: String,
|
||||
name: Option<String>,
|
||||
speaker_id: Option<String>,
|
||||
appearance_count: i32,
|
||||
score: f64,
|
||||
first_appearance_time: Option<f64>,
|
||||
@@ -168,7 +168,7 @@ pub async fn universal_search(
|
||||
results.retain(|r| match r {
|
||||
SearchResult::Chunk { chunk_id, .. } => seen_chunks.insert(chunk_id.clone()),
|
||||
SearchResult::Frame { frame_number, .. } => seen_frames.insert(*frame_number),
|
||||
SearchResult::Person { person_id, .. } => seen_persons.insert(person_id.clone()),
|
||||
SearchResult::Person { identity_id, .. } => seen_persons.insert(*identity_id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,9 +251,9 @@ pub async fn search_persons(
|
||||
let limit = query.limit.unwrap_or(20);
|
||||
let persons = search_persons_by_query(
|
||||
&db,
|
||||
&query.file_uuid,
|
||||
&query.query,
|
||||
query.min_appearances,
|
||||
query.max_age,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
@@ -305,7 +305,6 @@ pub struct PersonSearchQuery {
|
||||
pub file_uuid: String,
|
||||
pub query: Option<String>,
|
||||
pub min_appearances: Option<i32>,
|
||||
pub max_age: Option<i32>, // New filter for "children"
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -317,13 +316,9 @@ pub struct PersonSearchResponse {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PersonResult {
|
||||
pub person_id: String,
|
||||
pub identity_id: i32,
|
||||
pub identity_uuid: String,
|
||||
pub name: Option<String>,
|
||||
pub character_name: Option<String>,
|
||||
pub aliases: Option<Vec<String>>,
|
||||
pub age: Option<i32>,
|
||||
pub gender: Option<String>,
|
||||
pub speaker_id: Option<String>,
|
||||
pub appearance_count: i32,
|
||||
pub first_appearance_time: Option<f64>,
|
||||
pub last_appearance_time: Option<f64>,
|
||||
@@ -594,43 +589,37 @@ async fn search_persons_internal(
|
||||
db: &PostgresDb,
|
||||
req: &UniversalSearchRequest,
|
||||
) -> Result<Vec<SearchResult>, anyhow::Error> {
|
||||
let table = "person_identities";
|
||||
let uuid = match &req.file_uuid {
|
||||
Some(u) => u.replace('\'', "''"),
|
||||
None => return Err(anyhow::anyhow!("file_uuid is required for person search")),
|
||||
};
|
||||
|
||||
let id_table = schema::table_name("identities");
|
||||
let fd_table = schema::table_name("face_detections");
|
||||
let mut sql = format!(
|
||||
"SELECT person_id, name, speaker_id, appearance_count, first_appearance_time, last_appearance_time FROM {} WHERE 1=1",
|
||||
table
|
||||
"SELECT i.id, i.uuid::text, i.name, COUNT(fd.id) AS appearance_count, \
|
||||
MIN(fd.timestamp_secs) AS first_time, MAX(fd.timestamp_secs) AS last_time \
|
||||
FROM {} i JOIN {} fd ON fd.identity_id = i.id \
|
||||
WHERE fd.file_uuid = '{}'",
|
||||
id_table, fd_table, uuid
|
||||
);
|
||||
|
||||
if !req.query.is_empty() {
|
||||
sql.push_str(&format!(
|
||||
" AND (name ILIKE '%{}%' OR person_id ILIKE '%{}%' OR speaker_id ILIKE '%{}%')",
|
||||
req.query, req.query, req.query
|
||||
));
|
||||
}
|
||||
if let Some(ref filters) = req.filters {
|
||||
if let Some(ref speaker_id) = filters.speaker_id {
|
||||
sql.push_str(&format!(" AND speaker_id = '{}'", speaker_id));
|
||||
}
|
||||
if let Some(ref person_id) = filters.person_id {
|
||||
sql.push_str(&format!(" AND person_id = '{}'", person_id));
|
||||
}
|
||||
let q = req.query.replace('\'', "''");
|
||||
sql.push_str(&format!(" AND i.name ILIKE '%{}%'", q));
|
||||
}
|
||||
|
||||
sql.push_str(" GROUP BY i.id, i.uuid, i.name");
|
||||
sql.push_str(" ORDER BY appearance_count DESC");
|
||||
sql.push_str(&format!(" LIMIT {}", req.page_size.unwrap_or(20)));
|
||||
|
||||
let rows: Vec<(
|
||||
String,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
i32,
|
||||
Option<f64>,
|
||||
Option<f64>,
|
||||
)> = sqlx::query_as(&sql).fetch_all(db.pool()).await?;
|
||||
let rows: Vec<(i32, String, Option<String>, i64, Option<f64>, Option<f64>)> =
|
||||
sqlx::query_as(&sql).fetch_all(db.pool()).await?;
|
||||
|
||||
let results: Vec<SearchResult> = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(person_id, name, speaker_id, appearance_count, first_time, last_time)| {
|
||||
|(identity_id, identity_uuid, name, appearance_count, first_time, last_time)| {
|
||||
let score = if !req.query.is_empty()
|
||||
&& name.as_ref().map_or(false, |n| {
|
||||
n.to_lowercase().contains(&req.query.to_lowercase())
|
||||
@@ -641,10 +630,10 @@ async fn search_persons_internal(
|
||||
};
|
||||
|
||||
SearchResult::Person {
|
||||
person_id,
|
||||
identity_id,
|
||||
identity_uuid,
|
||||
name,
|
||||
speaker_id,
|
||||
appearance_count,
|
||||
appearance_count: appearance_count as i32,
|
||||
score,
|
||||
first_appearance_time: first_time,
|
||||
last_appearance_time: last_time,
|
||||
@@ -739,82 +728,49 @@ async fn search_frames_internal_v2(
|
||||
|
||||
async fn search_persons_by_query(
|
||||
db: &PostgresDb,
|
||||
file_uuid: &str,
|
||||
query: &Option<String>,
|
||||
min_appearances: Option<i32>,
|
||||
max_age: Option<i32>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<PersonResult>, anyhow::Error> {
|
||||
let table = "person_identities";
|
||||
let id_table = schema::table_name("identities");
|
||||
let fd_table = schema::table_name("face_detections");
|
||||
let mut sql = format!(
|
||||
"SELECT person_id, name, character_name, aliases, age, gender, speaker_id, appearance_count, first_appearance_time, last_appearance_time FROM {} WHERE 1=1",
|
||||
table
|
||||
"SELECT i.id, i.uuid::text, i.name, COUNT(fd.id) AS appearance_count, \
|
||||
MIN(fd.timestamp_secs) AS first_time, MAX(fd.timestamp_secs) AS last_time \
|
||||
FROM {} i JOIN {} fd ON fd.identity_id = i.id \
|
||||
WHERE fd.file_uuid = '{}'",
|
||||
id_table,
|
||||
fd_table,
|
||||
file_uuid.replace('\'', "''")
|
||||
);
|
||||
|
||||
if let Some(ref q) = query {
|
||||
// Search name, character_name, aliases (cast to text), person_id, speaker_id
|
||||
sql.push_str(&format!(
|
||||
" AND (name ILIKE '%{}%' OR character_name ILIKE '%{}%' OR aliases::text ILIKE '%{}%' OR person_id ILIKE '%{}%' OR speaker_id ILIKE '%{}%')",
|
||||
q, q, q, q, q
|
||||
));
|
||||
if let Some(q) = query {
|
||||
let safe = q.replace('\'', "''");
|
||||
sql.push_str(&format!(" AND i.name ILIKE '%{}%'", safe));
|
||||
}
|
||||
|
||||
sql.push_str(" GROUP BY i.id, i.uuid, i.name");
|
||||
|
||||
if let Some(min) = min_appearances {
|
||||
sql.push_str(&format!(" AND appearance_count >= {}", min));
|
||||
}
|
||||
if let Some(max_a) = max_age {
|
||||
// Strictly filter for age <= max_age.
|
||||
// Note: This excludes entries with NULL age.
|
||||
sql.push_str(&format!(" AND age <= {}", max_a));
|
||||
sql.push_str(&format!(" HAVING COUNT(fd.id) >= {}", min));
|
||||
}
|
||||
|
||||
sql.push_str(" ORDER BY appearance_count DESC");
|
||||
sql.push_str(&format!(" LIMIT {}", limit));
|
||||
|
||||
let rows: Vec<(
|
||||
String,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<serde_json::Value>,
|
||||
Option<i32>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
i32,
|
||||
Option<f64>,
|
||||
Option<f64>,
|
||||
)> = sqlx::query_as(&sql).fetch_all(db.pool()).await?;
|
||||
let rows: Vec<(i32, String, Option<String>, i64, Option<f64>, Option<f64>)> =
|
||||
sqlx::query_as(&sql).fetch_all(db.pool()).await?;
|
||||
|
||||
let results: Vec<PersonResult> = rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(
|
||||
person_id,
|
||||
name,
|
||||
character_name,
|
||||
aliases_json,
|
||||
age,
|
||||
gender,
|
||||
speaker_id,
|
||||
appearance_count,
|
||||
first_time,
|
||||
last_time,
|
||||
)| {
|
||||
let aliases = aliases_json.and_then(|v| {
|
||||
v.as_array().map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|val| val.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
});
|
||||
|
||||
|(identity_id, identity_uuid, name, appearance_count, first_time, last_time)| {
|
||||
PersonResult {
|
||||
person_id,
|
||||
identity_id,
|
||||
identity_uuid,
|
||||
name,
|
||||
character_name,
|
||||
aliases,
|
||||
age,
|
||||
gender,
|
||||
speaker_id,
|
||||
appearance_count,
|
||||
appearance_count: appearance_count as i32,
|
||||
first_appearance_time: first_time,
|
||||
last_appearance_time: last_time,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user