Overview
Extend the explore service with authenticated, user-aware graph queries leveraging the COLLECTED and WANTS relationships created by the collector service (#58). Also adds frontend UI tabs for personal collection browsing, wantlist, and recommendations.
Reference Implementation
vinyldigger has the data model and search/match patterns:
backend/src/models/search_result.py — is_in_collection, is_in_wantlist boolean flags on results
backend/src/services/search.py — matching logic between search results and collection/wantlist
The Neo4j query layer is specific to discogsography's graph model.
New API Endpoints
All require Authorization: Bearer <jwt> header. Added to the existing explore service.
Collection Queries
GET /api/user/collection
→ Paginated list of user's collected releases
→ Params: page, per_page, sort (date_added|rating|title|artist|year), genre, decade, label
→ Returns: releases with full graph context (artists, labels, genres)
GET /api/user/collection/graph
→ D3.js nodes/links structure of collected releases + connected artists/labels
→ For collection visualization
GET /api/user/collection/stats
→ {
total: int,
by_genre: [{genre, count}],
by_decade: [{decade, count}],
by_label: [{label, count}],
by_artist: [{artist, count}],
avg_rating: float
}
Wantlist Queries
GET /api/user/wantlist
→ Paginated list of user's wanted releases
GET /api/user/wantlist/stats
→ {total, by_genre, by_decade, by_artist, avg_rating}
Discovery & Recommendations
GET /api/user/recommendations
→ Releases to consider based on collection graph traversal
→ Ranked by: number of collected artists/labels that also released candidate
GET /api/user/collection/artists/gaps
→ For each artist in your collection: releases you don't own
→ "You have 3 Aphex Twin releases; here are 5 you don't"
GET /api/user/wantlist/context
→ Wantlist items enriched with graph context
→ Shows: other releases from same artist/label you already own
Cross-Check (from vinyldigger is_in_collection / is_in_wantlist pattern)
GET /api/releases/{discogs_release_id}/ownership
→ {in_collection: bool, in_wantlist: bool, collection_instance_count: int}
→ Useful for decorating existing explore results with personal flags
Neo4j Queries
Collection Stats by Genre
MATCH (u:User {id: $user_id})-[:COLLECTED]->(r:Release)-[:HAS_GENRE]->(g:Genre)
RETURN g.name AS genre, count(r) AS count
ORDER BY count DESC
Collection by Decade
MATCH (u:User {id: $user_id})-[:COLLECTED]->(r:Release)
WHERE r.released IS NOT NULL
RETURN (toInteger(r.released) / 10) * 10 AS decade, count(r) AS count
ORDER BY decade
Recommendations (artists you collect → their other releases)
MATCH (u:User {id: $user_id})-[:COLLECTED]->(owned:Release)<-[:RELEASED]-(a:Artist)
MATCH (a)-[:RELEASED]->(candidate:Release)
WHERE NOT (u)-[:COLLECTED]->(candidate)
AND NOT (u)-[:WANTS]->(candidate)
WITH candidate, collect(DISTINCT a.name) AS via_artists, count(DISTINCT a) AS artist_overlap
ORDER BY artist_overlap DESC
LIMIT 20
RETURN candidate, via_artists, artist_overlap
Artist Gap Analysis
MATCH (u:User {id: $user_id})-[:COLLECTED]->(c:Release)<-[:RELEASED]-(a:Artist)
MATCH (a)-[:RELEASED]->(all_releases:Release)
WHERE NOT (u)-[:COLLECTED]->(all_releases)
WITH a, count(DISTINCT c) AS owned_count, collect(DISTINCT all_releases.title) AS missing
WHERE size(missing) > 0
RETURN a.name AS artist, owned_count, size(missing) AS missing_count, missing[..5] AS sample_missing
ORDER BY owned_count DESC
Wantlist Context (what you already own from same artist/label)
MATCH (u:User {id: $user_id})-[:WANTS]->(w:Release)
OPTIONAL MATCH (w)<-[:RELEASED]-(a:Artist)-[:RELEASED]->(also:Release)
WHERE (u)-[:COLLECTED]->(also)
WITH w, collect(DISTINCT {artist: a.name, title: also.title}) AS related_owned
RETURN w, related_owned
ORDER BY size(related_owned) DESC
Ownership Check (vinyldigger is_in_collection / is_in_wantlist pattern)
MATCH (u:User {id: $user_id})
OPTIONAL MATCH (u)-[c:COLLECTED]->(r:Release {discogs_id: $release_id})
OPTIONAL MATCH (u)-[w:WANTS]->(r)
RETURN
count(c) > 0 AS in_collection,
count(c) AS instance_count,
count(w) > 0 AS in_wantlist
Integration with Existing Explore Queries
Decorate existing explore results (entity details, search, expand) with ownership flags:
# For authenticated requests, enrich release results with:
{
"release": {...}, # existing explore data
"ownership": {
"in_collection": true,
"instance_count": 2, # might own multiple pressings
"in_wantlist": false,
}
}
This mirrors vinyldigger's is_in_collection and is_in_wantlist boolean fields on SearchResult.
Frontend UI Extensions
Add to the explore service's frontend:
"My Collection" Tab
- Summary stats panel (total items, unique artists, unique labels, avg rating)
- Genre breakdown chart (D3.js donut or bar)
- Decade distribution histogram
- D3.js graph: collected releases connected via shared artists/labels (colored by decade/genre)
- Sortable/filterable list view
"Wantlist" Tab
- Paginated wantlist with release info
- "You also own from this artist" callouts (from wantlist context query)
- Wantlist stats
"Discover" Tab
- Recommendation list ranked by artist overlap
- "Complete your collection" artist gap analysis
- Filters by genre, decade, label
Collection Ownership Decorations
- Existing explore graph nodes for releases show collection/wantlist badges when authenticated
- Detail panels include ownership status
Auth UI
- Login/register from explore service navigation
- User settings: Discogs connection status + "Sync now" button (triggers collector)
Performance Considerations
- All user queries must use
User.id index (UUID lookup, not scan)
COLLECTED and WANTS relationships indexed by user_id in Neo4j
- Paginate all list endpoints (default per_page: 50, max: 100)
- For ownership decoration on existing queries: batch check multiple release IDs in single Cypher query
- Cache stats results in Redis (short TTL, e.g. 5 minutes)
Acceptance Criteria
Dependencies
Overview
Extend the
exploreservice with authenticated, user-aware graph queries leveraging theCOLLECTEDandWANTSrelationships created by the collector service (#58). Also adds frontend UI tabs for personal collection browsing, wantlist, and recommendations.Reference Implementation
vinyldigger has the data model and search/match patterns:
backend/src/models/search_result.py—is_in_collection,is_in_wantlistboolean flags on resultsbackend/src/services/search.py— matching logic between search results and collection/wantlistThe Neo4j query layer is specific to discogsography's graph model.
New API Endpoints
All require
Authorization: Bearer <jwt>header. Added to the existingexploreservice.Collection Queries
Wantlist Queries
Discovery & Recommendations
Cross-Check (from vinyldigger
is_in_collection/is_in_wantlistpattern)Neo4j Queries
Collection Stats by Genre
Collection by Decade
Recommendations (artists you collect → their other releases)
Artist Gap Analysis
Wantlist Context (what you already own from same artist/label)
Ownership Check (vinyldigger
is_in_collection/is_in_wantlistpattern)Integration with Existing Explore Queries
Decorate existing explore results (entity details, search, expand) with ownership flags:
This mirrors vinyldigger's
is_in_collectionandis_in_wantlistboolean fields onSearchResult.Frontend UI Extensions
Add to the explore service's frontend:
"My Collection" Tab
"Wantlist" Tab
"Discover" Tab
Collection Ownership Decorations
Auth UI
Performance Considerations
User.idindex (UUID lookup, not scan)COLLECTEDandWANTSrelationships indexed byuser_idin Neo4jAcceptance Criteria
/collectionreturns correct paginated data matchinguser_collectionstable/collection/statsreturns accurate breakdowns by genre, decade, label, artist/wantlistreturns correct data/recommendationsreturns non-owned, non-wanted releases from known artists, ranked/collection/artists/gapsreturns correct gap analysis/wantlist/contextshows related owned items from same artistsDependencies