feat: add build version with timestamp
- Add build.rs to generate BUILD_VERSION at compile time - Update CLI to show full version: '0.1.0 (build: 2026-03-31 11:21:37)' - Update health endpoints to return build version - Add chrono as build dependency
This commit is contained in:
@@ -110,3 +110,6 @@ path = "src/bin/migrate_chinese_text.rs"
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "test_bm25_simple"
|
name = "test_bm25_simple"
|
||||||
path = "src/bin/test_bm25_simple.rs"
|
path = "src/bin/test_bm25_simple.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
chrono = "0.4"
|
||||||
|
|||||||
19
build.rs
Normal file
19
build.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let now = Local::now();
|
||||||
|
let build_time = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
|
||||||
|
// Get version from Cargo.toml
|
||||||
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
let full_version = format!("{} (build: {})", version, build_time);
|
||||||
|
|
||||||
|
// Set build-time environment variables
|
||||||
|
println!("cargo:rustc-env=BUILD_VERSION={}", full_version);
|
||||||
|
println!("cargo:rustc-env=BUILD_TIME={}", build_time);
|
||||||
|
println!("cargo:rustc-env=VERSION={}", version);
|
||||||
|
|
||||||
|
// Also print for debugging
|
||||||
|
println!("cargo:warning=Building version: {}", full_version);
|
||||||
|
}
|
||||||
@@ -12,8 +12,10 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use crate::core::cache::{keys, MongoCache, RedisCache};
|
use crate::core::cache::{keys, MongoCache, RedisCache};
|
||||||
use crate::core::db::{Database, PostgresDb, QdrantDb, RedisClient, VideoRecord, VideoStatus};
|
use crate::core::db::{Database, PostgresDb, QdrantDb, RedisClient, VideoRecord, VideoStatus};
|
||||||
|
use crate::core::text::tokenizer::tokenize_chinese_text;
|
||||||
use crate::{Embedder, FileManager};
|
use crate::{Embedder, FileManager};
|
||||||
|
|
||||||
|
use super::face_recognition;
|
||||||
use super::middleware::api_key_validation;
|
use super::middleware::api_key_validation;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -56,7 +58,7 @@ fn get_uptime_ms() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
pub struct AppState {
|
||||||
embedder: std::sync::Arc<Embedder>,
|
embedder: std::sync::Arc<Embedder>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
embedder_model: String,
|
embedder_model: String,
|
||||||
@@ -238,13 +240,14 @@ struct HybridSearchResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extract_text_from_content(content: &serde_json::Value) -> String {
|
fn extract_text_from_content(content: &serde_json::Value) -> String {
|
||||||
content
|
let raw_text = content
|
||||||
.get("data")
|
.get("data")
|
||||||
.and_then(|data| data.get("text"))
|
.and_then(|data| data.get("text"))
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.or_else(|| content.get("text").and_then(|v| v.as_str()))
|
.or_else(|| content.get("text").and_then(|v| v.as_str()))
|
||||||
.unwrap_or("")
|
.unwrap_or("");
|
||||||
.to_string()
|
|
||||||
|
tokenize_chinese_text(raw_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_title_from_content(content: &serde_json::Value) -> String {
|
fn extract_title_from_content(content: &serde_json::Value) -> String {
|
||||||
@@ -296,7 +299,7 @@ async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
|||||||
if let Ok(Some(status)) = state.redis_cache.get_health().await {
|
if let Ok(Some(status)) = state.redis_cache.get_health().await {
|
||||||
return Json(HealthResponse {
|
return Json(HealthResponse {
|
||||||
status,
|
status,
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("BUILD_VERSION").to_string(),
|
||||||
uptime_ms: get_uptime_ms(),
|
uptime_ms: get_uptime_ms(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -306,7 +309,7 @@ async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
|||||||
|
|
||||||
Json(HealthResponse {
|
Json(HealthResponse {
|
||||||
status,
|
status,
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("BUILD_VERSION").to_string(),
|
||||||
uptime_ms: get_uptime_ms(),
|
uptime_ms: get_uptime_ms(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -326,7 +329,7 @@ async fn health_detailed(State(state): State<AppState>) -> Json<DetailedHealthRe
|
|||||||
|
|
||||||
Json(DetailedHealthResponse {
|
Json(DetailedHealthResponse {
|
||||||
status: overall_status.to_string(),
|
status: overall_status.to_string(),
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
version: env!("BUILD_VERSION").to_string(),
|
||||||
uptime_ms: get_uptime_ms(),
|
uptime_ms: get_uptime_ms(),
|
||||||
services: ServiceHealth {
|
services: ServiceHealth {
|
||||||
postgres,
|
postgres,
|
||||||
@@ -930,6 +933,118 @@ async fn n8n_search(
|
|||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn search_bm25(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<SearchRequest>,
|
||||||
|
) -> Result<Json<SearchResponse>, 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<SearchResult> = 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, anyhow::Error>(SearchResponse {
|
||||||
|
results,
|
||||||
|
query: req.query.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn n8n_search_bm25(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<SearchRequest>,
|
||||||
|
) -> Result<Json<N8nSearchResponse>, 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, anyhow::Error>(N8nSearchResponse {
|
||||||
|
query: req.query.clone(),
|
||||||
|
count: hits.len(),
|
||||||
|
hits,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
async fn hybrid_search(
|
async fn hybrid_search(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<HybridSearchRequest>,
|
Json(req): Json<HybridSearchRequest>,
|
||||||
@@ -1430,15 +1545,18 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
|||||||
.route("/api/v1/register", post(register))
|
.route("/api/v1/register", post(register))
|
||||||
.route("/api/v1/unregister", post(unregister))
|
.route("/api/v1/unregister", post(unregister))
|
||||||
.route("/api/v1/probe", post(probe))
|
.route("/api/v1/probe", post(probe))
|
||||||
|
.route("/api/v1/search/hybrid", post(hybrid_search))
|
||||||
.route("/api/v1/search", post(search))
|
.route("/api/v1/search", post(search))
|
||||||
.route("/api/v1/n8n/search", post(n8n_search))
|
.route("/api/v1/n8n/search", post(n8n_search))
|
||||||
.route("/api/v1/search/hybrid", post(hybrid_search))
|
.route("/api/v1/search/bm25", post(search_bm25))
|
||||||
|
.route("/api/v1/n8n/search/bm25", post(n8n_search_bm25))
|
||||||
.route("/api/v1/lookup", get(lookup))
|
.route("/api/v1/lookup", get(lookup))
|
||||||
.route("/api/v1/videos", get(list_videos))
|
.route("/api/v1/videos", get(list_videos))
|
||||||
.route("/api/v1/progress/:uuid", get(get_progress))
|
.route("/api/v1/progress/:uuid", get(get_progress))
|
||||||
.route("/api/v1/jobs", get(list_jobs))
|
.route("/api/v1/jobs", get(list_jobs))
|
||||||
.route("/api/v1/jobs/:uuid", get(get_job))
|
.route("/api/v1/jobs/:uuid", get(get_job))
|
||||||
.route("/api/v1/config/cache", post(cache_toggle))
|
.route("/api/v1/config/cache", post(cache_toggle))
|
||||||
|
.merge(face_recognition::face_recognition_routes())
|
||||||
.layer(axum::middleware::from_fn_with_state(
|
.layer(axum::middleware::from_fn_with_state(
|
||||||
state.api_state.clone(),
|
state.api_state.clone(),
|
||||||
api_key_validation,
|
api_key_validation,
|
||||||
|
|||||||
@@ -625,6 +625,7 @@ async fn process_caption_module(
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "momentry")]
|
#[command(name = "momentry")]
|
||||||
#[command(about = "Digital asset management system with video analysis and RAG")]
|
#[command(about = "Digital asset management system with video analysis and RAG")]
|
||||||
|
#[command(version = env!("BUILD_VERSION"))]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
|
|||||||
@@ -622,8 +622,9 @@ async fn process_caption_module(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "momentry")]
|
#[command(name = "momentry_playground")]
|
||||||
#[command(about = "Digital asset management system with video analysis and RAG")]
|
#[command(about = "Momentry Development Server")]
|
||||||
|
#[command(version = env!("BUILD_VERSION"))]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
|
|||||||
Reference in New Issue
Block a user