diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/QuantityType.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/QuantityType.java new file mode 100644 index 000000000..7cf722e73 --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/QuantityType.java @@ -0,0 +1,25 @@ +/* + * 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.algorithm.path.shortest; + +public enum QuantityType { + + SINGLE, + MULTIPLE, + ALL +} diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPath.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPath.java new file mode 100644 index 000000000..c9b49cfde --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPath.java @@ -0,0 +1,281 @@ +/* + * 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.algorithm.path.shortest; + +import java.util.Arrays; +import java.util.Iterator; +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.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; +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; + +public class SingleSourceShortestPath implements Computation { + + private static final Logger LOG = Log.logger(SingleSourceShortestPath.class); + + 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"; + + /** + * source vertex id. + */ + private Id sourceId; + + /** + * target vertex id. + * 1. single target: A + * 2. multiple target: A, B, C + * 3. all: * + */ + private IdSet targetIdSet; // empty when targetId is all + /** + * target quantity type + */ + private QuantityType targetQuantityType; + + /** + * weight property. + * weight value must be a positive number. + */ + private String weightProperty; + + /** + * default weight. + * default 1 + */ + private Double defaultWeight; + + //****************** global data ******************// + /** + * reached targets + */ + private IdSet reachedTargets; // empty when targetId is all + + @Override + public String category() { + return "path"; + } + + @Override + public String name() { + return "single_source_shortest_path"; + } + + @Override + public void init(Config config) { + String sourceIdStr = config.getString(OPTION_SOURCE_ID, ""); + if (StringUtils.isBlank(sourceIdStr)) { + throw new ComputerException("The param '%s' must not be blank", OPTION_SOURCE_ID); + } + this.sourceId = IdUtil.parseId(sourceIdStr); + + String targetIdStr = config.getString(OPTION_TARGET_ID, ""); + if (StringUtils.isBlank(targetIdStr)) { + throw new ComputerException("The param '%s' must not be blank", OPTION_TARGET_ID); + } + // remove spaces + targetIdStr = Arrays.stream(targetIdStr.split(",")) + .map(e -> e.trim()) + .collect(Collectors.joining(",")); + this.targetQuantityType = this.getQuantityType(targetIdStr); + if (this.targetQuantityType != QuantityType.ALL) { + this.targetIdSet = new IdSet(); + for (String targetId : targetIdStr.split(",")) { + targetIdSet.add(IdUtil.parseId(targetId)); + } + } + + this.weightProperty = config.getString(OPTION_WEIGHT_PROPERTY, ""); + + this.defaultWeight = config.getDouble(OPTION_DEFAULT_WEIGHT, 1); + if (this.defaultWeight <= 0) { + throw new ComputerException("The param '%s' must be greater than 0, " + + "actual got '%s'", + OPTION_DEFAULT_WEIGHT, this.defaultWeight); + } + } + + @Override + public void compute0(ComputationContext context, Vertex vertex) { + SingleSourceShortestPathValue value = new SingleSourceShortestPathValue(); + value.unreachable(); + vertex.value(value); + + // start from source vertex + if (!this.sourceId.equals(vertex.id())) { + vertex.inactivate(); + return; + } + value.zeroDistance(); // source vertex + + // single target && source == target + if (this.targetQuantityType == QuantityType.SINGLE && + this.targetIdSet.contains(this.sourceId)) { + LOG.debug("source vertex equals target vertex: {}", this.sourceId); + vertex.inactivate(); + return; + } + + if (vertex.numEdges() <= 0) { + // isolated vertex + LOG.debug("The source vertex is isolated: {}", this.sourceId); + vertex.inactivate(); + return; + } + + vertex.edges().forEach(edge -> { + SingleSourceShortestPathValue message = new SingleSourceShortestPathValue(); + message.addToPath(vertex, this.getEdgeWeight(edge)); + + context.sendMessage(edge.targetId(), message); + }); + + vertex.inactivate(); + } + + @Override + public void compute(ComputationContext context, Vertex vertex, + Iterator messages) { + if (this.isTarget(vertex) && !this.reachedTargets.contains(vertex.id())) { + // reached targets + this.reachedTargets.add(vertex.id()); + } + + while (messages.hasNext()) { + SingleSourceShortestPathValue message = messages.next(); + SingleSourceShortestPathValue value = vertex.value(); + + if (message.totalWeight() < value.totalWeight()) { + // find a shorter path + value.shorterPath(vertex, message.path(), message.totalWeight()); + } else { + continue; + } + + // target vertex finds all targets reached or nowhere to go + if ((this.isTarget(vertex) && this.isAllTargetsReached(vertex)) || + vertex.numEdges() <= 0) { + continue; + } + + vertex.edges().forEach(edge -> { + SingleSourceShortestPathValue forwardMessage = new SingleSourceShortestPathValue(); + forwardMessage.addToPath(value.path(), + value.totalWeight() + this.getEdgeWeight(edge)); + + context.sendMessage(edge.targetId(), forwardMessage); + }); + } + + vertex.inactivate(); + } + + @Override + public void beforeSuperstep(WorkerContext context) { + this.reachedTargets = context.aggregatedValue( + SingleSourceShortestPathMaster.SINGLE_SOURCE_SHORTEST_PATH_REACHED_TARGETS); + } + + @Override + public void afterSuperstep(WorkerContext context) { + context.aggregateValue( + SingleSourceShortestPathMaster.SINGLE_SOURCE_SHORTEST_PATH_REACHED_TARGETS, + this.reachedTargets); + } + + /** + * get quantityType by targetId + */ + private QuantityType getQuantityType(String targetIdStr) { + if (targetIdStr.equals("*")) { + return QuantityType.ALL; + } else if (targetIdStr.contains(",")) { + return QuantityType.MULTIPLE; + } else { + return QuantityType.SINGLE; + } + } + + /** + * get the weight of an edge by its weight property + */ + private double getEdgeWeight(Edge edge) { + double weight = this.defaultWeight; + + Value property = edge.property(this.weightProperty); + if (property != null) { + if (!property.isNumber()) { + throw new ComputerException("The value of %s must be a numeric value, " + + "actual got '%s'", + this.weightProperty, property.string()); + } + + weight = ((DoubleValue) property).doubleValue(); + if (weight <= 0) { + throw new ComputerException("The value of %s must be greater than 0, " + + "actual got '%s'", + this.weightProperty, property.string()); + } + } + return weight; + } + + /** + * determine whether vertex is one of the target + */ + private boolean isTarget(Vertex vertex) { + return this.targetQuantityType != QuantityType.ALL && + this.targetIdSet.contains(vertex.id()); + } + + /** + * determine whether all targets reached + */ + private boolean isAllTargetsReached(Vertex vertex) { + if (this.targetQuantityType == QuantityType.ALL) { + 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 false; + } +} diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathCombiner.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathCombiner.java new file mode 100644 index 000000000..78ea2cd26 --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathCombiner.java @@ -0,0 +1,30 @@ +/* + * 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.algorithm.path.shortest; + +import org.apache.hugegraph.computer.core.combiner.Combiner; + +public class SingleSourceShortestPathCombiner implements Combiner { + + @Override + public void combine(SingleSourceShortestPathValue v1, SingleSourceShortestPathValue v2, + SingleSourceShortestPathValue result) { + SingleSourceShortestPathValue shorter = v2.totalWeight() < v1.totalWeight() ? v2 : v1; + result.copy(shorter); + } +} diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathMaster.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathMaster.java new file mode 100644 index 000000000..358d77ad4 --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathMaster.java @@ -0,0 +1,47 @@ +/* + * 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.algorithm.path.shortest; + +import org.apache.hugegraph.computer.core.combiner.IdSetMergeCombiner; +import org.apache.hugegraph.computer.core.graph.value.ValueType; +import org.apache.hugegraph.computer.core.master.MasterComputation; +import org.apache.hugegraph.computer.core.master.MasterComputationContext; +import org.apache.hugegraph.computer.core.master.MasterContext; + +public class SingleSourceShortestPathMaster implements MasterComputation { + + public static final String SINGLE_SOURCE_SHORTEST_PATH_REACHED_TARGETS = + "single_source_shortest_path.reached_targets"; + + @Override + public void init(MasterContext context) { + context.registerAggregator(SINGLE_SOURCE_SHORTEST_PATH_REACHED_TARGETS, + ValueType.ID_SET, + IdSetMergeCombiner.class); + } + + @Override + public void close(MasterContext context) { + // pass + } + + @Override + public boolean compute(MasterComputationContext context) { + return true; + } +} diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathOutput.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathOutput.java new file mode 100644 index 000000000..4a646cf41 --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathOutput.java @@ -0,0 +1,48 @@ +/* + * 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.algorithm.path.shortest; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.hugegraph.computer.core.graph.vertex.Vertex; +import org.apache.hugegraph.computer.core.output.hg.HugeGraphOutput; +import org.apache.hugegraph.util.JsonUtil; + +public class SingleSourceShortestPathOutput extends HugeGraphOutput { + + @Override + protected void prepareSchema() { + this.client().schema().propertyKey(this.name()) + .asText() + .writeType(this.writeType()) + .valueList() + .ifNotExist() + .create(); + } + + @Override + protected String value(Vertex vertex) { + SingleSourceShortestPathValue value = vertex.value(); + + Map map = new HashMap(); + map.put("path", value.path().toString()); + map.put("total_weight", value.totalWeight()); + return JsonUtil.toJson(map); + } +} diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathParams.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathParams.java new file mode 100644 index 000000000..048f7314d --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathParams.java @@ -0,0 +1,48 @@ +/* + * 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.algorithm.path.shortest; + +import java.util.Map; + +import org.apache.hugegraph.computer.algorithm.AlgorithmParams; +import org.apache.hugegraph.computer.core.config.ComputerOptions; + +public class SingleSourceShortestPathParams implements AlgorithmParams { + + @Override + public void setAlgorithmParameters(Map params) { + this.setIfAbsent(params, ComputerOptions.MASTER_COMPUTATION_CLASS, + SingleSourceShortestPathMaster.class.getName()); + this.setIfAbsent(params, ComputerOptions.WORKER_COMPUTATION_CLASS, + SingleSourceShortestPath.class.getName()); + this.setIfAbsent(params, ComputerOptions.ALGORITHM_MESSAGE_CLASS, + SingleSourceShortestPathValue.class.getName()); + this.setIfAbsent(params, ComputerOptions.WORKER_COMBINER_CLASS, + SingleSourceShortestPathCombiner.class.getName()); + this.setIfAbsent(params, ComputerOptions.ALGORITHM_RESULT_CLASS, + SingleSourceShortestPathValue.class.getName()); + this.setIfAbsent(params, ComputerOptions.INPUT_FILTER_CLASS, + EXTRACTALLPROPERTYINPUTFILTER_CLASS_NAME); + this.setIfAbsent(params, ComputerOptions.OUTPUT_CLASS, + SingleSourceShortestPathOutput.class.getName()); + + this.setIfAbsent(params, SingleSourceShortestPath.OPTION_SOURCE_ID, ""); + this.setIfAbsent(params, SingleSourceShortestPath.OPTION_TARGET_ID, ""); + this.setIfAbsent(params, SingleSourceShortestPath.OPTION_WEIGHT_PROPERTY, ""); + } +} diff --git a/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathValue.java b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathValue.java new file mode 100644 index 000000000..f6ca1c166 --- /dev/null +++ b/computer-algorithm/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathValue.java @@ -0,0 +1,103 @@ +/* + * 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.algorithm.path.shortest; + +import java.io.IOException; +import java.util.List; + +import org.apache.hugegraph.computer.core.graph.value.DoubleValue; +import org.apache.hugegraph.computer.core.graph.value.IdList; +import org.apache.hugegraph.computer.core.graph.value.Value; +import org.apache.hugegraph.computer.core.graph.vertex.Vertex; +import org.apache.hugegraph.computer.core.io.RandomAccessInput; +import org.apache.hugegraph.computer.core.io.RandomAccessOutput; + +public class SingleSourceShortestPathValue implements Value.CustomizeValue> { + + /** + * path + */ + private final IdList path; + + /** + * total weight. + * infinity means unreachable. + */ + private final DoubleValue totalWeight; + + public SingleSourceShortestPathValue() { + this.path = new IdList(); + this.totalWeight = new DoubleValue(0); + } + + @Override + public void read(RandomAccessInput in) throws IOException { + this.path.read(in); + this.totalWeight.read(in); + } + + @Override + public void write(RandomAccessOutput out) throws IOException { + this.path.write(out); + this.totalWeight.write(out); + } + + @Override + public List value() { + return this.path.value(); + } + + public void unreachable() { + this.totalWeight.value(Double.POSITIVE_INFINITY); + } + + public void zeroDistance() { + this.totalWeight.value(0); + } + + public void shorterPath(Vertex vertex, IdList path, double weight) { + this.path.clear(); + this.path.addAll(path.copy().values()); + this.path.add(vertex.id()); + this.totalWeight.value(weight); + } + + public void addToPath(Vertex vertex, double weight) { + this.path.add(vertex.id()); + this.totalWeight.value(weight); + } + + public void addToPath(IdList path, double weight) { + this.path.addAll(path.copy().values()); + this.totalWeight.value(weight); + } + + public IdList path() { + return this.path; + } + + public double totalWeight() { + return this.totalWeight.doubleValue(); + } + + public void copy(SingleSourceShortestPathValue value) { + this.path.clear(); + this.path.addAll(value.path().copy().values()); + this.totalWeight.value(value.totalWeight()); + } +} diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/combiner/IdListMergeCombiner.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/combiner/IdListMergeCombiner.java new file mode 100644 index 000000000..3cc18444e --- /dev/null +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/combiner/IdListMergeCombiner.java @@ -0,0 +1,39 @@ +/* + * 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.combiner; + +import org.apache.hugegraph.computer.core.graph.id.Id; +import org.apache.hugegraph.computer.core.graph.value.IdList; + +public class IdListMergeCombiner implements Combiner { + + @Override + public void combine(IdList v1, IdList v2, IdList result) { + // merge + for (Id id : v1.values()) { + if (!result.contains(id)) { + result.add(id); + } + } + for (Id id : v2.values()) { + if (!result.contains(id)) { + result.add(id); + } + } + } +} diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/combiner/IdSetMergeCombiner.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/combiner/IdSetMergeCombiner.java new file mode 100644 index 000000000..5698da940 --- /dev/null +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/combiner/IdSetMergeCombiner.java @@ -0,0 +1,30 @@ +/* + * 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.combiner; + +import org.apache.hugegraph.computer.core.graph.value.IdSet; + +public class IdSetMergeCombiner implements Combiner { + + @Override + public void combine(IdSet v1, IdSet v2, IdSet result) { + // merge + result.addAll(v1); + result.addAll(v2); + } +} diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactory.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactory.java index 2f9284e7b..106873d6e 100644 --- a/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactory.java +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactory.java @@ -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("Unexpected id type %s", type.name()); + } + } catch (Exception e) { + throw new ComputerException("Failed to parse %s Id: '%s'", e, type, value); + } + } + public static Id createId() { try { return (Id) BYTES_ID_CONSTRUCTOR.newInstance(); diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/IdSet.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/IdSet.java index 7b4267f80..9e90ad232 100644 --- a/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/IdSet.java +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/IdSet.java @@ -107,4 +107,8 @@ public Set value() { public int compareTo(Value o) { throw new UnsupportedOperationException("compareTo"); } + + public int size() { + return this.values.size(); + } } diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValue.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValue.java index c9f5d05ed..268929dd4 100644 --- a/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValue.java +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValue.java @@ -117,6 +117,10 @@ public int size() { return this.values.size(); } + public void clear() { + this.values.clear(); + } + @Override public List value() { List list = new ArrayList<>(this.values.size()); @@ -162,8 +166,7 @@ public void read(RandomAccessInput in) throws IOException { this.read(in, true); } - protected void read(RandomAccessInput in, boolean readElemType) - throws IOException { + protected void read(RandomAccessInput in, boolean readElemType) throws IOException { int size = in.readInt(); if (readElemType) { this.elemType = SerialEnum.fromCode(ValueType.class, in.readByte()); @@ -187,8 +190,7 @@ public void write(RandomAccessOutput out) throws IOException { this.write(out, true); } - protected void write(RandomAccessOutput out, boolean writeElemType) - throws IOException { + protected void write(RandomAccessOutput out, boolean writeElemType) throws IOException { out.writeInt(this.values.size()); if (writeElemType) { out.writeByte(this.elemType.code()); diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/util/IdUtil.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/util/IdUtil.java new file mode 100644 index 000000000..1963909e4 --- /dev/null +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/util/IdUtil.java @@ -0,0 +1,53 @@ +/* + * 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 org.apache.commons.lang3.StringUtils; +import org.apache.hugegraph.computer.core.graph.id.Id; +import org.apache.hugegraph.computer.core.graph.id.IdFactory; +import org.apache.hugegraph.computer.core.graph.id.IdType; +import org.apache.hugegraph.util.JsonUtil; + +public class IdUtil { + + public static Id parseId(String idStr) { + if (StringUtils.isBlank(idStr)) { + throw new IllegalArgumentException("Can't parse Id for empty string"); + } + + try { + if (idStr.startsWith("U\"")) { + return IdFactory.parseId(IdType.UUID, + UUID.fromString(idStr.substring(1) + .replaceAll("\"", ""))); + } + + Object id = JsonUtil.fromJson(idStr, Object.class); + idStr = idStr.replaceAll("\"", ""); + return id instanceof Number ? + IdFactory.parseId(IdType.LONG, Long.valueOf(idStr)) : + IdFactory.parseId(IdType.UTF8, idStr); + } catch (Exception e) { + throw new IllegalArgumentException(String.format( + "The vertex id must be formatted as Number/String/UUID" + + ", but got '%s'", idStr)); + } + } +} diff --git a/computer-api/src/main/java/org/apache/hugegraph/computer/core/util/JsonUtilExt.java b/computer-api/src/main/java/org/apache/hugegraph/computer/core/util/JsonUtilExt.java new file mode 100644 index 000000000..3304881e2 --- /dev/null +++ b/computer-api/src/main/java/org/apache/hugegraph/computer/core/util/JsonUtilExt.java @@ -0,0 +1,46 @@ +/* + * 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.io.IOException; +import java.util.List; + +import org.apache.hugegraph.rest.SerializeException; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +// TODO: move to org.apache.hugegraph.util.JsonUtil later +public class JsonUtilExt { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static List fromJson2List(String json, Class clazz) { + try { + return MAPPER.readValue(json, getCollectionType(List.class, clazz)); + } catch (IOException e) { + throw new SerializeException("Failed to deserialize json '%s'", + e, json); + } + } + + private static JavaType getCollectionType(Class collectionClass, + Class... elementClasses) { + return MAPPER.getTypeFactory().constructParametricType(collectionClass, elementClasses); + } +} diff --git a/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/AlgorithmTestSuite.java b/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/AlgorithmTestSuite.java index 41e9174ce..f5124d010 100644 --- a/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/AlgorithmTestSuite.java +++ b/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/AlgorithmTestSuite.java @@ -28,6 +28,7 @@ import org.apache.hugegraph.computer.algorithm.community.wcc.WccTest; import org.apache.hugegraph.computer.algorithm.path.rings.RingsDetectionTest; import org.apache.hugegraph.computer.algorithm.path.rings.RingsDetectionWithFilterTest; +import org.apache.hugegraph.computer.algorithm.path.shortest.SingleSourceShortestPathTest; import org.apache.hugegraph.computer.algorithm.sampling.RandomWalkTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -45,7 +46,8 @@ ClusteringCoefficientTest.class, ClosenessCentralityTest.class, BetweennessCentralityTest.class, - RandomWalkTest.class + RandomWalkTest.class, + SingleSourceShortestPathTest.class }) public class AlgorithmTestSuite { } diff --git a/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathTest.java b/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathTest.java new file mode 100644 index 000000000..4b2b466a5 --- /dev/null +++ b/computer-test/src/main/java/org/apache/hugegraph/computer/algorithm/path/shortest/SingleSourceShortestPathTest.java @@ -0,0 +1,148 @@ +/* + * 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.algorithm.path.shortest; + +import java.util.Map; + +import org.apache.hugegraph.computer.algorithm.AlgorithmTestBase; +import org.apache.hugegraph.computer.core.config.ComputerOptions; +import org.apache.hugegraph.driver.GraphManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.SchemaManager; +import org.apache.hugegraph.structure.constant.T; +import org.apache.hugegraph.structure.graph.Vertex; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.hugegraph.util.Log; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; + +public class SingleSourceShortestPathTest extends AlgorithmTestBase { + + public static final String VL = "city"; + public static final String EL = "road"; + public static final String PROPERTY_KEY = "distance"; + + public static final String SOURCE_ID = "\"A\""; + public static final String TARGET_ID = "\"E\""; + public static final String SHORTEST_PATH = "[A, C, B, E]"; + public static final double TOTAL_WEIGHT = 28; + + + @BeforeClass + public static void setup() { + clearAll(); + + HugeClient client = client(); + SchemaManager schema = client.schema(); + + schema.propertyKey(PROPERTY_KEY) + .asDouble() + .ifNotExist() + .create(); + schema.vertexLabel(VL) + .useCustomizeStringId() + .ifNotExist() + .create(); + schema.edgeLabel(EL) + .sourceLabel(VL) + .targetLabel(VL) + .properties(PROPERTY_KEY) + .nullableKeys(PROPERTY_KEY) + .ifNotExist() + .create(); + + GraphManager graph = client.graph(); + Vertex vA = graph.addVertex(T.LABEL, VL, T.ID, "A"); + Vertex vB = graph.addVertex(T.LABEL, VL, T.ID, "B"); + Vertex vC = graph.addVertex(T.LABEL, VL, T.ID, "C"); + Vertex vD = graph.addVertex(T.LABEL, VL, T.ID, "D"); + Vertex vE = graph.addVertex(T.LABEL, VL, T.ID, "E"); + Vertex vF = graph.addVertex(T.LABEL, VL, T.ID, "F"); + + Vertex vJ = graph.addVertex(T.LABEL, VL, T.ID, "J"); + Vertex vK = graph.addVertex(T.LABEL, VL, T.ID, "K"); + + vA.addEdge(EL, vC, PROPERTY_KEY, 5); + vA.addEdge(EL, vD, PROPERTY_KEY, 30); + + vB.addEdge(EL, vA, PROPERTY_KEY, 2); + vB.addEdge(EL, vE, PROPERTY_KEY, 8); + + vC.addEdge(EL, vB, PROPERTY_KEY, 15); + vC.addEdge(EL, vF, PROPERTY_KEY, 7); + + vE.addEdge(EL, vB, PROPERTY_KEY, 6); + vE.addEdge(EL, vD, PROPERTY_KEY, 4); + + vF.addEdge(EL, vC, PROPERTY_KEY, 8); + vF.addEdge(EL, vD, PROPERTY_KEY, 10); + vF.addEdge(EL, vE, PROPERTY_KEY, 18); + + vJ.addEdge(EL, vK); + } + + @AfterClass + public static void clear() { + clearAll(); + } + + @Test + public void testRunAlgorithm() throws InterruptedException { + runAlgorithm(SingleSourceShortestPathTestParams.class.getName()); + } + + public static class SingleSourceShortestPathTestParams extends SingleSourceShortestPathParams { + + @Override + public void setAlgorithmParameters(Map params) { + this.setIfAbsent(params, ComputerOptions.OUTPUT_CLASS, + SingleSourceShortestPathTestOutput.class.getName()); + this.setIfAbsent(params, SingleSourceShortestPath.OPTION_SOURCE_ID, SOURCE_ID); + this.setIfAbsent(params, SingleSourceShortestPath.OPTION_TARGET_ID, TARGET_ID); + this.setIfAbsent(params, SingleSourceShortestPath.OPTION_WEIGHT_PROPERTY, + SingleSourceShortestPathTest.PROPERTY_KEY); + + super.setAlgorithmParameters(params); + } + } + + public static class SingleSourceShortestPathTestOutput extends SingleSourceShortestPathOutput { + + private static final Logger LOG = Log.logger(SingleSourceShortestPathTestOutput.class); + + @Override + public String value(org.apache.hugegraph.computer.core.graph.vertex.Vertex vertex) { + String json = super.value(vertex); + + if (vertex.id().value().toString().equals(TARGET_ID)) { + Map map = JsonUtil.fromJson(json, Map.class); + + LOG.info("source vertex to target vertex: {}, {}, " + + "shortest path: {}, total weight: {}", + SOURCE_ID, TARGET_ID, + map.get("path"), map.get("total_weight")); + Assert.assertEquals(map.get("path"), SHORTEST_PATH); + Assert.assertEquals(map.get("total_weight"), TOTAL_WEIGHT); + } + return json; + } + } +} diff --git a/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactoryTest.java b/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactoryTest.java index c95ea8bf9..6fa03fc1b 100644 --- a/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactoryTest.java +++ b/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/id/IdFactoryTest.java @@ -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()); + } } diff --git a/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValueTest.java b/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValueTest.java index 3b5310af5..99c61288c 100644 --- a/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValueTest.java +++ b/computer-test/src/main/java/org/apache/hugegraph/computer/core/graph/value/ListValueTest.java @@ -195,6 +195,18 @@ public void testSize() { Assert.assertEquals(2, value2.size()); } + @Test + public void testClear() { + ListValue value = new ListValue<>(ValueType.INT); + value.add(new IntValue(101)); + value.add(new IntValue(102)); + value.add(new IntValue(103)); + Assert.assertEquals(3, value.size()); + + value.clear(); + Assert.assertEquals(0, value.size()); + } + @Test public void testValues() { ListValue value1 = new ListValue<>(ValueType.INT); diff --git a/computer-test/src/main/java/org/apache/hugegraph/computer/core/util/IdUtilTest.java b/computer-test/src/main/java/org/apache/hugegraph/computer/core/util/IdUtilTest.java new file mode 100644 index 000000000..739690820 --- /dev/null +++ b/computer-test/src/main/java/org/apache/hugegraph/computer/core/util/IdUtilTest.java @@ -0,0 +1,56 @@ +/* + * 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 org.apache.hugegraph.computer.core.graph.id.IdType; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; + +public class IdUtilTest { + + @Test + public void testParseId() { + String idUtf8WithString = "\"abc\""; + String idUtf8WithNumber = "\"222\""; + String idLong = "222"; + String idUuid = "U\"3b676b77-c484-4ba6-b627-8c040bc42863\""; + + String idNull = null; + String idEmpty = ""; + String idDouble = "1.23"; + String idUuidInvalid = "U\"123\""; + + Assert.assertEquals(IdType.UTF8, IdUtil.parseId(idUtf8WithString).idType()); + Assert.assertEquals(IdType.UTF8, IdUtil.parseId(idUtf8WithNumber).idType()); + Assert.assertEquals(IdType.LONG, IdUtil.parseId(idLong).idType()); + Assert.assertEquals(IdType.UUID, IdUtil.parseId(idUuid).idType()); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + IdUtil.parseId(idNull).idType(); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + IdUtil.parseId(idEmpty).idType(); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + IdUtil.parseId(idDouble).idType(); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + IdUtil.parseId(idUuidInvalid).idType(); + }); + } +}