Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement search endpoint #8

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ curl -v -i -s -k -X POST https://api.paion-data.dev:8444/services \
curl -i -k -X POST https://api.paion-data.dev:8444/services/wilhelm-ws-expand/routes \
--data "paths[]=/wilhelm/expand" \
--data name=wilhelm-ws-expand

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

We should see `HTTP/1.1 201 Created` as signs of success.
Expand Down
153 changes: 87 additions & 66 deletions src/main/java/org/qubitpi/wilhelm/web/endpoints/Neo4JServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,93 @@ public Response getVocabularyByLanguagePaged(
.build();
}

/**
* Search all nodes whose label contains a specified keyword.
*
* @param keyword The provided keyword
*
* @return all nodes whose "name" attribute contains the search keyword
*/
@GET
@Path("/search/{keyword}")
@Produces(MediaType.APPLICATION_JSON)
@SuppressWarnings("MultipleStringLiterals")
public Response search(@NotNull @PathParam("keyword") final String keyword) {
final String query = String.format("MATCH (node) WHERE node.name =~ '.*%s.*' RETURN node", keyword);

return Response
.status(Response.Status.OK)
.entity(executeNonPathQuery(query))
.build();
}

/**
* Runs a cypher query against Neo4J database and return result as a JSON-serializable.
* <p>
* Use this method only if the {@code query} does not involve path, because this method cannot handle query result
* that has path object nested in it
*
* @param query A standard cypher query string
*
* @return query's native result
*/
private Object executeNonPathQuery(@NotNull final String query) {
return executeNativeQuery(query).records()
.stream()
.map(
record -> record.keys()
.stream()
.map(key -> new AbstractMap.SimpleImmutableEntry<>(key, expand(record.get(key))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
)
.collect(Collectors.toList());
}

/**
* Transforms a Neo4J {@link Value} object into a Jackson-serializable Java object.
*
* See https://neo4j.com/docs/java-manual/current/data-types/ for more details
*
* @param value An object graph. Cannot be {@code null}
*
* @return a {@link Map} representation of the object graph and can be Jackson-serialized
*/
private static Object expand(@NotNull final Value value) {
if (isTerminalValue(value)) {
if (value.type().equals(InternalTypeSystem.TYPE_SYSTEM.INTEGER())) {
return value.asInt();
} else if (value.type().equals(InternalTypeSystem.TYPE_SYSTEM.BOOLEAN())) {
return value.asBoolean();
} else {
return value.asString();
}
}

return StreamSupport.stream(value.keys().spliterator(), false)
.map(key -> new AbstractMap.SimpleImmutableEntry<>(key, expand(value.get(key))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

/**
* Returns whether or not a {@link Value} object is the "leaf" node in Jackson serialization.
* <p>
* A "leaf" node is defined to be one of
* <ul>
* <li> integer
* <li> string
* <li> boolean
* </ul>
*
* @param value An object graph. Cannot be {@code null}
*
* @return {@code true} if the object is simply a Jackson-serializable leaf node or {@code false} otherwise
*/
private static boolean isTerminalValue(@NotNull final Value value) {
return value.type().equals(InternalTypeSystem.TYPE_SYSTEM.INTEGER())
|| value.type().equals(InternalTypeSystem.TYPE_SYSTEM.STRING())
|| value.type().equals(InternalTypeSystem.TYPE_SYSTEM.BOOLEAN());
}

/**
* Recursively find all related terms and definitions of a word.
*
Expand Down Expand Up @@ -253,27 +340,6 @@ RETURN path, length(path) AS hops
.build();
}

/**
* Runs a cypher query against Neo4J database and return result as a JSON-serializable.
* <p>
* Use this method only if the {@code query} does not involve path, because this method cannot handle query result
* that has path object nested in it
*
* @param query A standard cypher query string
*
* @return query's native result
*/
private Object executeNonPathQuery(@NotNull final String query) {
return executeNativeQuery(query).records()
.stream()
.map(
record -> record.keys()
.stream()
.map(key -> new AbstractMap.SimpleImmutableEntry<>(key, expand(record.get(key))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
)
.collect(Collectors.toList());
}

/**
* Runs a cypher query against Neo4J database and return the query result unmodified.
Expand All @@ -294,49 +360,4 @@ private EagerResult executeNativeQuery(@NotNull final String query) {
.execute();
}
}

/**
* Transforms a Neo4J {@link Value} object into a Jackson-serializable Java object.
*
* See https://neo4j.com/docs/java-manual/current/data-types/ for more details
*
* @param value An object graph. Cannot be {@code null}
*
* @return a {@link Map} representation of the object graph and can be Jackson-serialized
*/
private static Object expand(@NotNull final Value value) {
if (isTerminalValue(value)) {
if (value.type().equals(InternalTypeSystem.TYPE_SYSTEM.INTEGER())) {
return value.asInt();
} else if (value.type().equals(InternalTypeSystem.TYPE_SYSTEM.BOOLEAN())) {
return value.asBoolean();
} else {
return value.asString();
}
}

return StreamSupport.stream(value.keys().spliterator(), false)
.map(key -> new AbstractMap.SimpleImmutableEntry<>(key, expand(value.get(key))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

/**
* Returns whether or not a {@link Value} object is the "leaf" node in Jackson serialization.
* <p>
* A "leaf" node is defined to be one of
* <ul>
* <li> integer
* <li> string
* <li> boolean
* </ul>
*
* @param value An object graph. Cannot be {@code null}
*
* @return {@code true} if the object is simply a Jackson-serializable leaf node or {@code false} otherwise
*/
private static boolean isTerminalValue(@NotNull final Value value) {
return value.type().equals(InternalTypeSystem.TYPE_SYSTEM.INTEGER())
|| value.type().equals(InternalTypeSystem.TYPE_SYSTEM.STRING())
|| value.type().equals(InternalTypeSystem.TYPE_SYSTEM.BOOLEAN());
}
}
Loading