Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
22be5a9
initial elasticsearch impl
aecsocket Feb 12, 2026
31362c2
working elastic cluster
aecsocket Feb 12, 2026
d3c76fa
replace SearchError with ApiError for preparation of search backend
aecsocket Feb 19, 2026
da44fe9
start factoring meili out to trait
aecsocket Feb 13, 2026
1d17f61
move meili to backend
aecsocket Feb 13, 2026
5e112e9
update routes to use search backend trait
aecsocket Feb 13, 2026
4ffafc1
wip
aecsocket Feb 23, 2026
71e3256
Update projects.rs
aecsocket Feb 23, 2026
badb907
search backend is only init'd once in config
aecsocket Feb 15, 2026
72eb548
wip
aecsocket Feb 16, 2026
def1f6a
wip: backend agnostic
aecsocket Feb 16, 2026
9ccc391
change search internal routes to delegate to backend
aecsocket Feb 19, 2026
bf1f64d
initial elasticsearch impl
aecsocket Feb 19, 2026
e72e2fb
fix filtering
aecsocket Feb 19, 2026
495f6be
elastic impl
aecsocket Feb 19, 2026
e4c4691
refactor indexing into its own module
aecsocket Feb 19, 2026
2a8bed1
clean up elastic code
aecsocket Feb 19, 2026
8281def
fix ci
aecsocket Feb 19, 2026
9d38af8
fix tests
aecsocket Feb 19, 2026
58f432b
fix elastic health check
aecsocket Feb 19, 2026
74f350f
fix up env rebase
aecsocket Feb 19, 2026
77383fb
fix compile
aecsocket Feb 19, 2026
0520360
dummy commit to update github pr
aecsocket Feb 19, 2026
347de69
Fix rebase
aecsocket Feb 23, 2026
225c227
Elastic basic https auth
aecsocket Feb 23, 2026
0a3414e
Fix duplicate projects showing up
aecsocket Feb 23, 2026
7ba4dc1
Fix up tests
aecsocket Feb 24, 2026
04a46e6
Replace search `ApiErrors` with `eyre::Reports`, propagate background…
aecsocket Feb 24, 2026
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
63 changes: 63 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Architecture

## Frontend

Use TAB instead of spaces (frontend only).

There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).

Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.

Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`.

Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`.

Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework.

### Website (apps/frontend)

Before a pull request can be opened for the website, run `pnpm prepr:frontend:web` from the root folder, otherwise CI will fail.

To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder.

### App Frontend (apps/app-frontend)

Before a pull request can be opened for the app frontend, run `pnpm prepr:frontend:app` from the root folder, otherwise CI will fail.

To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder.

### Localization

Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages.

## Labrinth

Labrinth is the backend API service for Modrinth. It can be ran by changing directory into `apps/labrinth` and running `cargo run -p labrinth`. This MUST be run in the `apps/labrinth` directory, as otherwise the `.env` will not be read. If some environment variables are missing, their default values can be found in `.env.local`. The backend runs on `localhost:8000` by default - you can `curl http://localhost:8000` to test requests.

### Testing

Before a pull request can be opened, run `cargo clippy -p labrinth --all-targets` and make sure there are ZERO warnings, otherwise CI will fail.

Use `cargo test -p labrinth --all-targets` to test your changes. All tests must pass, otherwise CI will fail.

To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`. Make sure to NEVER run `cargo sqlx prepare --workspace`.

Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` or `podman exec` to access these services.

When the user refers to "performing pre-PR checks", do the following:

- Run clippy as described above
- DO NOT run tests unless explicitly requested (they take a long time)
- Prepare the sqlx cache

### Clickhouse

Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse instance. We use the `staging_ariadne` database to store data in testing.

### Postgres

Use `docker exec labrinth-postgres psql -U labrinth -d labrinth -c "SELECT 1"` to access the PostgreSQL instance, replacing the `SELECT 1` with your query.

# Guidelines

- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to.
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Architecture

Use TAB instead of spaces.

## Frontend

Use TAB instead of spaces (frontend only).

There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).

Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.
Expand Down Expand Up @@ -32,7 +32,7 @@ Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks yo

## Labrinth

Labrinth is the backend API service for Modrinth.
Labrinth is the backend API service for Modrinth. It can be ran by changing directory into `apps/labrinth` and running `cargo run -p labrinth`. This MUST be run in the `apps/labrinth` directory, as otherwise the `.env` will not be read. If some environment variables are missing, their default values can be found in `.env.local`. The backend runs on `localhost:8000` by default - you can `curl http://localhost:8000` to test requests.

