diff --git a/Cargo.lock b/Cargo.lock index 0d2e2a6..d2838c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,7 +620,7 @@ checksum = "6236364b88b9b6d0bc181ba374cf1ab55ba3ef97a1cb6f8cddad48a273767fb5" [[package]] name = "cases" -version = "0.2.9" +version = "0.2.12" dependencies = [ "arrow", "askama", @@ -662,9 +662,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1279,9 +1279,9 @@ checksum = "b04dc5a38e4f151a79d9f2451ae6037fb6eaf5cba34771f44781f80e508498e3" [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "encode_unicode" @@ -2999,9 +2999,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr3" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e4dd828515431dd6c4a030d26f7eaed7dd4778226e9d2bb968d65ca4ec3d4d" +checksum = "be5bfc63c4dc85083c9daaf7112d0261701d4058677c3bff7f2afc44e30ef3e1" dependencies = [ "proc-macro2", "quote", @@ -3009,14 +3009,14 @@ dependencies = [ [[package]] name = "proc-macro-error3" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee475e440453418ff1335189eddf7101ba502cd818ab7ae04209bc83aa925aa" +checksum = "dd0d42490f6b7b143eef32b9e3522e42bf25dadc02c69ed72236f80adb949b5c" dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3478,9 +3478,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -4279,9 +4279,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", diff --git a/Cargo.toml b/Cargo.toml index 9a15615..f421b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cases" -version = "0.2.9" +version = "0.2.12" edition = "2024" rust-version = "1.92" diff --git a/src/bin/main.rs b/src/bin/main.rs index 5c22d28..5fa8314 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,5 +1,7 @@ use axum::{Router, http::StatusCode, routing::get}; -use cases::{AppState, CONFIG, Tan, case, help, kv_sep_partition_option, search, style}; +use cases::{ + AppState, CONFIG, Tan, api_search, case, help, kv_sep_partition_option, search, style, +}; use fjall::Config; use std::{net::SocketAddr, sync::Arc, time::Duration}; @@ -58,6 +60,7 @@ async fn main() { .route("/case/{id}", get(case)) .route("/style.css", get(style)) .route("/help.txt", get(help)) + .route("/api/search", get(api_search)) .layer(middleware_stack) .with_state(app_state); diff --git a/src/controller.rs b/src/controller.rs index d9d37ce..70d7ba4 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,5 +1,6 @@ use askama::Template; use axum::{ + Json, body::Body, extract::{Path, Query, State}, http::{Response, StatusCode, header}, @@ -7,7 +8,7 @@ use axum::{ }; use bincode::config::standard; use indexmap::IndexSet; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::sync::LazyLock; use tantivy::{ DocAddress, Score, TantivyDocument, @@ -123,12 +124,8 @@ pub struct QuerySearch { #[derive(Template)] #[template(path = "search.html")] pub struct SearchPage { - search: String, - offset: usize, - total: usize, - search_type: String, - enable_vsearch: bool, - cases: Vec<(u32, String, Case)>, + search_meta: SearchMeta, + cases: Vec, } #[cfg(feature = "vsearch")] @@ -142,10 +139,22 @@ static MODEL: LazyLock> = LazyLock::new(|| { Mutex::new(model) }); -pub async fn search( +#[derive(Serialize)] +struct SearchMeta { + offset: usize, + search: String, + search_type: String, + limit: usize, + total: usize, + export: bool, + enable_vsearch: bool, +} + +async fn search_cases( Query(input): Query, State(state): State, -) -> impl IntoResponse { +) -> (Vec, SearchMeta) { + let now = std::time::Instant::now(); let mut offset = input.offset.unwrap_or_default(); if offset > *MAX_RESULTS { offset = *MAX_RESULTS @@ -162,7 +171,6 @@ pub async fn search( let mut ids: IndexSet = IndexSet::with_capacity(20); let mut total = 0; if !search.trim().is_empty() { - let now = std::time::Instant::now(); let search = fast2s::convert(&search); if search_type == "keyword" { let (query, _) = state.searcher.query_parser.parse_query_lenient(&search); @@ -249,13 +257,48 @@ pub async fn search( .chars() .take(240) .collect(); - cases.push((id, preview, case)); + let case_data = CaseData { + id, + preview, + doc_id: case.doc_id, + case_id: case.case_id, + case_name: case.case_name, + court: case.court, + case_type: case.case_type, + procedure: case.procedure, + judgment_date: case.judgment_date, + public_date: case.public_date, + parties: case.parties, + cause: case.cause, + legal_basis: case.legal_basis, + full_text: case.full_text, + }; + cases.push(case_data); } } + let search_meta = SearchMeta { + offset, + search, + search_type, + total, + export, + limit, + enable_vsearch: cfg!(feature = "vsearch"), + }; + + (cases, search_meta) +} + +pub async fn search(query: Query, state: State) -> impl IntoResponse { + let (cases, search_meta) = search_cases(query, state).await; + // export to csv - if export { - let fname = format!("{search}_{total}_{limit}_{offset}.csv"); + if search_meta.export { + let fname = format!( + "{}_{}_{}_{}.csv", + search_meta.search, search_meta.total, search_meta.limit, search_meta.offset + ); let body = Vec::new(); let mut wtr = csv::Writer::from_writer(body); wtr.write_record([ @@ -274,9 +317,9 @@ pub async fn search( "full_text", ]) .unwrap(); - for (id, _, case) in &cases { + for case in &cases { wtr.write_record([ - &id.to_string(), + &case.id.to_string(), &case.doc_id, &case.case_id, &case.case_name, @@ -304,18 +347,40 @@ pub async fn search( return (headers, wtr.into_inner().unwrap()).into_response(); } - let body = SearchPage { - search, - search_type, - offset, - cases, - total, - enable_vsearch: cfg!(feature = "vsearch"), - }; - + let body = SearchPage { search_meta, cases }; into_response(&body) } +pub async fn api_search(query: Query, state: State) -> impl IntoResponse { + let (cases, search_meta) = search_cases(query, state).await; + let search_data = SearchData { search_meta, cases }; + Json(search_data).into_response() +} + +#[derive(Serialize)] +pub struct CaseData { + id: u32, + preview: String, + doc_id: String, + case_id: String, + case_name: String, + court: String, + case_type: String, + procedure: String, + judgment_date: String, + public_date: String, + parties: String, + cause: String, + legal_basis: String, + full_text: String, +} + +#[derive(Serialize)] +pub struct SearchData { + search_meta: SearchMeta, + cases: Vec, +} + pub async fn style() -> impl IntoResponse { let headers = [ (header::CONTENT_TYPE, "text/css"), diff --git a/src/lib.rs b/src/lib.rs index c6e9bf2..d87a722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ use bincode::{Decode, Encode}; pub use config::CONFIG; -pub use controller::{case, help, search, style}; +pub use controller::{api_search, case, help, search, style}; use fjall::{KvSeparationOptions, PartitionCreateOptions, PartitionHandle}; use scraper::Html; use serde::{Deserialize, Serialize}; diff --git a/static/help.txt b/static/help.txt index c1a8495..570ec68 100644 --- a/static/help.txt +++ b/static/help.txt @@ -1,3 +1,5 @@ +更多合作请发邮件至 contact@caseopen.org (请勿使用微软旗下邮箱服务如outlook.com) + ### 关键词查询语法简明指南 可用字段一览: @@ -95,6 +97,16 @@ https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html 导出功能: 最多导出10000条,调整offset参数可获得更多结果,offset=10000,即可获得第10000~20000条结果。如: -https://caseopen.org/?search=%E6%8B%90%E5%8D%96&offset=10000&search_type=default&export=true +https://caseopen.org/?search=%E6%8B%90%E5%8D%96&offset=10000&search_type=keyword&export=true + +----------------------------- + +api: + +/api/search?search=拐卖妇女儿童 + +可选参数: +- search_type: keyword(默认关键词搜索)/ vsearch(语义搜索,刑事案件可用) +- offset: 偏移量,默认0 -更多合作请发邮件至 contact@caseopen.org +如:/api/search?search=拐卖妇女儿童&search_type=vsearch&offset=100 \ No newline at end of file diff --git a/templates/search.html b/templates/search.html index c915778..51db818 100644 --- a/templates/search.html +++ b/templates/search.html @@ -20,12 +20,12 @@ type="text" id="search" name="search" - value="{{ search }}" + value="{{ search_meta.search }}" /> - {% if enable_vsearch %} + {% if search_meta.enable_vsearch %} {% endif %} @@ -44,47 +44,47 @@

- {% if search_type =="keyword" %} 找到 {{ total }},{% endif %} + {% if search_meta.search_type =="keyword" %} 找到 {{ search_meta.total }},{% endif %} 导出

- {% if enable_vsearch %} - {% if search_type == "keyword" %} - {% if total < 30 %} - {% if !search.is_empty() %} -

当前结果较少,建议使用 语义搜索

+ {% if search_meta.enable_vsearch %} + {% if search_meta.search_type == "keyword" %} + {% if search_meta.total < 30 %} + {% if !search_meta.search.is_empty() %} +

当前结果较少,建议使用 语义搜索

{% endif %} {% endif %} {% endif %} {% endif %}
- {% for (id, preview, case) in cases %} + {% for case in cases %}
- +

{{ case.case_name }}

{{ case.judgment_date }} - {{ case.case_type }} - {{ case.procedure }}

-

{{ preview }}

+

{{ case.preview }}

{{ case.case_id}} - {{ case.court }}

{% endfor %}