From 64e17c7c1b17bbb32edaed73b737034fa3d92ce5 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Thu, 11 Jun 2026 21:18:44 +0100 Subject: [PATCH] Include compatible dependencies field in project search (#6366) * include compatible dependency projects field * fix --- ...9056ac842c402bd0941c45ebd1dc6d627d43.json} | 14 +++- .../src/search/backend/typesense/mod.rs | 7 ++ apps/labrinth/src/search/indexing.rs | 81 ++++++++++++------- apps/labrinth/src/search/mod.rs | 9 +++ apps/labrinth/tests/search.rs | 19 +++++ 5 files changed, 96 insertions(+), 34 deletions(-) rename apps/labrinth/.sqlx/{query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json => query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json} (53%) diff --git a/apps/labrinth/.sqlx/query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json b/apps/labrinth/.sqlx/query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json similarity index 53% rename from apps/labrinth/.sqlx/query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json rename to apps/labrinth/.sqlx/query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json index 5d7941bfe..694308e56 100644 --- a/apps/labrinth/.sqlx/query-20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27.json +++ b/apps/labrinth/.sqlx/query-3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT v.mod_id dependent_project_id, d.mod_dependency_id dependency_project_id,\n m.name dependency_name, m.slug dependency_slug, m.icon_url dependency_icon_url\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE v.mod_id = ANY($1)\n AND d.mod_dependency_id IS NOT NULL\n AND m.status = ANY($2)\n ", + "query": "\n SELECT DISTINCT v.mod_id dependent_project_id,\n d.mod_dependency_id dependency_project_id,\n d.dependency_type dependency_type,\n m.name dependency_name,\n m.slug dependency_slug,\n m.icon_url dependency_icon_url\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE v.mod_id = ANY($1)\n AND d.mod_dependency_id IS NOT NULL\n AND m.status = ANY($2)\n ", "describe": { "columns": [ { @@ -15,16 +15,21 @@ }, { "ordinal": 2, - "name": "dependency_name", + "name": "dependency_type", "type_info": "Varchar" }, { "ordinal": 3, - "name": "dependency_slug", + "name": "dependency_name", "type_info": "Varchar" }, { "ordinal": 4, + "name": "dependency_slug", + "type_info": "Varchar" + }, + { + "ordinal": 5, "name": "dependency_icon_url", "type_info": "Varchar" } @@ -39,9 +44,10 @@ false, true, false, + false, true, true ] }, - "hash": "20cb8a3d45911a126aa17451b1e6e84e9238dee422cc2b400cea0048a71e4c27" + "hash": "3afdd6b9070ea0951682facf207b9056ac842c402bd0941c45ebd1dc6d627d43" } diff --git a/apps/labrinth/src/search/backend/typesense/mod.rs b/apps/labrinth/src/search/backend/typesense/mod.rs index 5959d9578..750458714 100644 --- a/apps/labrinth/src/search/backend/typesense/mod.rs +++ b/apps/labrinth/src/search/backend/typesense/mod.rs @@ -485,6 +485,13 @@ impl SearchField { sort: false, optional: true, }, + SearchField::CompatibleDependencyProjectIds => TypesenseFieldSpec { + path: "compatible_dependency_project_ids", + ty: "string[]", + facet: true, + sort: false, + optional: true, + }, } } } diff --git a/apps/labrinth/src/search/indexing.rs b/apps/labrinth/src/search/indexing.rs index 314f841f9..96793510f 100644 --- a/apps/labrinth/src/search/indexing.rs +++ b/apps/labrinth/src/search/indexing.rs @@ -21,7 +21,7 @@ use crate::database::models::{ use crate::database::redis::RedisPool; use crate::models::exp; use crate::models::ids::ProjectId; -use crate::models::projects::from_duplicate_version_fields; +use crate::models::projects::{DependencyType, from_duplicate_version_fields}; use crate::models::v2::projects::LegacyProject; use crate::routes::v2_reroute; use crate::search::{SearchProjectDependency, UploadSearchProject}; @@ -124,10 +124,14 @@ pub async fn index_local( info!("Indexing local dependencies!"); let dependencies: DashMap> = - sqlx::query!( - " - SELECT DISTINCT v.mod_id dependent_project_id, d.mod_dependency_id dependency_project_id, - m.name dependency_name, m.slug dependency_slug, m.icon_url dependency_icon_url + sqlx::query!( + " + SELECT DISTINCT v.mod_id dependent_project_id, + d.mod_dependency_id dependency_project_id, + d.dependency_type dependency_type, + m.name dependency_name, + m.slug dependency_slug, + m.icon_url dependency_icon_url FROM versions v INNER JOIN dependencies d ON d.dependent_id = v.id INNER JOIN mods m ON m.id = d.mod_dependency_id @@ -135,32 +139,35 @@ pub async fn index_local( AND d.mod_dependency_id IS NOT NULL AND m.status = ANY($2) ", - &project_ids, - &searchable_statuses, - ) - .fetch(pool) - .try_fold( - DashMap::new(), - |acc: DashMap>, m| { - if let Some(dependency_project_id) = m.dependency_project_id { - acc.entry(DBProjectId(m.dependent_project_id)) - .or_default() - .push(SearchProjectDependency { - project_id: ProjectId::from(DBProjectId( - dependency_project_id, - )) - .to_string(), - name: m.dependency_name, - slug: m.dependency_slug, - icon_url: m.dependency_icon_url, - }); - } + &project_ids, + &searchable_statuses, + ) + .fetch(pool) + .try_fold( + DashMap::new(), + |acc: DashMap>, m| { + if let Some(dependency_project_id) = m.dependency_project_id { + acc.entry(DBProjectId(m.dependent_project_id)) + .or_default() + .push(SearchProjectDependency { + project_id: ProjectId::from(DBProjectId( + dependency_project_id, + )) + .to_string(), + dependency_type: DependencyType::from_string( + &m.dependency_type, + ), + name: m.dependency_name, + slug: m.dependency_slug, + icon_url: m.dependency_icon_url, + }); + } - async move { Ok(acc) } - }, - ) - .await - .wrap_err("failed to fetch project dependencies")?; + async move { Ok(acc) } + }, + ) + .await + .wrap_err("failed to fetch project dependencies")?; struct PartialGallery { url: String, @@ -398,6 +405,18 @@ pub async fn index_local( .iter() .map(|dependency| dependency.project_id.clone()) .collect::>(); + let compatible_dependency_project_ids = dependencies + .iter() + .filter(|dependency| { + matches!( + dependency.dependency_type, + DependencyType::Required + | DependencyType::Optional + | DependencyType::Embedded + ) + }) + .map(|dependency| dependency.project_id.clone()) + .collect::>(); if let Some(versions) = versions.remove(&project.id) { // Aggregated project loader fields @@ -539,6 +558,8 @@ pub async fn index_local( open_source, color: project.color.map(|x| x as u32), dependency_project_ids: dependency_project_ids.clone(), + compatible_dependency_project_ids: + compatible_dependency_project_ids.clone(), dependencies: dependencies.clone(), loader_fields, project_loader_fields: project_loader_fields.clone(), diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 90c0f9e3c..0f46a8111 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -2,6 +2,7 @@ use crate::database::redis::RedisPool; use crate::models::exp; use crate::models::exp::minecraft::JavaServerPing; use crate::models::ids::{ProjectId, VersionId}; +use crate::models::projects::DependencyType; use crate::queue::server_ping; use crate::routes::ApiError; use crate::{database::PgPool, env::ENV}; @@ -196,6 +197,7 @@ pub enum SearchField { MinecraftJavaServerContentSupportedGameVersions, MinecraftJavaServerPingData, DependencyProjectIds, + CompatibleDependencyProjectIds, } #[derive(Debug, Error)] @@ -252,6 +254,8 @@ pub struct UploadSearchProject { #[serde(default)] pub dependency_project_ids: Vec, #[serde(default)] + pub compatible_dependency_project_ids: Vec, + #[serde(default)] pub dependencies: Vec, // Hidden fields to get the Project model out of the search results. @@ -267,6 +271,7 @@ pub struct UploadSearchProject { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SearchProjectDependency { pub project_id: String, + pub dependency_type: DependencyType, pub name: String, pub slug: Option, pub icon_url: Option, @@ -311,6 +316,8 @@ pub struct ResultSearchProject { #[serde(default)] pub dependency_project_ids: Vec, #[serde(default)] + pub compatible_dependency_project_ids: Vec, + #[serde(default)] pub dependencies: Vec, // Hidden fields to get the Project model out of the search results. @@ -350,6 +357,8 @@ impl From for ResultSearchProject { featured_gallery: source.featured_gallery, color: source.color, dependency_project_ids: source.dependency_project_ids, + compatible_dependency_project_ids: source + .compatible_dependency_project_ids, dependencies: source.dependencies, loaders: source.loaders, project_loader_fields: source.project_loader_fields, diff --git a/apps/labrinth/tests/search.rs b/apps/labrinth/tests/search.rs index 9bcba177c..c009aa929 100644 --- a/apps/labrinth/tests/search.rs +++ b/apps/labrinth/tests/search.rs @@ -9,6 +9,7 @@ use common::environment::TestEnvironment; use common::environment::with_test_environment; use common::search::setup_search_projects; use futures::stream::StreamExt; +use labrinth::models::projects::DependencyType; use serde_json::json; use crate::common::api_common::Api; @@ -95,6 +96,12 @@ async fn search_projects() { )]]), vec![7], ), + ( + json!([[format!( + "compatible_dependency_project_ids:{dependency_project_id}" + )]]), + vec![7], + ), ]; // TODO: versions, game versions // Untested: @@ -151,11 +158,23 @@ async fn search_projects() { projects.hits[0].dependency_project_ids[0], dependency_project_id ); + assert_eq!( + projects.hits[0].compatible_dependency_project_ids.len(), + 1 + ); + assert_eq!( + projects.hits[0].compatible_dependency_project_ids[0], + dependency_project_id + ); assert_eq!(projects.hits[0].dependencies.len(), 1); assert_eq!( projects.hits[0].dependencies[0].project_id, dependency_project_id ); + assert_eq!( + projects.hits[0].dependencies[0].dependency_type, + DependencyType::Required + ); assert!( projects.hits[0].dependencies[0] .slug