### Testing

Expand All @@ -42,7 +42,7 @@ Use `cargo test -p labrinth --all-targets` to test your changes. All tests must

To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`. Make sure to NEVER run `cargo sqlx prepare --workspace`.

Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` or `podman exec` to access these services.

When the user refers to "performing pre-PR checks", do the following:

Expand Down
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ dotenv-build = "0.1.1"
dotenvy = "0.15.7"
dunce = "1.0.5"
either = "1.15.0"
elasticsearch = "9.1.0-alpha.1"
encoding_rs = "0.8.35"
enumset = "1.1.10"
eyre = "0.6.12"
Expand Down
5 changes: 5 additions & 0 deletions apps/labrinth/.env.docker-compose
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth
DATABASE_MIN_CONNECTIONS=0
DATABASE_MAX_CONNECTIONS=16

SEARCH_BACKEND=meilisearch
MEILISEARCH_READ_ADDR=http://localhost:7700
MEILISEARCH_WRITE_ADDRS=http://localhost:7700
MEILISEARCH_KEY=modrinth
ELASTICSEARCH_URL=http://localhost:9200
ELASTICSEARCH_INDEX_PREFIX=labrinth
ELASTICSEARCH_USERNAME=elastic
ELASTICSEARCH_PASSWORD=elastic

REDIS_URL=redis://labrinth-redis
REDIS_MIN_CONNECTIONS=0
Expand Down
13 changes: 13 additions & 0 deletions apps/labrinth/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,29 @@ DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth
DATABASE_MIN_CONNECTIONS=0
DATABASE_MAX_CONNECTIONS=16

SEARCH_BACKEND=meilisearch

# Meilisearch configuration
MEILISEARCH_READ_ADDR=http://localhost:7700
MEILISEARCH_WRITE_ADDRS=http://localhost:7700
# 5 minutes in milliseconds
SEARCH_OPERATION_TIMEOUT=300000

ELASTICSEARCH_URL=http://localhost:9200
ELASTICSEARCH_INDEX_PREFIX=labrinth

# # For a sharded Meilisearch setup (sharded-meilisearch docker compose profile)
# MEILISEARCH_READ_ADDR=http://localhost:7710
# MEILISEARCH_WRITE_ADDRS=http://localhost:7700,http://localhost:7701

MEILISEARCH_KEY=modrinth
MEILISEARCH_META_NAMESPACE=

# Elasticsearch configuration
ELASTICSEARCH_URL=http://localhost:9200
ELASTICSEARCH_INDEX_PREFIX=labrinth
ELASTICSEARCH_USERNAME=
ELASTICSEARCH_PASSWORD=

