Skip to content
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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