Skip to content

Commit

Permalink
Move filtering to backend & use URL search params
Browse files Browse the repository at this point in the history
For now, the filters are hidden behind the `experimentalFeatures`
flag. They are to be extended and or redone in the future, but
the aim of this commit is to lay the foundation for that.
  • Loading branch information
owi92 committed Feb 22, 2024
1 parent 0efd765 commit 7960477
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 99 deletions.
39 changes: 36 additions & 3 deletions backend/src/api/model/search/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use chrono::Utc;
use chrono::{ NaiveDate, Utc};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{fmt, borrow::Cow};
Expand Down Expand Up @@ -114,12 +114,17 @@ pub(crate) use handle_search_result;
/// Main entry point for the main search (including all items).
pub(crate) async fn perform(
user_query: &str,
type_filter: &str,
start: &str,
end: &str,
context: &Context,
) -> ApiResult<SearchOutcome> {
if user_query.is_empty() {
return Ok(SearchOutcome::EmptyQuery(EmptyQuery));
}

println!("{:?} {:?} {:?}", type_filter, start, end);

// Search for opencastId if applicable
let uuid_query = user_query.trim();
if looks_like_opencast_uuid(&uuid_query) {
Expand All @@ -137,6 +142,19 @@ pub(crate) async fn perform(
return Ok(SearchOutcome::Results(SearchResults { items, total_hits }));
}

// I'm sure there's a better way to do this.
fn timestamp(time_string: &str, fallback: &str) -> i64 {
let valid_time_string = if time_string.is_empty() {
fallback
} else {
time_string
};
NaiveDate::parse_from_str(valid_time_string, "%Y-%m-%d")
.unwrap_or(NaiveDate::parse_from_str(fallback, "%Y-%m-%d").unwrap())
.and_hms_opt(0, 0, 0)
.unwrap()
.timestamp()
}

// Prepare the event search
let filter = Filter::And(
Expand All @@ -147,6 +165,8 @@ pub(crate) async fn perform(
Filter::Leaf("is_live = false ".into()),
Filter::Leaf(format!("end_time_timestamp >= {}", Utc::now().timestamp()).into()),
].into())])
.chain(std::iter::once(Filter::Leaf(format!("created_timestamp >= {}", timestamp(start, "0-1-1")).into())))
.chain(std::iter::once(Filter::Leaf(format!("created_timestamp <= {}", timestamp(end, "9999-12-31")).into())))
.collect()
).to_string();
let mut event_query = context.search.event_index.search();
Expand Down Expand Up @@ -179,15 +199,28 @@ pub(crate) async fn perform(

// Merge results according to Meilis score.
//
// TODO: Comparing scores of differen indices is not well defined right now.
// TODO: Comparing scores of different indices is not well defined right now.
// We can either use score details or adding dummy searchable fields to the
// realm index. See this discussion for more info:
// https://github.com/orgs/meilisearch/discussions/489#discussioncomment-6160361
let events = event_results.hits.into_iter()
.map(|result| (NodeValue::from(result.result), result.ranking_score));
let realms = realm_results.hits.into_iter()
.map(|result| (NodeValue::from(result.result), result.ranking_score));
let mut merged = realms.chain(events).collect::<Vec<_>>();
let mut merged: Vec<(NodeValue, Option<f64>)> = Vec::new();
// Probably also not the best way of doing this.
match type_filter {
"videos" => {
merged.extend(events.into_iter());
}
"pages" => {
merged.extend(realms.into_iter());
}
"all" | _ => {
merged.extend(events.into_iter());
merged.extend(realms.into_iter());
}
}
merged.sort_unstable_by(|(_, score0), (_, score1)| score1.unwrap().total_cmp(&score0.unwrap()));

let total_hits: usize = [event_results.estimated_total_hits, realm_results.estimated_total_hits]
Expand Down
4 changes: 2 additions & 2 deletions backend/src/api/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ impl Query {
}

/// Returns `null` if the query is too short.
async fn search(query: String, context: &Context) -> ApiResult<SearchOutcome> {
search::perform(&query, context).await
async fn search(query: String, filter: String, start: String, end: String, context: &Context) -> ApiResult<SearchOutcome> {
search::perform(&query, &filter, &start, &end, context).await
}

/// Searches through all events that the user has write access to
Expand Down
7 changes: 5 additions & 2 deletions backend/src/search/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub(crate) struct Event {
pub(crate) thumbnail: Option<String>,
pub(crate) duration: i64,
pub(crate) created: DateTime<Utc>,
pub(crate) created_timestamp: i64,
pub(crate) start_time: Option<DateTime<Utc>>,
pub(crate) end_time: Option<DateTime<Utc>>,
pub(crate) end_time_timestamp: Option<i64>,
Expand Down Expand Up @@ -67,6 +68,7 @@ impl_from_db!(
|row| {
let host_realms = row.host_realms::<Vec<Realm>>();
let end_time = row.end_time();
let created = row.created();
Self {
id: row.id(),
series_id: row.series(),
Expand All @@ -78,7 +80,8 @@ impl_from_db!(
duration: row.duration(),
is_live: row.is_live(),
audio_only: row.audio_only(),
created: row.created(),
created,
created_timestamp: created.timestamp(),
start_time: row.start_time(),
end_time,
end_time_timestamp: end_time.map(|date_time| date_time.timestamp()),
Expand Down Expand Up @@ -116,6 +119,6 @@ pub(super) async fn prepare_index(index: &Index) -> Result<()> {
index,
"event",
&["title", "creators", "description", "series_title"],
&["listed", "read_roles", "write_roles", "is_live", "end_time_timestamp"],
&["listed", "read_roles", "write_roles", "is_live", "end_time_timestamp", "created_timestamp"],
).await
}
18 changes: 13 additions & 5 deletions frontend/src/layout/header/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,21 @@ export const SearchField: React.FC<SearchFieldProps> = ({ variant }) => {
useEffect(() => () => clearTimeout(lastTimeout.current));

const onSearchRoute = isSearchActive();
const defaultValue = onSearchRoute
? new URLSearchParams(document.location.search).get("q") ?? undefined
: undefined;
const getSearchParam = (searchParameter: string) => {
const searchParams = new URLSearchParams(document.location.search);
return onSearchRoute
? searchParams.get(searchParameter) ?? undefined
: undefined;
};
const defaultValue = getSearchParam("q");


const search = useCallback(debounce((expression: string) => {
router.goto(SearchRoute.url({ query: expression }), onSearchRoute);
}, 300), []);
const filter = getSearchParam("f");
const start = getSearchParam("start");
const end = getSearchParam("end");
router.goto(SearchRoute.url({ query: expression, filter, start, end }), onSearchRoute);
}, 250), []);

return (
<div css={{
Expand Down
Loading

0 comments on commit 7960477

Please sign in to comment.