Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cases"
version = "0.2.9"
version = "0.2.12"
edition = "2024"
rust-version = "1.92"

Expand Down
5 changes: 4 additions & 1 deletion src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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);

Expand Down
113 changes: 89 additions & 24 deletions src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use askama::Template;
use axum::{
Json,
body::Body,
extract::{Path, Query, State},
http::{Response, StatusCode, header},
response::{Html, IntoResponse},
};
use bincode::config::standard;
use indexmap::IndexSet;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use tantivy::{
DocAddress, Score, TantivyDocument,
Expand Down Expand Up @@ -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<CaseData>,
}

#[cfg(feature = "vsearch")]
Expand All @@ -142,10 +139,22 @@ static MODEL: LazyLock<Mutex<TextEmbedding>> = 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<QuerySearch>,
State(state): State<AppState>,
) -> impl IntoResponse {
) -> (Vec<CaseData>, SearchMeta) {
let now = std::time::Instant::now();
let mut offset = input.offset.unwrap_or_default();
if offset > *MAX_RESULTS {
offset = *MAX_RESULTS
Expand All @@ -162,7 +171,6 @@ pub async fn search(
let mut ids: IndexSet<u32> = 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);
Expand Down Expand Up @@ -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<QuerySearch>, state: State<AppState>) -> 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([
Expand All @@ -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,
Expand Down Expand Up @@ -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<QuerySearch>, state: State<AppState>) -> 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,
}
Comment on lines +360 to +376

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

CaseData.full_text is unconditionally serialized for every search hit, even though nothing but CSV export needs it.

The HTML template (templates/search.html) only renders case.preview, never case.full_text; the new /api/search JSON endpoint (Lines 354-358) serializes it for every result regardless, and since api_search never branches on search_meta.export, calling it with export=true raises the limit to EXPORT_LIMIT (10000 by default) and returns full document text for up to 10000 hits as JSON. Only the CSV branch of search (Lines 320-334) actually consumes case.full_text. Excluding it from serialization keeps CSV working while avoiding a large, unintended payload/bandwidth cost on the new JSON endpoint.

♻️ Suggested fix
 #[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,
+    #[serde(skip_serializing)]
     full_text: String,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[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 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,
#[serde(skip_serializing)]
full_text: String,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controller.rs` around lines 360 - 376, Remove full_text from the
serialized CaseData response while preserving its availability for the CSV
export path in search. Update the CaseData serialization behavior and any
related JSON search handling so HTML and /api/search return only the fields they
consume, while the CSV branch continues using case.full_text.


#[derive(Serialize)]
pub struct SearchData {
search_meta: SearchMeta,
cases: Vec<CaseData>,
}

pub async fn style() -> impl IntoResponse {
let headers = [
(header::CONTENT_TYPE, "text/css"),
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
16 changes: 14 additions & 2 deletions static/help.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
更多合作请发邮件至 contact@caseopen.org (请勿使用微软旗下邮箱服务如outlook.com)

### 关键词查询语法简明指南

可用字段一览:
Expand Down Expand Up @@ -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
Comment on lines +108 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the supported export parameter.

QuerySearch accepts export, and search_cases uses it to change the API result limit. Add it to this parameter list, or explicitly state that it is unsupported for /api/search, so the documentation matches the shared handler contract in src/controller.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/help.txt` around lines 108 - 110, Update the optional-parameter list
in help.txt to document the supported export parameter used by QuerySearch and
search_cases, including its effect on the API result limit; if /api/search does
not support it, explicitly state that instead. Align the wording with the shared
handler contract in QuerySearch and search_cases from src/controller.rs.


更多合作请发邮件至 contact@caseopen.org
如:/api/search?search=拐卖妇女儿童&search_type=vsearch&offset=100
Loading