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(ArtistController.class);
rc.registerClasses(AutocompleteController.class);
rc.registerClasses(CoverController.class);
rc.registerClasses(TagController.class);
rc.registerClasses(ReleaseController.class);

View File

@ -5,14 +5,16 @@ import java.util.List;
public class ArtistDetails {
public ArtistDetails() {
releases = new ArrayList<>();
tags = new ArrayList<>();
}
public String name;
public String comment;
public long year;
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 artist;
public long year;
public List<Tag> tags;
public List<WeightedTag> tags;
public ReleaseDetails() {
this.tags = new ArrayList<>();

View File

@ -1,14 +1,12 @@
package net.simon987.musicgraph.entities;
public class Tag {
public String name;
public double weight;
public long id;
public Tag(long id, String name, double weight) {
public Tag(long id, String name) {
this.id = id;
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 net.simon987.musicgraph.entities.*;
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 org.glassfish.jersey.internal.inject.AbstractBinder;
import org.neo4j.driver.v1.*;
@ -83,7 +83,9 @@ public class MusicDatabase extends AbstractBinder {
"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" +
"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",
params);
@ -113,13 +115,24 @@ public class MusicDatabase extends AbstractBinder {
((List<Map>) map.get("tags"))
.stream()
.filter(x -> x.get("name") != null)
.map(x -> new Tag(
.map(x -> new WeightedTag(
(Long) x.get("id"),
(String) x.get("name"),
(Double) x.get("weight")
))
.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) {
e.printStackTrace();
@ -141,7 +154,7 @@ public class MusicDatabase extends AbstractBinder {
"WHERE a.id = $mbid\n" +
// Only match artists with > 0 releases
"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" +
"LIMIT 1",
params);
@ -167,6 +180,7 @@ public class MusicDatabase extends AbstractBinder {
StatementResult result = query(session,
"MATCH (t:Tag)-[r:IS_TAGGED]-(a:Artist)\n" +
"WHERE ID(t) = $tag_id\n" +
// Is rels really necessary?
"RETURN t, {rels: collect(DISTINCT r), nodes: collect(DISTINCT a)} as rank1\n" +
"LIMIT 1",
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) {
long start = System.nanoTime();
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) {
Artist artist = new Artist();
artist.id = node.id();
@ -228,8 +354,15 @@ public class MusicDatabase extends AbstractBinder {
private static Tag makeTag(Node node) {
return new Tag(
node.id(),
node.get("name").asString(),
0
node.get("name").asString()
);
}
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"))
.stream()
.filter(x -> x.get("name") != null)
.map(x -> new Tag(
.map(x -> new WeightedTag(
(Long) x.get("id"),
(String) x.get("name"),
(Double) x.get("weight")
@ -298,6 +431,8 @@ public class MusicDatabase extends AbstractBinder {
try (Session session = driver.session()) {
AutoCompleteData data = new AutoCompleteData();
StatementResult result = query(session,
"MATCH (a:Artist) " +
"WHERE a.sortname STARTS WITH $prefix " +
@ -305,22 +440,49 @@ public class MusicDatabase extends AbstractBinder {
"LIMIT 30",
params);
AutoCompleteData data = new AutoCompleteData();
while (result.hasNext()) {
Map<String, Object> map = result.next().get("a").asMap();
ArtistOverview a = new ArtistOverview();
a.name = (String) map.get("name");
a.comment = (String) map.get("comment");
a.year = (long) map.get("year");
a.mbid = (String) map.get("id");
AutocompleteLine line = new AutocompleteLine();
line.name = (String) map.get("name");
line.type = "artist";
line.comment = (String) map.get("comment");
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;
}
}
}

View File

@ -49,14 +49,4 @@ public class ArtistController {
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;
public class AutoCompleteData {
public List<ArtistOverview> artists;
public List<AutocompleteLine> lines;
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;
public class ArtistOverview {
public class AutocompleteLine {
public String name;
public String mbid;
public String type;
public String id;
public String comment;
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;
import net.simon987.musicgraph.entities.RelatedTags;
import net.simon987.musicgraph.entities.SearchResult;
import net.simon987.musicgraph.io.MusicDatabase;
import net.simon987.musicgraph.logging.LogManager;
@ -28,7 +29,16 @@ public class TagController {
@Produces(MediaType.APPLICATION_JSON)
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);
}
@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);
}
}