Skip to content

Commit

Permalink
Support 2 types of expand - apoc & plain BFS
Browse files Browse the repository at this point in the history
  • Loading branch information
QubitPi committed Oct 29, 2024
1 parent d8a41b8 commit 0d7d5ec
Show file tree
Hide file tree
Showing 9 changed files with 471 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
target/
logback/
.DS_Store
jetty-base/
jetty-home-11.0.15/
application.properties
jetty-home-11.0.15.tar.gz
58 changes: 47 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,74 @@ Aristotle

Aristotle is a [JSR 370] [JAX-RS] webservice of CRUD operations against a graph database. It supports Neo4J now.

Configuration
-------------

- `NEO4J_URI`
- `NEO4J_USERNAME`
- `NEO4J_PASSWORD`
- `NEO4J_DATABASE`

Test
----

```console
mvn clean verify
```

Start Locally in Jetty
----------------------

Make sure port 8080 is not occupied and the following environment variables are set:

```console
export NEO4J_URI=
export NEO4J_USERNAME=
export NEO4J_PASSWORD=
export NEO4J_DATABASE=
```

Then start webservice with:

```bash
./jetty-start.sh
```



Deployment
----------

```bash
This is a one-person project. Agility outplays team scaling, so deployment is manual:


```console
mvn clean package
```

```console
export JETTY_HOME=
```

### Sending Logs to ELK Cloud

Simply add Logstash integration and install agent on the production server. The logs will be available on integration
dashboard.

### Gateway Registration

```bash
export GATEWAY_PUBLIC_IP=52.53.186.26

# healthcheck
curl -v -i -s -k -X POST https://api.paion-data.dev:8444/services \
--data name=wilhelm-ws-healthcheck \
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/data/healthcheck"
curl -i -k -X POST https://api.paion-data.dev:8444/services/wilhelm-ws-healthcheck/routes \
--data "paths[]=/wilhelm/healthcheck" \
--data name=wilhelm-ws-healthcheck

# language paged
curl -v -i -s -k -X POST https://api.paion-data.dev:8444/services \
--data name=wilhelm-ws-languages \
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/data/languages"
curl -i -k -X POST https://api.paion-data.dev:8444/services/wilhelm-ws-languages/routes \
--data "paths[]=/wilhelm/languages" \
--data name=wilhelm-ws-languages

# expand
curl -v -i -s -k -X POST https://api.paion-data.dev:8444/services \
--data name=wilhelm-ws-expand \
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/data/expand"
Expand All @@ -54,8 +88,10 @@ We should see `HTTP/1.1 201 Created` as signs of success.

#### Example requests:

- https://api.paion-data.dev/wilhelm/languages/german?perPage=100&page=1
- https://api.paion-data.dev/wilhelm/expand/nämlich
- healthcheck: https://api.paion-data.dev/wilhelm/healthcheck
- vocabulary count: https://api.paion-data.dev/wilhelm/languages/german?perPage=100&page=1
- query vocabulary paged: https://api.paion-data.dev/wilhelm/languages/german/count
- expand: https://api.paion-data.dev/wilhelm/expand/nämlich

License
-------
Expand Down
30 changes: 30 additions & 0 deletions jetty-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
set -x
set -e

# Copyright Jiaqi Liu
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

mvn clean package -Dcheckstyle.skip -DskipTests

wget -O jetty-home-11.0.15.tar.gz https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/11.0.15/jetty-home-11.0.15.tar.gz
tar -xzvf jetty-home-11.0.15.tar.gz
export JETTY_HOME=$(pwd)/jetty-home-11.0.15

mkdir -p jetty-base
cd jetty-base
java -jar $JETTY_HOME/start.jar --add-module=annotations,server,http,deploy,servlet,webapp,resources,jsp

