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

feat(algorithm): support single source shortest path algorithm #285

Merged
merged 24 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
28691fd
feat: ListValue add clear() method
diaohancai Nov 21, 2023
d9c5c84
feat: Source Target Shortest Path
diaohancai Nov 21, 2023
3aff95d
test: test data is complicated
diaohancai Nov 21, 2023
b6c0f4c
test: ListValue.clear unit test
diaohancai Nov 21, 2023
f5a5de9
fix: ring loop
diaohancai Nov 21, 2023
4c37b2d
optimize: message combine
diaohancai Nov 21, 2023
15f95f6
Merge branch 'master' into pr/285
imbajin Dec 4, 2023
6630b82
refactor(algorithm): Single Source Shortest Path
diaohancai Dec 26, 2023
9524900
refactor(algorithm): output json
diaohancai Dec 26, 2023
2804bd7
feat(algorithm): multiple target optimization
diaohancai Dec 26, 2023
cdaba23
feat(core): IdList Merge Combiner
diaohancai Dec 26, 2023
125bba6
chore(algorithm): simple adjustments
diaohancai Dec 31, 2023
9587802
optimization(algorithm): change reachedTargets from IdList to IdSet
diaohancai Jan 1, 2024
01cde50
chore: json style key
diaohancai Jan 22, 2024
6b42bf5
improve: convert id from string to ID with type
diaohancai Jan 25, 2024
55b0ed3
chore: add IdUtilTest unit test
diaohancai Jan 25, 2024
2828bb7
fix: all targets reached
diaohancai Feb 6, 2024
a0a0103
improve: remove unnecessary member var
diaohancai Feb 6, 2024
c4ca8fe
improve: source vertex and target vertex specify idType
diaohancai Feb 6, 2024
174584e
chore: get properties
diaohancai Feb 7, 2024
aa8238d
improve: input vertex id parse
diaohancai Feb 29, 2024
0b1c720
chore: log improvement
diaohancai Feb 29, 2024
6510424
test: apply exception testing
diaohancai Mar 3, 2024
d55c7c7
test: parse empty id throws IllegalArgumentException
diaohancai Mar 3, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,20 @@
package org.apache.hugegraph.computer.algorithm.path.shortest;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.hugegraph.computer.core.common.exception.ComputerException;
import org.apache.hugegraph.computer.core.config.Config;
import org.apache.hugegraph.computer.core.graph.edge.Edge;
import org.apache.hugegraph.computer.core.graph.id.Id;
import org.apache.hugegraph.computer.core.graph.id.IdCategory;
import org.apache.hugegraph.computer.core.graph.value.DoubleValue;
import org.apache.hugegraph.computer.core.graph.value.IdSet;
import org.apache.hugegraph.computer.core.graph.value.Value;
import org.apache.hugegraph.computer.core.graph.vertex.Vertex;
import org.apache.hugegraph.computer.core.util.IdUtil;
import org.apache.hugegraph.computer.core.worker.Computation;
import org.apache.hugegraph.computer.core.worker.ComputationContext;
import org.apache.hugegraph.computer.core.worker.WorkerContext;
Expand All @@ -43,31 +42,36 @@ public class SingleSourceShortestPath implements Computation<SingleSourceShortes

private static final Logger LOG = Log.logger(SingleSourceShortestPath.class);

public static final String OPTION_VERTEX_ID_TYPE = "single_source_shortest_path.vertex_id_type";
public static final String OPTION_SOURCE_ID = "single_source_shortest_path.source_id";
public static final String OPTION_TARGET_ID = "single_source_shortest_path.target_id";
public static final String OPTION_WEIGHT_PROPERTY =
"single_source_shortest_path.weight_property";
public static final String OPTION_DEFAULT_WEIGHT =
"single_source_shortest_path.default_weight";

/**
* id type of vertex.
* string|number|uuid
*/
// todo improve: automatic inference
private String vertexIdTypeStr;
diaohancai marked this conversation as resolved.
Show resolved Hide resolved
private IdCategory vertexIdType;

/**
* source vertex id
*/
private String sourceId;
private String sourceIdStr;
diaohancai marked this conversation as resolved.
Show resolved Hide resolved
private Id sourceId;

/**
* target vertex id.
* 1. single target: one vertex id
* 2. multiple target: multiple vertex ids separated by comma
* 3. all: *
*/
private String targetId;

/**
* cache of targetId
*/
private Set<String> targetIdSet;

private String targetIdStr;
diaohancai marked this conversation as resolved.
Show resolved Hide resolved
private IdSet targetIdSet; // empty when targetIdStr == "*"
/**
* target quantity type
*/
Expand All @@ -89,7 +93,7 @@ public class SingleSourceShortestPath implements Computation<SingleSourceShortes
/**
* reached targets
*/
private IdSet reachedTargets;
private IdSet reachedTargets; // empty when targetIdStr == "*"

