Skip to content
94 changes: 71 additions & 23 deletions apps/labrinth/src/routes/v3/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use eyre::eyre;
use futures::TryStreamExt;
use itertools::Itertools;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use validator::Validate;

Expand Down Expand Up @@ -94,11 +95,15 @@
pub struct RandomProjects {
#[validate(range(min = 1, max = 100))]
pub count: u32,
pub project_type: Option<String>,
}

#[utoipa::path(
tag = "projects",
params(("count" = u32, Query)),
params(
("count" = u32, Query),
("project_type" = Option<String>, Query),
),
responses((status = OK))
)]
#[get("/projects_random")]
Expand All @@ -110,36 +115,79 @@
random_projects_get(count, pool, redis).await
}

// Filtered candidates are sparser and unevenly spaced, so the nearest-point pick
// tends to repeat; oversample a neighborhood and shuffle it down to counter that.
const RANDOM_PROJECT_TYPE_OVERSAMPLE_FACTOR: u32 = 20;

pub async fn random_projects_get(
web::Query(count): web::Query<RandomProjects>,
web::Query(params): web::Query<RandomProjects>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
count.validate().map_err(|err| {
params.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;

let project_ids = sqlx::query!(
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
// number of mods nearest to a random point in the ID space is equivalent to
// random sampling
"WITH random_id_point AS (
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
let statuses = crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>();

let mut project_ids = if let Some(project_type) = &params.project_type {
let fetch_limit = params.count * RANDOM_PROJECT_TYPE_OVERSAMPLE_FACTOR;

sqlx::query!(

Check failure on line 139 in apps/labrinth/src/routes/v3/projects.rs

View workflow job for this annotation

GitHub Actions / docker

`SQLX_OFFLINE=true` but there is no cached data for this query, run `cargo sqlx prepare` to update the query cache or unset `SQLX_OFFLINE`
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
// number of mods nearest to a random point in the ID space is equivalent to
// random sampling
"WITH random_id_point AS (
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
)
SELECT id FROM mods
WHERE status = ANY($1)
AND EXISTS (
SELECT 1 FROM versions v
INNER JOIN loaders_versions lv ON v.id = lv.version_id
INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = lv.loader_id
INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
WHERE v.mod_id = mods.id AND pt.name = $3
OFFSET 0
Comment thread
creeperkatze marked this conversation as resolved.
)
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
LIMIT $2",
&statuses,
fetch_limit as i32,
project_type,
)
SELECT id FROM mods
WHERE status = ANY($1)
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
LIMIT $2",
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
count.count as i32,
)
.fetch(&**pool)
.map_ok(|m| db_ids::DBProjectId(m.id))
.try_collect::<Vec<_>>()
.await?;
.fetch(&**pool)
.map_ok(|m| db_ids::DBProjectId(m.id))
.try_collect::<Vec<_>>()
.await?
} else {
sqlx::query!(

Check failure on line 167 in apps/labrinth/src/routes/v3/projects.rs

View workflow job for this annotation

GitHub Actions / docker

`SQLX_OFFLINE=true` but there is no cached data for this query, run `cargo sqlx prepare` to update the query cache or unset `SQLX_OFFLINE`
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
// number of mods nearest to a random point in the ID space is equivalent to
// random sampling
"WITH random_id_point AS (
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
)
SELECT id FROM mods
WHERE status = ANY($1)
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
LIMIT $2",
&statuses,
params.count as i32,
)
.fetch(&**pool)
.map_ok(|m| db_ids::DBProjectId(m.id))
.try_collect::<Vec<_>>()
.await?
};

if params.project_type.is_some() {
project_ids.shuffle(&mut rand::thread_rng());
project_ids.truncate(params.count as usize);
}

let projects_data =
db_models::DBProject::get_many_ids(&project_ids, &**pool, &redis)
Expand Down
Loading