Backend aplikacji został zbudowany w języku Rust z wykorzystaniem nowoczesnych i wydajnych bibliotek, co gwarantuje wysoką niezawodność i szybkość działania.
- Framework webowy: Rocket
- Klient bazy danych Neo4j: neo4rs
- Runtime asynchroniczny: Tokio
- Serializacja/deserializacja JSON: Serde
System opiera się na klasycznym podziale odpowiedzialności, inspirowanym architekturą warstwową:
- Model: Struktury danych (
struct) reprezentujące encje w bazie (np.Event,User). - Controller: Funkcje obsługujące endpointy API, odpowiedzialne za przyjmowanie żądań i zwracanie odpowiedzi.
- Service: Warstwa logiki biznesowej, gdzie realizowane są operacje na danych.
- Repo: Moduł odpowiedzialny za bezpośrednią interakcję z bazą danych Neo4j.
/// Reprezentuje pojedyncze wydarzenie w systemie.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
/// Unikalny identyfikator wydarzenia.
pub id: i32,
/// Nazwa wydarzenia.
pub name: String,
/// Data i czas rozpoczęcia wydarzenia w formacie ISO 8601.
pub start_datetime: String,
/// Lista słów kluczowych powiązanych z wydarzeniem.
pub keywords: Vec<String>,
}
/// Struktura używana przy tworzeniu nowego wydarzenia.
#[derive(Debug, Deserialize)]
pub struct CreateEventRequest {
pub name: String,
pub start_datetime: String,
pub keywords: Vec<String>,
}
/// Struktura używana przy aktualizacji istniejącego wydarzenia.
/// Wszystkie pola są opcjonalne.
#[derive(Debug, Deserialize)]
pub struct UpdateEventRequest {
pub name: Option<String>,
pub start_datetime: Option<String>,
pub keywords: Vec<String>,
}/// Reprezentuje użytkownika systemu.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
/// Unikalna nazwa użytkownika (identyfikator).
pub name: String,
/// Opcjonalny adres email użytkownika.
pub email: Option<String>,
}
/// Reprezentuje relację zapisu użytkownika na wydarzenie.
#[derive(Debug, Serialize)]
pub struct UserEventRegistration {
pub user_name: String,
pub event_id: i32,
pub is_attending: bool,
}Połączenie z bazą jest zarządzane przez dedykowaną strukturę Neo4jConnection, która wykorzystuje Arc<Graph> do bezpiecznego współdzielenia puli połączeń w środowisku asynchronicznym.
use neo4rs::{ConfigBuilder, Error, Graph};
use std::sync::Arc;
pub struct Neo4jConnection {
pub graph: Arc<Graph>,
}
impl Neo4jConnection {
pub async fn new(uri: &str, user: &str, password: &str) -> Result<Self, Error> {
let config = ConfigBuilder::default()
.uri(uri)
.user(user)
.password(password)
.build()?;
let graph = Arc::new(Graph::connect(config).await?);
Ok(Neo4jConnection { graph })
}
}Właściwości oznaczone (*) są wymagane i unikalne
Poniżej znajdują się kluczowe zapytania Cypher używane w aplikacji do zarządzania danymi w grafie Neo4j.
Opis: Znajduje konkretne wydarzenie na podstawie jego id i zwraca jego dane wraz z listą powiązanych słów kluczowych.
MATCH (e:Event {id: $id})
OPTIONAL MATCH (e)-[:HAS]->(k:EventKeyword)
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
collect(k.name) AS keywordsOPTIONAL MATCHzapewnia, że zapytanie zadziała nawet, jeśli wydarzenie nie ma żadnych słów kluczowych.collect(k.name)agreguje nazwy wszystkich powiązanych słów kluczowych do jednej listy.
Opis: Zwraca listę wszystkich wydarzeń w bazie wraz z ich słowami kluczowymi.
MATCH (e:Event)
OPTIONAL MATCH (e)-[:HAS]->(k:EventKeyword)
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
collect(k.name) AS keywordsOpis: Tworzy nowy węzeł Event, dynamicznie przydziela mu id (o 1 większe od maksymalnego istniejącego), a następnie tworzy lub łączy podane słowa kluczowe.
// 1. Znajdź maksymalne istniejące ID i dodaj 1
MATCH (e:Event)
WITH COALESCE(MAX(e.id), 0) + 1 AS newId
// 2. Utwórz węzeł Event z nowym ID i danymi z parametrów
CREATE (e:Event {
id: newId,
name: $eventName,
startDatetime: datetime($startDatetime)
})
// 3. Dla każdego słowa kluczowego z listy, utwórz węzeł (jeśli nie istnieje) i połącz z wydarzeniem
WITH e
UNWIND $keywords AS kw
MERGE (k:EventKeyword { name: kw })
MERGE (e)-[:HAS]->(k)
// 4. Zwróć dane nowo utworzonego wydarzenia
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
collect(k.name) AS keywordsCOALESCE(MAX(e.id), 0) + 1bezpiecznie inkrementuje ID, startując od 1, jeśli baza jest pusta.MERGEzapobiega tworzeniu duplikatów słów kluczowych.
Opis: Usuwa węzeł wydarzenia o podanym id oraz wszystkie jego relacje.
MATCH (e:Event {id: $eventId})
DETACH DELETE eDETACH DELETEkasuje węzeł wraz ze wszystkimi jego połączeniami, zapobiegając pozostawieniu "osieroconych" relacji.
Opis: Aktualizuje dane wydarzenia. Nadpisuje tylko te pola, które zostały przekazane w parametrach. Usuwa stare powiązania ze słowami kluczowymi i tworzy nowe na podstawie dostarczonej listy.
// 1. Znajdź wydarzenie i zaktualizuj pola, jeśli nowe wartości nie są puste
MATCH (e:Event {id: $eventId})
SET
e.name = coalesce($eventName, e.name),
e.startDatetime = coalesce(datetime($startDatetime), e.startDatetime)
WITH e
// 2. Usuń stare relacje do słów kluczowych
OPTIONAL MATCH (e)-[oldRel:HAS]->(:EventKeyword)
DELETE oldRel
WITH e
// 3. Utwórz nowe relacje do słów kluczowych
UNWIND $keywords AS kw
MERGE (k:EventKeyword { name: kw })
MERGE (e)-[:HAS]->(k)
// 4. Zwróć zaktualizowane wydarzenie
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
collect(k.name) AS keywordscoalesce()pozwala na warunkową aktualizację – jeśli parametr jestnull, zachowuje starą wartość.
Opis: Zwraca ograniczoną liczbę wydarzeń (np. 3), które mogą być użyte jako "polecane" lub "najnowsze".
MATCH (e:Event)
OPTIONAL MATCH (e)-[:HAS]->(k:EventKeyword)
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
collect(k.name) AS keywords
LIMIT 3Opis: Filtruje wydarzenia, zwracając tylko te, które są powiązane ze wszystkimi słowami kluczowymi z podanej listy.
MATCH (e:Event)-[:HAS]->(k:EventKeyword)
WITH e, COLLECT(k.name) AS keywords
WHERE all(kw IN $kws WHERE kw IN keywords)
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
keywordsall(...)działa jak predykat, który jest prawdziwy tylko wtedy, gdy wszystkie elementy z listy$kwsznajdują się w zebranej liściekeywordswydarzenia.
Opis: Zwraca listę wszystkich unikalnych nazw słów kluczowych istniejących w bazie.
MATCH (k:EventKeyword)
RETURN k.nameOpis: Tworzy relację REGISTERED_TO pomiędzy użytkownikiem a wydarzeniem, o ile taka relacja jeszcze nie istnieje.
MATCH (u:User {name: $userName})
MATCH (e:Event {id: $eventId})
MERGE (u)-[:REGISTERED_TO]->(e)Opis: Usuwa relację REGISTERED_TO między użytkownikiem a wydarzeniem.
MATCH (u:User {name: $userName})-[r:REGISTERED_TO]->(e:Event {id: $eventId})
DELETE rOpis: Zwraca listę wydarzeń, na które zapisał się dany użytkownik.
MATCH (:User {name: $userName})-[:REGISTERED_TO]->(e:Event)
OPTIONAL MATCH (e)-[:HAS]->(k:EventKeyword)
RETURN
e.id AS eventId,
e.name AS eventName,
e.startDatetime AS start,
collect(k.name) AS keywordsOpis: Zwraca true lub false w zależności od tego, czy istnieje relacja REGISTERED_TO między użytkownikiem a wydarzeniem.
MATCH (u:User {name: $userName}), (e:Event {id: $eventId})
RETURN EXISTS((u)-[:REGISTERED_TO]->(e)) AS isAttendingOpis: System wyszukuje przyszłe wydarzenia, w których użytkownik jeszcze nie bierze udziału. Rekomendacje opierają się na współczynniku podobieństwa Jaccarda między zbiorami słów kluczowych wydarzeń, na które użytkownik jest już zapisany, a innymi wydarzeniami. Zwracane są tylko te wydarzenia, których współczynnik podobieństwa przekracza próg 0.5.
// 1. Znajdź wydarzenia, na które zapisany jest użytkownik (u)
MATCH (u:User {name: $userName})-[:REGISTERED_TO]->(e:Event)
// 2. Znajdź inne, przyszłe wydarzenia (other), w których użytkownik nie uczestniczy
MATCH (e)-[:HAS]->(k:EventKeyword)<-[:HAS]-(other:Event)
WHERE other.startDatetime > datetime() AND NOT (u)-[:REGISTERED_TO]->(other)
// 3. Oblicz współczynnik Jaccarda
WITH other,
// Zlicz wspólne słowa kluczowe (przecięcie zbiorów)
count(k) AS intersection,
// Zbierz słowa kluczowe z obu wydarzeń
[(e)-[:HAS]->(ek) | ek.name] AS set1,
[(other)-[:HAS]->(ok) | ok.name] AS set2
WITH other,
// Oblicz Jaccard = |A ∩ B| / |A ∪ B|
(1.0 * intersection) / size(set1 + [x IN set2 WHERE NOT x IN set1]) AS jaccard,
set2 AS keywords
// 4. Odfiltruj wyniki poniżej progu i posortuj
WHERE jaccard > 0.5
RETURN
other.id AS eventId,
other.name AS eventName,
other.startDatetime AS start,
keywords
ORDER BY jaccard DESCOpis: Bardziej zaawansowane podejście, które wykorzystuje uczenie maszynowe do znalezienia "podobnych" użytkowników. Proces polega na wygenerowaniu wektorowych reprezentacji (osadzeń) dla użytkowników i wydarzeń, a następnie znalezieniu użytkowników o najbardziej zbliżonych wektorach.
Tworzymy wirtualny graf w pamięci, zawierający tylko użytkowników, wydarzenia i relacje REGISTERED_TO.
CALL gds.graph.project(
'registrations',
['User', 'Event'],
{
REGISTERED_TO: {
orientation: 'UNDIRECTED'
}
}
)Uruchamiamy algorytm FastRP, aby dla każdego węzła w grafie wygenerować wektor cech (embedding).
CALL gds.fastRP.mutate(
'registrations',
{
embeddingDimension: 256,
iterationWeights: [0.8, 1, 1, 1],
mutateProperty: 'embedding'
}
)Algorytm KNN (k-najbliższych sąsiadów) oblicza podobieństwo między użytkownikami na podstawie ich osadzeń i tworzy nowe relacje SIMILAR między najbardziej podobnymi.
CALL gds.knn.write(
'registrations',
{
nodeLabels: ['User'],
nodeProperties: ['embedding'],
topK: 10,
writeRelationshipType: 'SIMILAR',
writeProperty: 'score'
}
)Finalne zapytanie wyszukuje wydarzenia, w których uczestniczą użytkownicy podobni do naszego, a w których nasz użytkownik jeszcze nie bierze udziału.
// 1. Znajdź wydarzenia, w których uczestniczy docelowy użytkownik (u)
MATCH (u:User {name: $userName})-[:REGISTERED_TO]->(e:Event)
WITH u, COLLECT(DISTINCT e) AS userEvents
// 2. Znajdź podobnych użytkowników (similarUser) i ich wydarzenia (recEvent)
MATCH (u)-[:SIMILAR]->(similarUser)-[:REGISTERED_TO]->(recEvent:Event)
// 3. Odfiltruj wydarzenia, które już są na liście użytkownika i są przyszłe
WHERE recEvent.startDatetime > datetime() AND NOT recEvent IN userEvents
// 4. Zbierz unikalne rekomendacje i ich słowa kluczowe
WITH DISTINCT recEvent
OPTIONAL MATCH (recEvent)-[:HAS]->(k:EventKeyword)
RETURN
recEvent.id AS eventId,
recEvent.name AS eventName,
recEvent.startDatetime AS start,
collect(k.name) AS keywords