REDIS_URL=redis://localhost
REDIS_MIN_CONNECTIONS=0
Expand Down
7 changes: 4 additions & 3 deletions apps/labrinth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async-stripe = { workspace = true, features = [
"billing",
"checkout",
"connect",
"webhook-events",
"webhook-events"
] }
async-trait = { workspace = true }
base64 = { workspace = true }
Expand All @@ -43,6 +43,7 @@ deadpool-redis.workspace = true
derive_more = { workspace = true, features = ["deref", "deref_mut"] }
dotenvy = { workspace = true }
either = { workspace = true }
elasticsearch = { workspace = true, features = ["experimental-apis"] }
eyre = { workspace = true }
futures = { workspace = true }
futures-util = { workspace = true }
Expand Down Expand Up @@ -85,11 +86,11 @@ reqwest = { workspace = true, features = [
"http2",
"json",
"multipart",
"rustls-tls-webpki-roots",
"rustls-tls-webpki-roots"
] }
rust_decimal = { workspace = true, features = [
"serde-with-float",
"serde-with-str",
"serde-with-str"
] }
rust_iso3166 = { workspace = true }
rust-s3 = { workspace = true }
Expand Down
4 changes: 0 additions & 4 deletions apps/labrinth/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ use thiserror::Error;
pub enum AuthenticationError {
#[error(transparent)]
Internal(#[from] eyre::Report),
#[error("Environment Error")]
Env(#[from] dotenvy::Error),
#[error("An unknown database error occurred: {0}")]
Sqlx(#[from] sqlx::Error),
#[error("Database Error: {0}")]
Expand Down Expand Up @@ -58,7 +56,6 @@ impl actix_web::ResponseError for AuthenticationError {
AuthenticationError::Internal(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthenticationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthenticationError::Sqlx(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthenticationError::Database(..) => {
StatusCode::INTERNAL_SERVER_ERROR
Expand Down Expand Up @@ -94,7 +91,6 @@ impl AuthenticationError {
pub fn error_name(&self) -> &'static str {
match self {
AuthenticationError::Internal(..) => "internal_error",
AuthenticationError::Env(..) => "environment_error",
AuthenticationError::Sqlx(..) => "database_error",
AuthenticationError::Database(..) => "database_error",
AuthenticationError::SerDe(..) => "invalid_input",
Expand Down
23 changes: 11 additions & 12 deletions apps/labrinth/src/background_task.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::database;
use crate::database::PgPool;
use crate::database::redis::RedisPool;
use crate::queue::billing::{index_billing, index_subscriptions};
Expand All @@ -7,9 +8,9 @@ use crate::queue::payouts::{
insert_bank_balances_and_webhook, process_affiliate_payouts,
process_payout, remove_payouts_for_refunded_charges,
};
use crate::search::indexing::index_projects;
use crate::search::SearchBackend;
use crate::util::anrok;
use crate::{database, search};
use actix_web::web;
use clap::ValueEnum;
use tracing::{error, info, warn};

Expand All @@ -34,18 +35,19 @@ impl BackgroundTask {
pool: PgPool,
ro_pool: PgPool,
redis_pool: RedisPool,
search_config: search::SearchConfig,
search_backend: web::Data<dyn SearchBackend>,
clickhouse: clickhouse::Client,
stripe_client: stripe::Client,
anrok_client: anrok::Client,
email_queue: EmailQueue,
mural_client: muralpay::Client,
) {
) -> eyre::Result<()> {
use BackgroundTask::*;
// TODO: all of these tasks should return `eyre::Result`s
match self {
Migrations => run_migrations().await,
IndexSearch => {
index_search(ro_pool, redis_pool, search_config).await
return index_search(ro_pool, redis_pool, search_backend).await;
}
ReleaseScheduled => release_scheduled(pool).await,
UpdateVersions => update_versions(pool, redis_pool).await,
Expand Down Expand Up @@ -77,6 +79,7 @@ impl BackgroundTask {
run_email(email_queue).await;
}
}
Ok(())
}
}

Expand Down Expand Up @@ -122,14 +125,10 @@ pub async fn run_migrations() {
pub async fn index_search(
ro_pool: PgPool,
redis_pool: RedisPool,
search_config: search::SearchConfig,
) {
search_backend: web::Data<dyn SearchBackend>,
) -> eyre::Result<()> {
info!("Indexing local database");
let result = index_projects(ro_pool, redis_pool, &search_config).await;
if let Err(e) = result {
warn!("Local project indexing failed: {:?}", e);
}
info!("Done indexing local database");
search_backend.index_projects(ro_pool, redis_pool).await
}

pub async fn release_scheduled(pool: PgPool) {
Expand Down
14 changes: 11 additions & 3 deletions apps/labrinth/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ where
}

pub fn init() -> eyre::Result<()> {
dotenvy::dotenv().ok();
EnvVars::from_env()?;
LazyLock::force(&ENV);
Ok(())
Expand Down Expand Up @@ -128,9 +129,6 @@ vars! {
LABRINTH_EXTERNAL_NOTIFICATION_KEY: String;
RATE_LIMIT_IGNORE_KEY: String;
DATABASE_URL: String;
MEILISEARCH_READ_ADDR: String;
MEILISEARCH_WRITE_ADDRS: StringCsv;
MEILISEARCH_KEY: String;
REDIS_URL: String;
BIND_ADDR: String;
SELF_ADDR: String;
Expand All @@ -142,6 +140,16 @@ vars! {
ALLOWED_CALLBACK_URLS: Json<Vec<String>>;
ANALYTICS_ALLOWED_ORIGINS: Json<Vec<String>>;

// search
SEARCH_BACKEND: crate::search::SearchBackendKind;
MEILISEARCH_READ_ADDR: String;
MEILISEARCH_WRITE_ADDRS: StringCsv;
MEILISEARCH_KEY: String;
ELASTICSEARCH_URL: String;
ELASTICSEARCH_INDEX_PREFIX: String;
ELASTICSEARCH_USERNAME: String = "";
ELASTICSEARCH_PASSWORD: String = "";

// storage
STORAGE_BACKEND: crate::file_hosting::FileHostKind;

Expand Down
Loading