mv ../target/wilhelm-ws-1.0-SNAPSHOT.war webapps/ROOT.war
java -jar $JETTY_HOME/start.jar
93 changes: 93 additions & 0 deletions src/main/java/org/qubitpi/wilhelm/Graph.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Jiaqi Liu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.qubitpi.wilhelm;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A JSON-serializable object representation of a knowledge graph in wilhelm-ws.
*/
public class Graph {

private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

private final Set<Node> nodes;
private final Set<Link> links;

/**
* All-args constructor.
*
* @param nodes
* @param links
*/
public Graph(final Set<Node> nodes, final Set<Link> links) {
this.nodes = nodes;
this.links = links;
}

public static Graph emptyGraph() {
return new Graph(Collections.emptySet(), Collections.emptySet());
}

@JsonIgnore
public boolean isEmpty() {
return getNodes().isEmpty() && getLinks().isEmpty();
}

public List<Node> getUndirectedNeighborsOf(Node node) {
final Set<String> neighborIds = getLinks().stream()
.filter(link -> node.getId().equals(link.getSourceNodeId()) || node.getId().equals(link.getTargetNodeId()))
.flatMap(link -> Stream.of(link.getSourceNodeId(), link.getTargetNodeId()))
.filter(id -> !node.getId().equals(id))
.collect(Collectors.toUnmodifiableSet());

return getNodes().stream()
.filter(it -> neighborIds.contains(it.getId()))
.collect(Collectors.toUnmodifiableList());
}

public Graph merge(final Graph that) {
return new Graph(
Stream.of(this.getNodes(), that.getNodes()).flatMap(Set::stream).collect(Collectors.toSet()),
Stream.of(this.getLinks(), that.getLinks()).flatMap(Set::stream).collect(Collectors.toSet())
);
}

public Set<Node> getNodes() {
return nodes;
}

public Set<Link> getLinks() {
return links;
}

@Override
public String toString() {
try {
return JSON_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
36 changes: 26 additions & 10 deletions src/main/java/org/qubitpi/wilhelm/Language.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import net.jcip.annotations.ThreadSafe;

import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -58,6 +59,25 @@ public enum Language {
this.databaseName = databaseName;
}

private static Language valueOf(@NotNull final String language, Function<Language, String> nameExtractor) {
return Arrays.stream(values())
.filter(value -> nameExtractor.apply(value).equals(language))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
String.format(
"'%s' is not a recognized language. Acceptable ones are %s",
language,
Arrays.stream(values())
.map(nameExtractor)
.collect(Collectors.joining(", "))
)
));
}

public static Language ofDatabaseName(@NotNull final String language) {
return valueOf(language, Language::getDatabaseName);
}

/**
* Constructs a {@link Language} from its client-side name.
*
Expand All @@ -68,16 +88,7 @@ public enum Language {
* @throws IllegalArgumentException if the language name is not a valid one
*/
public static Language ofClientValue(@NotNull final String language) throws IllegalArgumentException {
return Arrays.stream(values())
.filter(value -> value.pathName.equals(language))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
String.format(
"'%s' is not a recognized language. Acceptable ones are %s",
language,
Arrays.stream(values()).map(Language::getPathName).collect(Collectors.joining(", ")
)
)));
return valueOf(language, Language::getPathName);
}

@NotNull
Expand All @@ -89,4 +100,9 @@ public String getPathName() {
public String getDatabaseName() {
return databaseName;
}

@Override
public String toString() {
return getDatabaseName();
}
}
97 changes: 97 additions & 0 deletions src/main/java/org/qubitpi/wilhelm/Link.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright Jiaqi Liu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.qubitpi.wilhelm;

import org.neo4j.driver.types.Relationship;

import jakarta.validation.constraints.NotNull;

import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class Link {

private final String label;
private final String sourceNodeId;
private final String targetNodeId;
private final Map<String, Object> attributes;

public Link(
@NotNull final String label,
@NotNull final String sourceNodeId,
@NotNull final String targetNodeId,
@NotNull final Map<String, Object> attributes
) {
this.label = label;
this.sourceNodeId = sourceNodeId;
this.targetNodeId = targetNodeId;
this.attributes = attributes;
}

public static Link valueOf(Relationship relationship) {
final String label = relationship.asMap().get("name").toString();
final Map<String, Object> attributes = relationship.asMap().entrySet().stream()
.filter(entry -> !"name".equals(entry.getKey()))
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));

return new Link(label, relationship.startNodeElementId(), relationship.endNodeElementId(), attributes);
}

public String getLabel() {
return label;
}

public String getSourceNodeId() {
return sourceNodeId;
}

public String getTargetNodeId() {
return targetNodeId;
}

public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public boolean equals(final Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
final Link that = (Link) other;
return Objects.equals(getLabel(), that.getLabel()) && Objects.equals(
getSourceNodeId(),
that.getSourceNodeId()
) && Objects.equals(
getTargetNodeId(),
that.getTargetNodeId()
) && Objects.equals(getAttributes(), that.getAttributes());
}

@Override
public int hashCode() {
return Objects.hash(getLabel(), getSourceNodeId(), getTargetNodeId(), getAttributes());
}

@Override
public String toString() {
return String.format("(%s)-%s-(%s)", getSourceNodeId(), getLabel(), getTargetNodeId());
}
}
Loading

0 comments on commit 0d7d5ec

Please sign in to comment.