Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support searching and sorting experiments via API and sorting on UI #280

Merged
merged 2 commits into from
Dec 11, 2024

Conversation

Datron
Copy link
Collaborator

@Datron Datron commented Nov 8, 2024

Problem

It is difficult to find experiments on different filters and sort them

Solution

  • Provide an API interface that allows a user to search and sort experiments based on multiple parameters.
  • Add support in the UI to sort columns, and in experiments listing add the ability to sort on last_modified_at and created_at

PS: more frontend changes are to be expected to support searching. That is NOT present in this PR.

Screen.Recording.2024-11-08.at.2.35.02.PM.mov

@Datron Datron requested a review from a team as a code owner November 8, 2024 09:08
@Datron Datron force-pushed the search-experiments branch 2 times, most recently from c666640 to 523a478 Compare November 18, 2024 06:41
@Datron Datron mentioned this pull request Nov 20, 2024

impl Default for SortBy {
fn default() -> Self {
Self::Desc
Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel default should be Asc

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@pratikmishra356 you always want to see the latest data, especially if you've been using Superposition for years.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Datron but it can differ based on entity , position/weight needs to be sort in asc order

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You can set it in the sort function based on needs, don't need to call default

}
if let Some(ref context_search) = filters.context {
builder = builder.filter(
sql::<Bool>("context::text LIKE ")
Copy link
Collaborator

Choose a reason for hiding this comment

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

are we type casting here ? context

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes we are, this just lets me modify the context type so I can use LIKE on it. It's currently an experiment to see if this is a viable way to search through contexts

all_contexts = all_contexts
.into_iter()
.filter_map(|mut context| {
Context::filter_keys_by_prefix(&context, &prefix_list)
Copy link
Collaborator

Choose a reason for hiding this comment

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

@Datron filter_keys_by_prefix uses try_from which results in lots of error logs
2024-11-25T07:06:27Z ERROR superposition_types::config] Override validation error: Override is empty
Should we instead check if map is empty and then probably try_from

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@pratikmishra356 yes we can, but that is out of scope for this PR

@@ -633,56 +633,54 @@ async fn list_contexts(
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we separate Asc, Desc like you did in experiment/list ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What would be the benefit? Could you share an example?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Datron Datron force-pushed the search-experiments branch 2 times, most recently from 94e1bd4 to 4a57a01 Compare November 26, 2024 10:48
Comment on lines 202 to 209
#[derive(Debug, Deserialize, Clone, Deref)]
#[deref(forward)]
pub struct StringArgs(
#[serde(deserialize_with = "deserialize_stringified_list")] pub Vec<String>,
);

pub fn deserialize_stringified_list<'de, D, I>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we instead implement the Deserialize trait on StringArgs and write the logic in it instead of writing this as a function

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried Deserialize, it was more verbose, and all examples I found did it this way. Can we continue with this method for now?

Copy link
Collaborator

Choose a reason for hiding this comment

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

docker-compose/postgres/Dockerfile Show resolved Hide resolved
}
if let Some(ref context_search) = filters.context {
builder = builder.filter(
sql::<Bool>("context::text LIKE ")
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is this query doing here ?

crates/frontend/src/components/table.rs Outdated Show resolved Hide resolved
@@ -122,10 +126,71 @@ pub struct ExperimentsResponse {
pub struct StatusTypes(pub Vec<ExperimentStatusType>);

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExpListFilters {
pub struct ExperimentListFilters {
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this type not there in experimentation_platform for the api type ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is present, do you recommend moving this to superposition_types?

Copy link
Collaborator

Choose a reason for hiding this comment

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

its good to have, or we can wait till this PR goes in:
#291

after this PR is merged, I would raise a separate PR for moving the api types to superposition_types probably we can move it then

Comment on lines 203 to 245
#[derive(Debug, Deserialize, Clone, Deref)]
#[deref(forward)]
pub struct StringArgs(
#[serde(deserialize_with = "deserialize_stringified_list")] pub Vec<String>,
);

pub fn deserialize_stringified_list<'de, D, I>(
deserializer: D,
) -> std::result::Result<Vec<I>, D::Error>
where
D: de::Deserializer<'de>,
I: de::DeserializeOwned,
{
struct StringVecVisitor<I>(std::marker::PhantomData<I>);

impl<'de, I> de::Visitor<'de> for StringVecVisitor<I>
where
I: de::DeserializeOwned,
{
type Value = Vec<I>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"a string containing comma separated values eg: CREATED,INPROGRESS",
)
}

fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
let mut query_vector = Vec::new();
for param in v.split(',') {
let p: I = I::deserialize(param.into_deserializer())?;
query_vector.push(p);
}
Ok(query_vector)
}
}

deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::<I>))
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

this should work, right ?

Suggested change
#[derive(Debug, Deserialize, Clone, Deref)]
#[deref(forward)]
pub struct StringArgs(
#[serde(deserialize_with = "deserialize_stringified_list")] pub Vec<String>,
);
pub fn deserialize_stringified_list<'de, D, I>(
deserializer: D,
) -> std::result::Result<Vec<I>, D::Error>
where
D: de::Deserializer<'de>,
I: de::DeserializeOwned,
{
struct StringVecVisitor<I>(std::marker::PhantomData<I>);
impl<'de, I> de::Visitor<'de> for StringVecVisitor<I>
where
I: de::DeserializeOwned,
{
type Value = Vec<I>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"a string containing comma separated values eg: CREATED,INPROGRESS",
)
}
fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
let mut query_vector = Vec::new();
for param in v.split(',') {
let p: I = I::deserialize(param.into_deserializer())?;
query_vector.push(p);
}
Ok(query_vector)
}
}
deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::<I>))
}
#[derive(Debug, Clone, Deref)]
#[deref(forward)]
pub struct StringArgs(pub Vec<String>);
impl<'de> Deserialize<'de> for StringArgs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let items = s.split(',').map(|item| item.trim().to_string()).collect();
Ok(Self(items))
}
}```

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the case you present, if the user passes a wrong value then there is no error, which I want.

Copy link
Collaborator

Choose a reason for hiding this comment

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

this does return an error, if the data passed is not a string, which is more than enough to determine if the value is right or not
as the data in it must contain is not mandatory, because there can always be a single element in the array which does not require any sorts of delimiter

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@ayushjain17 I tried this, but this function is used to deserialize a lot of other vectors and type cast them. I don't want to manually impl deserialize on all of them so I won't change this for now.

@Datron Datron force-pushed the search-experiments branch from bc964e7 to e2ee925 Compare December 3, 2024 12:47
@Datron Datron requested a review from ayushjain17 December 3, 2024 12:52
crates/frontend/src/types.rs Outdated Show resolved Hide resolved
crates/frontend/src/types.rs Outdated Show resolved Hide resolved
)]
#[serde(rename_all = "lowercase")]
pub enum SortBy {
#[display(fmt = "desc")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is a dedicated how to display definition required, rename_all doesn't handle this case too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

rename_all is for deserialization, the to_string representation would be different me thinks

Comment on lines +21 to +24
#[strum(to_string = "last_modified_at")]
LastModifiedAt,
#[strum(to_string = "created_at")]
CreatedAt,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This also won't rename_all handle the case of to_string which is just the snake_case repr.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Didn't get you

Copy link
Collaborator

Choose a reason for hiding this comment

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

he meant why do we need strum to_string here, is serde rename_all not enough here ?
just like here

crates/frontend/src/types.rs Outdated Show resolved Hide resolved
pub enum ColumnSortable {
Yes {
sort_fn: Callback<()>,
sort_by: SortBy,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
sort_by: SortBy,
order: Order,

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No

crates/frontend/src/components/table/types.rs Outdated Show resolved Hide resolved
crates/frontend/src/components/table.rs Outdated Show resolved Hide resolved
Comment on lines +125 to +126
filters.to_string(),
pagination
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we make it consistent over here, I got confused that how are these two not having the exact same syntax

Suggested change
filters.to_string(),
pagination
filters.to_string(),
pagination.to_string()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You won't get confused if you see the whole function.

Comment on lines +122 to +127
format!(
"{}/experiments?{}&{}",
host,
filters.to_string(),
pagination
)
Copy link
Collaborator

@ayushjain17 ayushjain17 Dec 10, 2024

Choose a reason for hiding this comment

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

this does not seem correct, if someone does not send filters, the url will become:
base_url/experimnets?&pagi_filt1=abc

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are always date filters

Copy link
Collaborator

Choose a reason for hiding this comment

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

that is not being guaranteed by the type
all fields in ExperimentListFilters are optional, so one can simply avoid sending any params and then it breaks over here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is guaranteed by code,

from_date: Utc.timestamp_opt(0, 0).single(),
        to_date: Utc.timestamp_opt(4130561031, 0).single(),

https://github.com/juspay/superposition/pull/280/files/64469a660bb91eabe003b378e3a0ec64d2de3961#diff-a26bd230250ef10be78fd6369216b71e556f5b808a9cd12e2be3b6551c6e8cbbR41

Doesn't need to be guaranteed by the type

Comment on lines +94 to +97
let value: String = row
.get(cname)
.unwrap_or(&Value::String(String::new()))
.html_display();
Copy link
Collaborator

@ayushjain17 ayushjain17 Dec 10, 2024

Choose a reason for hiding this comment

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

can we rather do something like this:

Suggested change
let value: String = row
.get(cname)
.unwrap_or(&Value::String(String::new()))
.html_display();
let value = row.get(cname).map(|v| v.html_display()).unwrap_or_default();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why the difference?

Copy link
Collaborator

Choose a reason for hiding this comment

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

its better memory wise, right
in the default case we just need a String, and thats what would be get directly

else, currently in default case, you are getting a Value type which again is converted to String by html_display

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will look into that, can we take it as it is now?

@ayushjain17 ayushjain17 merged commit 95b87c5 into main Dec 11, 2024
4 checks passed
@ayushjain17 ayushjain17 deleted the search-experiments branch December 11, 2024 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants