Skip to content

Commit e3a1e5d

Browse files
feat: ct substring search and midend done
1 parent 51eac40 commit e3a1e5d

File tree

4 files changed

+123
-73
lines changed

4 files changed

+123
-73
lines changed

backend/src/db/mod.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use color_eyre::eyre::eyre;
44
use models::DBAdminDashboardQP;
5+
pub use queries::ExamFilter;
56
use sqlx::{postgres::PgPoolOptions, prelude::FromRow, PgPool, Postgres, Transaction};
67
use std::time::Duration;
78

@@ -66,17 +67,15 @@ impl Database {
6667
/// Searches for papers from a given query. Uses some voodoo black magic by @rajivharlalka
6768
pub async fn search_papers(
6869
&self,
69-
query: &String,
70-
exam: Option<Exam>,
70+
query: &str,
71+
exam_filter: ExamFilter,
72+
exam_filter_str: String,
7173
) -> Result<Vec<qp::SearchQP>, sqlx::Error> {
72-
let exam_param = exam.map(String::from).unwrap_or("".into());
73-
let use_exam = !exam_param.is_empty();
74-
75-
let query_sql = queries::get_qp_search_query(use_exam);
74+
let (query_sql, use_exam_arg) = queries::get_qp_search_query(exam_filter);
7675
let query = sqlx::query_as(&query_sql).bind(query);
7776

78-
let query = if use_exam {
79-
query.bind(exam_param)
77+
let query = if use_exam_arg {
78+
query.bind(exam_filter_str)
8079
} else {
8180
query
8281
};

backend/src/db/queries.rs

Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//!
33
//! Some of these are functions that return a query that is dynamically generated based on requirements.
44
5+
use crate::qp::Exam;
6+
57
/// Query to get similar papers. Matches `course_code` ($1) always. Other parameters are optional and can be enabled or disabled using the arguments to this function.
68
pub fn get_similar_papers_query(
79
year: bool,
@@ -80,62 +82,98 @@ pub fn get_all_unapproved_query() -> String {
8082
format!("SELECT {} FROM iqps WHERE approve_status = false and is_deleted=false ORDER BY upload_timestamp ASC", ADMIN_DASHBOARD_QP_FIELDS)
8183
}
8284

85+
/// An enum representing the exam filter for the search query
86+
pub enum ExamFilter {
87+
Exam(Exam), // Match an exact exam or use `ct` substring match
88+
Any, // Match anything
89+
MidEnd, // Midsem or endsem
90+
}
91+
92+
impl TryFrom<&String> for ExamFilter {
93+
type Error = color_eyre::eyre::Error;
94+
95+
fn try_from(value: &String) -> Result<Self, Self::Error> {
96+
if value.is_empty() {
97+
Ok(ExamFilter::Any)
98+
} else if value == "midend" {
99+
Ok(ExamFilter::MidEnd)
100+
} else {
101+
Ok(ExamFilter::Exam(Exam::try_from(value)?))
102+
}
103+
}
104+
}
105+
83106
/// Returns the query for searching question papers. It is mostly voodoo, @Rajiv please update the documentation.
84107
///
85108
/// Optionally, the `exam` argument can be used to also add a clause to match the exam field.
86-
pub fn get_qp_search_query(exam: bool) -> String {
87-
let exam_filter = if exam {
88-
"WHERE (exam = $2 OR exam = '')"
89-
} else {
90-
""
109+
///
110+
/// Query parameters:
111+
/// $1 - Search query
112+
/// $2 - Exam filter string (can be midsem, endsem, midend, or ct)
113+
///
114+
/// Returns the query and a boolean representing whether the second argument is required
115+
pub fn get_qp_search_query(exam_filter: ExamFilter) -> (String, bool) {
116+
let (exam_filter, use_exam_arg) = match exam_filter {
117+
ExamFilter::Any => ("", false),
118+
ExamFilter::MidEnd => (
119+
"WHERE (exam = 'midsem' OR exam = 'endsem' OR exam = '')",
120+
false,
121+
),
122+
ExamFilter::Exam(exam) => match exam {
123+
Exam::CT(_) => ("WHERE (exam LIKE 'ct%' OR exam = '')", false),
124+
_ => ("WHERE (exam = $2 OR exam = '')", true),
125+
},
91126
};
92127

93-
format!("
94-
WITH filtered AS (
95-
SELECT * from iqps {exam_filter}
96-
),
97-
fuzzy AS (
98-
SELECT id,
99-
similarity(course_code || ' ' || course_name, $1) AS sim_score,
100-
row_number() OVER (ORDER BY similarity(course_code || ' ' || course_name, $1) DESC) AS rank_ix
101-
FROM filtered
102-
WHERE (course_code || ' ' || course_name) %>> $1 AND approve_status = true
103-
ORDER BY rank_ix
104-
LIMIT 30
105-
),
106-
full_text AS (
107-
SELECT id,
108-
ts_rank_cd(fts_course_details, websearch_to_tsquery($1)) AS rank_score,
109-
row_number() OVER (ORDER BY ts_rank_cd(fts_course_details, websearch_to_tsquery($1)) DESC) AS rank_ix
110-
FROM filtered
111-
WHERE fts_course_details @@ websearch_to_tsquery($1) AND approve_status = true
112-
ORDER BY rank_ix
113-
LIMIT 30
114-
),
115-
partial_search AS (
116-
SELECT id,
117-
ts_rank_cd(fts_course_details, {to_tsquery}) AS rank_score,
118-
row_number() OVER (ORDER BY ts_rank_cd(fts_course_details, {to_tsquery}) DESC) as rank_ix
119-
FROM filtered
120-
WHERE fts_course_details @@ {to_tsquery} AND approve_status = true
121-
LIMIT 30
128+
(
129+
format!("
130+
WITH filtered AS (
131+
SELECT * from iqps {exam_filter}
132+
),
133+
fuzzy AS (
134+
SELECT id,
135+
similarity(course_code || ' ' || course_name, $1) AS sim_score,
136+
row_number() OVER (ORDER BY similarity(course_code || ' ' || course_name, $1) DESC) AS rank_ix
137+
FROM filtered
138+
WHERE (course_code || ' ' || course_name) %>> $1 AND approve_status = true
139+
ORDER BY rank_ix
140+
LIMIT 30
141+
),
142+
full_text AS (
143+
SELECT id,
144+
ts_rank_cd(fts_course_details, websearch_to_tsquery($1)) AS rank_score,
145+
row_number() OVER (ORDER BY ts_rank_cd(fts_course_details, websearch_to_tsquery($1)) DESC) AS rank_ix
146+
FROM filtered
147+
WHERE fts_course_details @@ websearch_to_tsquery($1) AND approve_status = true
148+
ORDER BY rank_ix
149+
LIMIT 30
150+
),
151+
partial_search AS (
152+
SELECT id,
153+
ts_rank_cd(fts_course_details, {to_tsquery}) AS rank_score,
154+
row_number() OVER (ORDER BY ts_rank_cd(fts_course_details, {to_tsquery}) DESC) as rank_ix
155+
FROM filtered
156+
WHERE fts_course_details @@ {to_tsquery} AND approve_status = true
157+
LIMIT 30
158+
),
159+
result AS (
160+
SELECT {intermediate_fields}
161+
FROM fuzzy
162+
FULL OUTER JOIN full_text ON fuzzy.id = full_text.id
163+
FULL OUTER JOIN partial_search ON coalesce(fuzzy.id, full_text.id) = partial_search.id
164+
JOIN filtered ON coalesce(fuzzy.id, full_text.id, partial_search.id) = filtered.id
165+
ORDER BY
166+
coalesce(1.0 / (50 + fuzzy.rank_ix), 0.0) * 1 +
167+
coalesce(1.0 / (50 + full_text.rank_ix), 0.0) * 1 +
168+
coalesce(1.0 / (50 + partial_search.rank_ix), 0.0) * 1
169+
DESC
170+
) SELECT {search_qp_fields} FROM result",
171+
search_qp_fields = SEARCH_QP_FIELDS,
172+
to_tsquery = "to_tsquery('simple', websearch_to_tsquery('simple', $1)::text || ':*')",
173+
exam_filter = exam_filter,
174+
intermediate_fields = ADMIN_DASHBOARD_QP_FIELDS.split(", ").map(|field| format!("filtered.{}", field)).collect::<Vec<String>>().join(", ")
122175
),
123-
result AS (
124-
SELECT {intermediate_fields}
125-
FROM fuzzy
126-
FULL OUTER JOIN full_text ON fuzzy.id = full_text.id
127-
FULL OUTER JOIN partial_search ON coalesce(fuzzy.id, full_text.id) = partial_search.id
128-
JOIN filtered ON coalesce(fuzzy.id, full_text.id, partial_search.id) = filtered.id
129-
ORDER BY
130-
coalesce(1.0 / (50 + fuzzy.rank_ix), 0.0) * 1 +
131-
coalesce(1.0 / (50 + full_text.rank_ix), 0.0) * 1 +
132-
coalesce(1.0 / (50 + partial_search.rank_ix), 0.0) * 1
133-
DESC
134-
) SELECT {search_qp_fields} FROM result",
135-
search_qp_fields = SEARCH_QP_FIELDS,
136-
to_tsquery = "to_tsquery('simple', websearch_to_tsquery('simple', $1)::text || ':*')",
137-
exam_filter = exam_filter,
138-
intermediate_fields = ADMIN_DASHBOARD_QP_FIELDS.split(", ").map(|field| format!("filtered.{}", field)).collect::<Vec<String>>().join(", ")
176+
use_exam_arg
139177
)
140178
}
141179

backend/src/routing/handlers.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,32 @@ pub async fn search(
6363
Query(params): Query<HashMap<String, String>>,
6464
) -> HandlerReturn<Vec<qp::SearchQP>> {
6565
let response = if let Some(query) = params.get("query") {
66-
let exam = params.get("exam").map(qp::Exam::try_from).transpose()?;
67-
68-
let papers = state.db.search_papers(query, exam).await?;
69-
70-
let papers = papers
71-
.iter()
72-
.map(|paper| paper.clone().with_url(&state.env_vars))
73-
.collect::<Result<Vec<qp::SearchQP>, color_eyre::eyre::Error>>()?;
74-
75-
Ok(BackendResponse::ok(
76-
format!("Successfully fetched {} papers.", papers.len()),
77-
papers,
78-
))
66+
let exam_query_str = params
67+
.get("exam")
68+
.map(|value| value.to_owned())
69+
.unwrap_or("".into());
70+
71+
if let Ok(exam_filter) = (&exam_query_str).try_into() {
72+
let papers = state
73+
.db
74+
.search_papers(query, exam_filter, exam_query_str.to_owned())
75+
.await?;
76+
77+
let papers = papers
78+
.iter()
79+
.map(|paper| paper.clone().with_url(&state.env_vars))
80+
.collect::<Result<Vec<qp::SearchQP>, color_eyre::eyre::Error>>()?;
81+
82+
Ok(BackendResponse::ok(
83+
format!("Successfully fetched {} papers.", papers.len()),
84+
papers,
85+
))
86+
} else {
87+
Ok(BackendResponse::error(
88+
"Invalid `exam` URL parameter.".into(),
89+
StatusCode::BAD_REQUEST,
90+
))
91+
}
7992
} else {
8093
Ok(BackendResponse::error(
8194
"`query` URL parameter is required.".into(),

frontend/src/components/Search/SearchForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ function CourseSearchForm() {
9393
id="exam"
9494
options={[
9595
{ value: '', title: 'Mid / End Semester / Class Test' },
96-
// { value: 'midend', title: 'Mid / End Semester' },
96+
{ value: 'midend', title: 'Mid / End Semester' },
9797
{ value: 'midsem', title: 'Mid Semester' },
9898
{ value: 'endsem', title: 'End Semester' },
99-
// { value: 'ct', title: 'Class Test' }
99+
{ value: 'ct', title: 'Class Test' }
100100
]}
101101
value={exam}
102102
onInput={(e) => setExam(e.currentTarget.value as Exam)}

0 commit comments

Comments
 (0)