diff --git a/src/controller.rs b/src/controller.rs index 3c60f5f..902a0b8 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -20,7 +20,10 @@ use tracing::info; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; #[cfg(feature = "vsearch")] -use qdrant_client::qdrant::{SearchPointsBuilder, point_id::PointIdOptions}; +use qdrant_client::{ + Qdrant, + qdrant::{RecommendPointsBuilder, SearchPointsBuilder, point_id::PointIdOptions}, +}; use crate::{AppState, CONFIG, Case, remove_html_tags}; @@ -30,10 +33,24 @@ static MAX_RESULTS: LazyLock = LazyLock::new(|| CONFIG.max_results.unwrap #[derive(Template)] #[template(path = "case.html", escape = "none")] pub struct CasePage { + id: u32, case: Case, + enable_similar: bool, + similar_cases: Vec<(u32, String, String)>, } -pub async fn case(State(state): State, Path(id): Path) -> impl IntoResponse { +#[cfg(feature = "vsearch")] +#[derive(Debug, Deserialize)] +pub struct QueryCase { + #[cfg(feature = "vsearch")] + with_similar: Option, +} + +pub async fn case( + #[cfg(feature = "vsearch")] Query(params): Query, + State(state): State, + Path(id): Path, +) -> impl IntoResponse { info!("id: {}", id); if let Some(v) = state.db.get(id.to_be_bytes()).unwrap() { let (mut case, _): (Case, _) = bincode::decode_from_slice(&v, standard()).unwrap(); @@ -44,7 +61,47 @@ pub async fn case(State(state): State, Path(id): Path) -> impl In { case.full_text = case.full_text[start..].to_owned(); } - let case = CasePage { case }; + + #[allow(unused_mut)] + let mut enable_similar = false; + #[allow(unused_mut)] + let mut similar_cases = Vec::new(); + #[cfg(feature = "vsearch")] + { + let mut with_similar = params.with_similar.unwrap_or(false); + if case.case_type != "刑事案件" { + with_similar = false; + } else { + enable_similar = true; + } + + if with_similar { + let now = std::time::Instant::now(); + let similar_ids = similar(id, &state.qclient).await; + + for sid in similar_ids { + if let Some(v) = state.db.get(sid.to_be_bytes()).unwrap() { + let (scase, _): (Case, _) = + bincode::decode_from_slice(&v, standard()).unwrap(); + similar_cases.push((sid, scase.case_name, scase.case_id)); + } + } + let elapsed = now.elapsed().as_secs_f32(); + info!( + "similar id: {}, found {} similar cases, elapsed: {}s", + id, + similar_cases.len(), + elapsed + ); + } + } + + let case = CasePage { + id, + case, + enable_similar, + similar_cases, + }; into_response(&case) } else { (StatusCode::NOT_FOUND, "Not found").into_response() @@ -275,3 +332,23 @@ fn into_response(t: &T) -> Response { Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } + +#[cfg(feature = "vsearch")] +pub async fn similar(id: u32, qclient: &Qdrant) -> Vec { + let mut ids = Vec::with_capacity(10); + if let Ok(rsp) = qclient + .recommend(RecommendPointsBuilder::new("cases", 10).add_positive(id as u64)) + .await + { + for point in &rsp.result { + if let Some(id) = point.id.as_ref().unwrap().point_id_options.as_ref() + && let PointIdOptions::Num(id) = id + { + ids.push(*id as u32); + } + } + } else { + tracing::error!("Qdrant recommend {id} failed"); + } + ids +} diff --git a/static/style.css b/static/style.css index 6faec4c..6ded0a6 100644 --- a/static/style.css +++ b/static/style.css @@ -525,6 +525,7 @@ b.highlight { .search-nav, .search-second-nav, .pagination, + .similar-cases, footer { display: none !important; } diff --git a/templates/case.html b/templates/case.html index 448920e..35827e6 100644 --- a/templates/case.html +++ b/templates/case.html @@ -26,6 +26,23 @@

{{ case.case_name }}

法律依据:{{ case.legal_basis }}




{{ case.full_text }}
+


+ {% if enable_similar %} +
+

+ 相似文书 +

+ {% if similar_cases.len() > 0 %} +
    + {% for (sim_id, sim_case_name, sim_case_id) in similar_cases %} +
  • + {{sim_case_id}} - {{sim_case_name}} +
  • + {% endfor %} +
+ {% endif %} +
+ {% endif %}