refactoring, labels support, autocomplete for tags

This commit is contained in:
simon 2019-06-22 17:08:57 -04:00
parent fa1e1a5515
commit 99c2391aab
16 changed files with 349 additions and 42 deletions

View File

@ -29,6 +29,7 @@ public class Main {
rc.registerClasses(Index.class); rc.registerClasses(Index.class);
rc.registerClasses(ArtistController.class); rc.registerClasses(ArtistController.class);
rc.registerClasses(AutocompleteController.class);
rc.registerClasses(CoverController.class); rc.registerClasses(CoverController.class);
rc.registerClasses(TagController.class); rc.registerClasses(TagController.class);
rc.registerClasses(ReleaseController.class); rc.registerClasses(ReleaseController.class);

View File

@ -5,14 +5,16 @@ import java.util.List;
public class ArtistDetails { public class ArtistDetails {
public ArtistDetails() {
releases = new ArrayList<>();
tags = new ArrayList<>();
}
public String name; public String name;
public String comment; public String comment;
public long year; public long year;
public List<Release> releases; public List<Release> releases;
public List<Tag> tags; public List<WeightedTag> tags;
public List<Label> labels;
public ArtistDetails() {
releases = new ArrayList<>();
tags = new ArrayList<>();
labels = new ArrayList<>();
}
} }

View File

@ -0,0 +1,14 @@
package net.simon987.musicgraph.entities;
public class Label {
public long id;
public String mbid;
public String name;
public Label(long id, String mbid, String name) {
this.id = id;
this.mbid = mbid;
this.name = name;
}
}

View File

@ -0,0 +1,7 @@
package net.simon987.musicgraph.entities;
public class LabelSearchResult extends SearchResult {
public Label label;
}

View File

@ -0,0 +1,16 @@
package net.simon987.musicgraph.entities;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RelatedLabels {
public Set<Label> labels;
public List<Relation> relations;
public RelatedLabels() {
labels = new HashSet<>();
relations = new ArrayList<>();
}
}

View File

@ -0,0 +1,16 @@
package net.simon987.musicgraph.entities;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RelatedTags {
public Set<Tag> tags;
public List<Relation> relations;
public RelatedTags() {
tags = new HashSet<>();
relations = new ArrayList<>();
}
}

View File

@ -9,7 +9,7 @@ public class ReleaseDetails {
public String mbid; public String mbid;
public String artist; public String artist;
public long year; public long year;
public List<Tag> tags; public List<WeightedTag> tags;
public ReleaseDetails() { public ReleaseDetails() {
this.tags = new ArrayList<>(); this.tags = new ArrayList<>();

View File

@ -1,14 +1,12 @@
package net.simon987.musicgraph.entities; package net.simon987.musicgraph.entities;
public class Tag { public class Tag {
public String name; public String name;
public double weight;
public long id; public long id;
public Tag(long id, String name) {
public Tag(long id, String name, double weight) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.weight = weight;
} }
} }

View File

@ -0,0 +1,11 @@
package net.simon987.musicgraph.entities;
public class WeightedTag extends Tag {
public double weight;
public WeightedTag(long id, String name, double weight) {
super(id, name);
this.weight = weight;
}
}

View File

@ -3,7 +3,7 @@ package net.simon987.musicgraph.io;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import net.simon987.musicgraph.entities.*; import net.simon987.musicgraph.entities.*;
import net.simon987.musicgraph.logging.LogManager; import net.simon987.musicgraph.logging.LogManager;
import net.simon987.musicgraph.webapi.ArtistOverview; import net.simon987.musicgraph.webapi.AutocompleteLine;
import net.simon987.musicgraph.webapi.AutoCompleteData; import net.simon987.musicgraph.webapi.AutoCompleteData;
import org.glassfish.jersey.internal.inject.AbstractBinder; import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.neo4j.driver.v1.*; import org.neo4j.driver.v1.*;
@ -83,7 +83,9 @@ public class MusicDatabase extends AbstractBinder {
"MATCH (a:Artist {id: $mbid})-[:CREDITED_FOR]->(r:Release)\n" + "MATCH (a:Artist {id: $mbid})-[:CREDITED_FOR]->(r:Release)\n" +
"WITH collect({id: ID(r), mbid:r.id, name:r.name, year:r.year, labels:labels(r)}) as releases, a\n" + "WITH collect({id: ID(r), mbid:r.id, name:r.name, year:r.year, labels:labels(r)}) as releases, a\n" +
"OPTIONAL MATCH (a)-[r:IS_TAGGED]->(t:Tag)\n" + "OPTIONAL MATCH (a)-[r:IS_TAGGED]->(t:Tag)\n" +
"RETURN a {name:a.name, year:a.year, comment:a.comment, releases:releases, tags:collect({weight: r.weight, name: t.name, id:ID(t)})}\n" + "WITH collect({weight: r.weight, name: t.name, id:ID(t)}) as tags, a, releases\n" +
"OPTIONAL MATCH (a)-[r:CREDITED_FOR]->(:Release)-[]-(l:Label)\n" +
"RETURN a {name:a.name, year:a.year, comment:a.comment, releases:releases, tags:tags, labels:collect(DISTINCT {id:ID(l),mbid:l.id,name:l.name})} \n" +
"LIMIT 1", "LIMIT 1",
params); params);
@ -113,13 +115,24 @@ public class MusicDatabase extends AbstractBinder {
((List<Map>) map.get("tags")) ((List<Map>) map.get("tags"))
.stream() .stream()
.filter(x -> x.get("name") != null) .filter(x -> x.get("name") != null)
.map(x -> new Tag( .map(x -> new WeightedTag(
(Long) x.get("id"), (Long) x.get("id"),
(String) x.get("name"), (String) x.get("name"),
(Double) x.get("weight") (Double) x.get("weight")
)) ))
.collect(Collectors.toList()) .collect(Collectors.toList())
); );
details.labels.addAll(
((List<Map>) map.get("labels"))
.stream()
.filter(x -> x.get("id") != null)
.map(x -> new Label(
(Long) x.get("id"),
(String) x.get("mbid"),
(String) x.get("name")
))
.collect(Collectors.toList())
);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -141,7 +154,7 @@ public class MusicDatabase extends AbstractBinder {
"WHERE a.id = $mbid\n" + "WHERE a.id = $mbid\n" +
// Only match artists with > 0 releases // Only match artists with > 0 releases
"MATCH (b)-[:CREDITED_FOR]->(:Release)\n" + "MATCH (b)-[:CREDITED_FOR]->(:Release)\n" +
"WHERE r.weight > 0.25\n" + "WHERE r.weight > 0.15\n" +
"RETURN a as artist, {rels: collect(DISTINCT r), nodes: collect(DISTINCT b)} as rank1\n" + "RETURN a as artist, {rels: collect(DISTINCT r), nodes: collect(DISTINCT b)} as rank1\n" +
"LIMIT 1", "LIMIT 1",
params); params);
@ -167,6 +180,7 @@ public class MusicDatabase extends AbstractBinder {
StatementResult result = query(session, StatementResult result = query(session,
"MATCH (t:Tag)-[r:IS_TAGGED]-(a:Artist)\n" + "MATCH (t:Tag)-[r:IS_TAGGED]-(a:Artist)\n" +
"WHERE ID(t) = $tag_id\n" + "WHERE ID(t) = $tag_id\n" +
// Is rels really necessary?
"RETURN t, {rels: collect(DISTINCT r), nodes: collect(DISTINCT a)} as rank1\n" + "RETURN t, {rels: collect(DISTINCT r), nodes: collect(DISTINCT a)} as rank1\n" +
"LIMIT 1", "LIMIT 1",
params); params);
@ -186,6 +200,92 @@ public class MusicDatabase extends AbstractBinder {
} }
} }
public LabelSearchResult getRelatedByLabel(long id) {
try (Session session = driver.session()) {
Map<String, Object> params = new HashMap<>();
params.put("label_id", id);
StatementResult result = query(session,
"MATCH (l:Label)-[]-(:Release)-[:CREDITED_FOR]-(a:Artist) " +
"WHERE ID(l) = $label_id " +
"RETURN l, {nodes: collect(DISTINCT a)} as rank1 " +
"LIMIT 1",
params);
LabelSearchResult out = new LabelSearchResult();
if (result.hasNext()) {
Record row = result.next();
out.label = makeLabel(row.get(0).asNode());
artistsFromRelMap(out, row.get(1).asMap());
}
return out;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public RelatedLabels getRelatedLabelByLabel(long id) {
try (Session session = driver.session()) {
Map<String, Object> params = new HashMap<>();
params.put("label_id", id);
StatementResult result = query(session,
"MATCH (l:Label)-[r:IS_RELATED_TO]-(l2:Label) " +
"WHERE ID(t) = $tag_id " +
"RETURN {rels: collect(DISTINCT r), nodes: collect(DISTINCT t2)} as rank1 " +
"LIMIT 1",
params);
RelatedLabels out = new RelatedLabels();
if (result.hasNext()) {
Record row = result.next();
labelsFromRelMap(out, row.get(0).asMap());
}
return out;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public RelatedTags getRelatedTagByTag(long id) {
try (Session session = driver.session()) {
Map<String, Object> params = new HashMap<>();
params.put("tag_id", id);
StatementResult result = query(session,
"MATCH (t:Tag)-[r:IS_RELATED_TO]-(t2:Tag) " +
"WHERE ID(t) = $tag_id " +
"RETURN {rels: collect(DISTINCT r), nodes: collect(DISTINCT t2)} as rank1 " +
"LIMIT 1",
params);
RelatedTags out = new RelatedTags();
if (result.hasNext()) {
Record row = result.next();
tagsFromRelMap(out, row.get(0).asMap());
}
return out;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private void parseRelatedResult(StatementResult result, SearchResult out) { private void parseRelatedResult(StatementResult result, SearchResult out) {
long start = System.nanoTime(); long start = System.nanoTime();
if (result.hasNext()) { if (result.hasNext()) {
@ -212,6 +312,32 @@ public class MusicDatabase extends AbstractBinder {
)); ));
} }
private void tagsFromRelMap(RelatedTags out, Map<String, Object> rank1) {
out.relations.addAll(((List<Relationship>) rank1.get("rels"))
.stream()
.map(MusicDatabase::makeRelation)
.collect(Collectors.toList()
));
out.tags.addAll(((List<Node>) rank1.get("nodes"))
.stream()
.map(MusicDatabase::makeTag)
.collect(Collectors.toList()
));
}
private void labelsFromRelMap(RelatedLabels out, Map<String, Object> rank1) {
out.relations.addAll(((List<Relationship>) rank1.get("rels"))
.stream()
.map(MusicDatabase::makeRelation)
.collect(Collectors.toList()
));
out.labels.addAll(((List<Node>) rank1.get("nodes"))
.stream()
.map(MusicDatabase::makeLabel)
.collect(Collectors.toList()
));
}
private static Artist makeArtist(Node node) { private static Artist makeArtist(Node node) {
Artist artist = new Artist(); Artist artist = new Artist();
artist.id = node.id(); artist.id = node.id();
@ -228,8 +354,15 @@ public class MusicDatabase extends AbstractBinder {
private static Tag makeTag(Node node) { private static Tag makeTag(Node node) {
return new Tag( return new Tag(
node.id(), node.id(),
node.get("name").asString(), node.get("name").asString()
0 );
}
private static Label makeLabel(Node node) {
return new Label(
node.id(),
node.get("id").asString(),
node.get("name").asString()
); );
} }
@ -275,7 +408,7 @@ public class MusicDatabase extends AbstractBinder {
((List<Map>) map.get("tags")) ((List<Map>) map.get("tags"))
.stream() .stream()
.filter(x -> x.get("name") != null) .filter(x -> x.get("name") != null)
.map(x -> new Tag( .map(x -> new WeightedTag(
(Long) x.get("id"), (Long) x.get("id"),
(String) x.get("name"), (String) x.get("name"),
(Double) x.get("weight") (Double) x.get("weight")
@ -298,6 +431,8 @@ public class MusicDatabase extends AbstractBinder {
try (Session session = driver.session()) { try (Session session = driver.session()) {
AutoCompleteData data = new AutoCompleteData();
StatementResult result = query(session, StatementResult result = query(session,
"MATCH (a:Artist) " + "MATCH (a:Artist) " +
"WHERE a.sortname STARTS WITH $prefix " + "WHERE a.sortname STARTS WITH $prefix " +
@ -305,22 +440,49 @@ public class MusicDatabase extends AbstractBinder {
"LIMIT 30", "LIMIT 30",
params); params);
AutoCompleteData data = new AutoCompleteData();
while (result.hasNext()) { while (result.hasNext()) {
Map<String, Object> map = result.next().get("a").asMap(); Map<String, Object> map = result.next().get("a").asMap();
ArtistOverview a = new ArtistOverview(); AutocompleteLine line = new AutocompleteLine();
a.name = (String) map.get("name"); line.name = (String) map.get("name");
a.comment = (String) map.get("comment"); line.type = "artist";
a.year = (long) map.get("year"); line.comment = (String) map.get("comment");
a.mbid = (String) map.get("id"); line.year = (long) map.get("year");
line.id = (String) map.get("id");
data.artists.add(a); data.lines.add(line);
}
params.put("prefix", prefix.toLowerCase());
StatementResult tagResult = query(session,
"MATCH (t:Tag)-[:IS_TAGGED]-(:Artist) " +
"WHERE t.name STARTS WITH $prefix " +
"RETURN DISTINCT t ORDER BY t.occurrences DESC " +
"LIMIT 15",
params);
while (tagResult.hasNext()) {
Node node = tagResult.next().get("t").asNode();
AutocompleteLine line = new AutocompleteLine();
line.name = node.get("name").asString();
line.type = "tag";
line.id = String.valueOf(node.id());
// Interlace tags with the artists, keeping listeners order
if (data.lines.size() > 0) {
for (int i = 0; i < data.lines.size(); i++) {
if (data.lines.get(i).name.toLowerCase().compareTo(line.name) > 0) {
data.lines.add(i+1, line);
break;
}
}
} else {
data.lines.add(line);
}
} }
return data; return data;
} }
} }
} }

View File

@ -49,14 +49,4 @@ public class ArtistController {
return db.getArtistDetails(mbid); return db.getArtistDetails(mbid);
} }
@GET
@Path("autocomplete/{prefix}")
@Produces(MediaType.APPLICATION_JSON)
public AutoCompleteData autoComplete(@PathParam("prefix") String prefix) {
prefix = prefix.replace('+', ' ');
logger.info(String.format("Autocomplete for '%s'", prefix));
return db.autoComplete(prefix);
}
} }

View File

@ -4,9 +4,9 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class AutoCompleteData { public class AutoCompleteData {
public List<ArtistOverview> artists; public List<AutocompleteLine> lines;
public AutoCompleteData() { public AutoCompleteData() {
artists = new ArrayList<>(30); lines = new ArrayList<>(30);
} }
} }

View File

@ -0,0 +1,35 @@
package net.simon987.musicgraph.webapi;
import net.simon987.musicgraph.io.MusicDatabase;
import net.simon987.musicgraph.logging.LogManager;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.logging.Logger;
@Path("/")
public class AutocompleteController {
private static Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;
public AutocompleteController() {
}
@GET
@Path("/autocomplete/{prefix}")
@Produces(MediaType.APPLICATION_JSON)
public AutoCompleteData autoComplete(@PathParam("prefix") String prefix) {
prefix = prefix.replace('+', ' ');
logger.info(String.format("Autocomplete for '%s'", prefix));
return db.autoComplete(prefix);
}
}

View File

@ -1,8 +1,9 @@
package net.simon987.musicgraph.webapi; package net.simon987.musicgraph.webapi;
public class ArtistOverview { public class AutocompleteLine {
public String name; public String name;
public String mbid; public String type;
public String id;
public String comment; public String comment;
public long year; public long year;
} }

View File

@ -0,0 +1,44 @@
package net.simon987.musicgraph.webapi;
import net.simon987.musicgraph.entities.RelatedLabels;
import net.simon987.musicgraph.entities.SearchResult;
import net.simon987.musicgraph.io.MusicDatabase;
import net.simon987.musicgraph.logging.LogManager;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.logging.Logger;
@Path("/label")
public class LabelController {
private static Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;
public LabelController() {
}
@GET
@Path("related/{id}")
@Produces(MediaType.APPLICATION_JSON)
public SearchResult getRelated(@PathParam("id") long id) {
logger.info(String.format("Related artist for label %d", id));
return db.getRelatedByLabel(id);
}
@GET
@Path("label/{id}")
@Produces(MediaType.APPLICATION_JSON)
public RelatedLabels getRelatedLabel(@PathParam("id") long id) {
logger.info(String.format("Related labels for label %d", id));
return db.getRelatedLabelByLabel(id);
}
}

View File

@ -1,5 +1,6 @@
package net.simon987.musicgraph.webapi; package net.simon987.musicgraph.webapi;
import net.simon987.musicgraph.entities.RelatedTags;
import net.simon987.musicgraph.entities.SearchResult; import net.simon987.musicgraph.entities.SearchResult;
import net.simon987.musicgraph.io.MusicDatabase; import net.simon987.musicgraph.io.MusicDatabase;
import net.simon987.musicgraph.logging.LogManager; import net.simon987.musicgraph.logging.LogManager;
@ -28,7 +29,16 @@ public class TagController {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public SearchResult getRelated(@PathParam("id") long id) { public SearchResult getRelated(@PathParam("id") long id) {
logger.info(String.format("Related tag for %d", id)); logger.info(String.format("Related artists for tag %d", id));
return db.getRelatedByTag(id); return db.getRelatedByTag(id);
} }
@GET
@Path("tag/{id}")
@Produces(MediaType.APPLICATION_JSON)
public RelatedTags getRelatedTag(@PathParam("id") long id) {
logger.info(String.format("Related tags for tag %d", id));
return db.getRelatedTagByTag(id);
}
} }