From 2ed23089b75462f7d753d3dac889f4f3ed8af78c Mon Sep 17 00:00:00 2001 From: Hanbeom kim Date: Mon, 6 Nov 2023 13:10:27 +0900 Subject: [PATCH] Add tag/remark to `ranked outlier` search filters --- CHANGELOG.md | 2 + src/graphql/outlier.rs | 113 ++++++++++++++++++++++++++++++--- src/graphql/triage/response.rs | 9 +-- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0336602..bfeb180f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Changed - Change the type of `id` in `ranked_outlier`/`saved_outlier` queries to `StringNumber`. +- Modified Ranked Outliers graphql query to take in a SearchFilter with + `tag` and `remark` ## [0.14.5] - 2023-11-02 diff --git a/src/graphql/outlier.rs b/src/graphql/outlier.rs index 6c429f97..b5fd6c84 100644 --- a/src/graphql/outlier.rs +++ b/src/graphql/outlier.rs @@ -1,4 +1,9 @@ -use super::{always_true, model::ModelDigest, Role, RoleGuard, DEFAULT_CONNECTION_SIZE}; +use super::{ + always_true, + model::ModelDigest, + triage::response::{key, TriageResponse}, + Role, RoleGuard, DEFAULT_CONNECTION_SIZE, +}; use crate::graphql::{earliest_key, latest_key}; use anyhow::anyhow; use async_graphql::{ @@ -73,6 +78,8 @@ pub struct OutlierDistanceRange { pub struct SearchFilterInput { pub time: Option, distance: Option, + tag: Option, + remark: Option, } #[Object] @@ -632,9 +639,19 @@ async fn load_ranked_outliers_with_filter( let store = crate::graphql::get_store(ctx).await?; let map = store.outlier_map().into_prefix_map(&prefix); + let remarks_map = store.triage_response_map(); + let tags_map = store.event_tag_set(); - let (nodes, has_previous, has_next) = - load_nodes_with_search_filter(&map, &filter, after, before, first, last)?; + let (nodes, has_previous, has_next) = load_nodes_with_search_filter( + &map, + &remarks_map, + &tags_map, + &filter, + after, + before, + first, + last, + )?; let mut connection = Connection::with_additional_fields( has_previous, @@ -650,9 +667,11 @@ async fn load_ranked_outliers_with_filter( Ok(connection) } -#[allow(clippy::type_complexity)] // since this is called within `load` only +#[allow(clippy::type_complexity, clippy::too_many_arguments)] // since this is called within `load` only fn load_nodes_with_search_filter<'m, M, I>( map: &'m M, + remarks_map: &review_database::IndexedMap<'_>, + tags_map: &review_database::IndexedSet<'_>, filter: &Option, after: Option, before: Option, @@ -673,9 +692,25 @@ where let (nodes, has_more) = if let Some(after) = after { let to = earliest_key(&after)?; - iter_through_search_filter_nodes(iter, &to, cmp::Ordering::is_ge, filter, last) + iter_through_search_filter_nodes( + iter, + remarks_map, + tags_map, + &to, + cmp::Ordering::is_ge, + filter, + last, + ) } else { - iter_through_search_filter_nodes(iter, &[], always_true, filter, last) + iter_through_search_filter_nodes( + iter, + remarks_map, + tags_map, + &[], + always_true, + filter, + last, + ) }?; Ok((nodes, has_more, false)) } else { @@ -689,16 +724,35 @@ where let (nodes, has_more) = if let Some(before) = before { let to = latest_key(&before)?; - iter_through_search_filter_nodes(iter, &to, cmp::Ordering::is_le, filter, first) + iter_through_search_filter_nodes( + iter, + remarks_map, + tags_map, + &to, + cmp::Ordering::is_le, + filter, + first, + ) } else { - iter_through_search_filter_nodes(iter, &[], always_true, filter, first) + iter_through_search_filter_nodes( + iter, + remarks_map, + tags_map, + &[], + always_true, + filter, + first, + ) }?; Ok((nodes, false, has_more)) } } +#[allow(clippy::too_many_lines)] fn iter_through_search_filter_nodes( iter: I, + remarks_map: &review_database::IndexedMap<'_>, + tags_map: &review_database::IndexedSet<'_>, to: &[u8], cond: fn(cmp::Ordering) -> bool, filter: &Option, @@ -709,6 +763,29 @@ where { let mut nodes = Vec::new(); let mut exceeded = false; + + let tag_id_list = if let Some(filter) = filter { + if let Some(tag) = &filter.tag { + let index = tags_map.index()?; + let tag_ids: Vec = index + .iter() + .filter(|(_, name)| { + let name = String::from_utf8_lossy(name).into_owned(); + name.contains(tag) + }) + .map(|(id, _)| id) + .collect(); + if tag_ids.is_empty() { + return Ok((nodes, exceeded)); + } + Some(tag_ids) + } else { + None + } + } else { + None + }; + for (k, v) in iter { if !(cond)(k.as_ref().cmp(to)) { break; @@ -720,6 +797,26 @@ where }; if let Some(filter) = filter { + if filter.remark.is_some() || tag_id_list.is_some() { + let key = key(&node.source, Utc.timestamp_nanos(node.id.0)); + if let Some(value) = remarks_map.get_by_key(&key)? { + let value: TriageResponse = bincode::DefaultOptions::new() + .deserialize(value.as_ref()) + .map_err(|_| "invalid value in database")?; + if let Some(remark) = &filter.remark { + if !value.remarks.contains(remark) { + continue; + } + } + if let Some(tag_ids) = &tag_id_list { + if !tag_ids.iter().any(|tag| value.tag_ids.contains(tag)) { + continue; + } + } + } else { + continue; + } + } if let Some(time) = &filter.time { if let Some(start) = time.start { if let Some(end) = time.end { diff --git a/src/graphql/triage/response.rs b/src/graphql/triage/response.rs index 165b36aa..d1e44938 100644 --- a/src/graphql/triage/response.rs +++ b/src/graphql/triage/response.rs @@ -11,16 +11,17 @@ use review_database::{ }; use serde::{Deserialize, Serialize}; +#[allow(clippy::module_name_repetitions)] #[derive(Deserialize, Serialize, SimpleObject)] #[graphql(complex)] -pub(super) struct TriageResponse { +pub struct TriageResponse { #[graphql(skip)] id: u32, key: Vec, source: String, time: DateTime, - tag_ids: Vec, - remarks: String, + pub tag_ids: Vec, + pub remarks: String, creation_time: DateTime, last_modified_time: DateTime, } @@ -134,7 +135,7 @@ async fn load( >(&map, after, before, first, last, TriageResponseTotalCount) } -fn key(source: &str, time: DateTime) -> Vec { +pub fn key(source: &str, time: DateTime) -> Vec { let mut key = Vec::new(); key.extend_from_slice(source.as_bytes()); key.extend_from_slice(&time.timestamp_nanos_opt().unwrap_or_default().to_be_bytes());