@Override
public String category() {
Expand All @@ -103,23 +107,30 @@ public String name() {

@Override
public void init(Config config) {
this.sourceId = config.getString(OPTION_SOURCE_ID, "");
if (StringUtils.isBlank(this.sourceId)) {
this.vertexIdTypeStr = config.getString(OPTION_VERTEX_ID_TYPE, "");
this.vertexIdType = IdCategory.parse(this.vertexIdTypeStr);

this.sourceIdStr = config.getString(OPTION_SOURCE_ID, "");
if (StringUtils.isBlank(this.sourceIdStr)) {
throw new ComputerException("The param '%s' must not be blank", OPTION_SOURCE_ID);
}
this.sourceId = IdUtil.parseId(this.vertexIdType, this.sourceIdStr);

this.targetId = config.getString(OPTION_TARGET_ID, "");
if (StringUtils.isBlank(this.targetId)) {
this.targetIdStr = config.getString(OPTION_TARGET_ID, "");
if (StringUtils.isBlank(this.targetIdStr)) {
throw new ComputerException("The param '%s' must not be blank", OPTION_TARGET_ID);
}

// remove spaces
this.targetId = Arrays.stream(this.targetId.split(","))
.map(e -> e.trim())
.collect(Collectors.joining(","));
// cache targetId
this.targetIdSet = new HashSet<>(Arrays.asList(this.targetId.split(",")));
this.targetIdStr = Arrays.stream(this.targetIdStr.split(","))
.map(e -> e.trim())
.collect(Collectors.joining(","));
this.targetQuantityType = this.getQuantityType();
if (this.targetQuantityType != QuantityType.ALL) {
this.targetIdSet = new IdSet();
for (String targetIdStr : this.targetIdStr.split(",")) {
targetIdSet.add(IdUtil.parseId(this.vertexIdType, targetIdStr));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems the targets id should be different types? like [111, "abc", "222", 333, U"uuid..."]

}
}

this.weightProperty = config.getString(OPTION_WEIGHT_PROPERTY, "");

Expand All @@ -138,24 +149,25 @@ public void compute0(ComputationContext context, Vertex vertex) {
vertex.value(value);

// start from source vertex
if (!this.idEquals(vertex, this.sourceId)) {
if (!this.sourceId.equals(vertex.id())) {
vertex.inactivate();
return;
}
value.zeroDistance(); // source vertex

// single target && source vertex == target vertex
// single target && source == target
if (this.targetQuantityType == QuantityType.SINGLE &&
this.sourceId.equals(this.targetId)) {
LOG.debug("source vertex {} equals target vertex {}", this.sourceId, this.targetId);
this.sourceIdStr.equals(this.targetIdStr)) {
LOG.debug("source vertex {} equals target vertex {}",
this.sourceIdStr, this.targetIdStr);
vertex.inactivate();
return;
}

if (vertex.numEdges() <= 0) {
// isolated vertex
LOG.debug("source vertex {} can not reach target vertex {}",
this.sourceId, this.targetId);
this.sourceIdStr, this.targetIdStr);
vertex.inactivate();
return;
}
Expand Down Expand Up @@ -223,22 +235,15 @@ public void afterSuperstep(WorkerContext context) {
* get QuantityType by this.targetId
*/
private QuantityType getQuantityType() {
if (this.targetId.equals("*")) {
if (this.targetIdStr.equals("*")) {
return QuantityType.ALL;
} else if (this.targetId.contains(",")) {
} else if (this.targetIdStr.contains(",")) {
return QuantityType.MULTIPLE;
} else {
return QuantityType.SINGLE;
}
}

/**
* determine whether vertex.id and id are equal
*/
private boolean idEquals(Vertex vertex, String id) {
return vertex.id().value().toString().equals(id);
}

/**
* get the weight of an edge by its weight property
*/
Expand Down Expand Up @@ -267,32 +272,26 @@ private double getEdgeWeight(Edge edge) {
* determine whether vertex is one of the target
*/
private boolean isTarget(Vertex vertex) {
return this.targetIdSet.contains(vertex.id().toString());
return this.targetQuantityType != QuantityType.ALL &&
this.targetIdSet.contains(vertex.id());
}

/**
* determine whether all targets reached
*/
private boolean isAllTargetsReached(Vertex vertex) {
if (this.targetQuantityType == QuantityType.SINGLE &&
this.idEquals(vertex, this.targetId)) {
return true;
if (this.targetQuantityType == QuantityType.ALL) {
return false;
}

if (this.targetQuantityType == QuantityType.MULTIPLE) {
if (this.targetIdSet.size() == this.reachedTargets.size()) {
List<String> reachedTargets = this.reachedTargets.value()
.stream().map(Id::toString)
.collect(Collectors.toList());
for (String targetId : this.targetIdSet) {
if (!reachedTargets.contains(targetId)) {
return false;
}
if (this.targetIdSet.size() == this.reachedTargets.size()) {
for (Id targetId : this.targetIdSet.value()) {
if (!this.reachedTargets.contains(targetId)) {
return false;
}
return true;
}
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You 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.apache.hugegraph.computer.core.graph.id;

import org.apache.commons.lang3.StringUtils;

public enum IdCategory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why not reuse IdType

Copy link
Contributor Author

@diaohancai diaohancai Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdType: LONG, UTF8, UUID.
IdCategory: NUMBER, STRING, UUID.
I suppose that IdType is too technical and IdCategory is more user-friendly.
In fact, they correspond one to one.·

Copy link
Contributor Author

@diaohancai diaohancai Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove IdCategory, reuse IdType if necessary.


NUMBER,
STRING,
UUID,
;

public static IdCategory parse(String code) {
if (StringUtils.isBlank(code)) {
return null;
}

for (IdCategory idAlias : IdCategory.values()) {
if (idAlias.name().equals(code.toUpperCase())) {
return idAlias;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ public static Id createId(IdType type) {
}
}

public static Id parseId(IdType type, Object value) {
try {
switch (type) {
case LONG:
return (Id) BYTES_ID_LONG_METHOD.invoke(null, value);
case UTF8:
return (Id) BYTES_ID_STRING_METHOD.invoke(null, value);
case UUID:
return (Id) BYTES_ID_UUID_METHOD.invoke(null, value);
default:
throw new ComputerException("Can't parse Id for %s", type.name());
diaohancai marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (Exception e) {
throw new ComputerException("Failed to parse Id", e);
diaohancai marked this conversation as resolved.
Show resolved Hide resolved
}
}

public static Id createId() {
try {
return (Id) BYTES_ID_CONSTRUCTOR.newInstance();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You 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.apache.hugegraph.computer.core.util;

import java.util.UUID;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.hugegraph.computer.core.common.exception.ComputerException;
import org.apache.hugegraph.computer.core.graph.id.Id;
import org.apache.hugegraph.computer.core.graph.id.IdCategory;
import org.apache.hugegraph.computer.core.graph.id.IdFactory;
import org.apache.hugegraph.computer.core.graph.id.IdType;

public class IdUtil {

private static String UUID_REGEX = "^[0-9a-fA-F]{8}-" +
"[0-9a-fA-F]{4}-" +
"[0-9a-fA-F]{4}-" +
"[0-9a-fA-F]{4}-" +
"[0-9a-fA-F]{12}$";
private static Pattern P = Pattern.compile(UUID_REGEX);

public static Id parseId(String idStr) {
if (StringUtils.isBlank(idStr)) {
throw new ComputerException("Can't parse Id for empty string");
}

if (StringUtils.isNumeric(idStr)) {
return IdFactory.parseId(IdType.LONG, Long.valueOf(idStr));
} else if (P.matcher(idStr).matches()) {
return IdFactory.parseId(IdType.UUID, UUID.fromString(idStr));
} else {
return IdFactory.parseId(IdType.UTF8, idStr);
}
}

public static Id parseId(IdCategory idCategory, String idStr) {
if (StringUtils.isBlank(idStr)) {
throw new ComputerException("Can't parse Id for empty string");
}

if (idCategory == null) {
// automatic inference
return parseId(idStr);
}

switch (idCategory) {
case NUMBER:
return IdFactory.parseId(IdType.LONG, Long.valueOf(idStr));
case UUID:
return IdFactory.parseId(IdType.UUID, UUID.fromString(idStr));
default:
return IdFactory.parseId(IdType.UTF8, idStr);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ public String value(org.apache.hugegraph.computer.core.graph.vertex.Vertex verte

LOG.info("source vertex {} to target vertex {}, " +
"shortest path: {}, total weight: {}",
SOURCE_ID, TARGET_ID, map.get("path"), map.get("totalWeight"));
SOURCE_ID, TARGET_ID, map.get("path"), map.get("total_weight"));
Assert.assertEquals(map.get("path"), SHORTEST_PATH);
Assert.assertEquals(map.get("totalWeight"), TOTAL_WEIGHT);
Assert.assertEquals(map.get("total_weight"), TOTAL_WEIGHT);
}
return json;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,12 @@ public void testCreateIdFromType() {
Assert.assertEquals(BytesId.of(new UUID(0L, 0L)),
IdFactory.createId(IdType.UUID));
}

@Test
public void testParseId() {
UUID uuid = UUID.fromString("3b676b77-c484-4ba6-b627-8c040bc42863");
Assert.assertEquals(IdType.LONG, IdFactory.parseId(IdType.LONG, 222).idType());
Assert.assertEquals(IdType.UTF8, IdFactory.parseId(IdType.UTF8, "aaa222").idType());
Assert.assertEquals(IdType.UUID, IdFactory.parseId(IdType.UUID, uuid).idType());
}
}
Loading
Loading