diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtil.scala index 2c1b27a426..8e42d468a6 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtil.scala @@ -39,27 +39,55 @@ object TopologicalSortUtil { def findAllTopologicalOrderPermutations[T](graph: Graph[T, DiHyperEdge]): Set[Vector[Graph[T, DiHyperEdge]#NodeT]] = { type NodeT = Graph[T, DiHyperEdge]#NodeT - def findPermutations(listOfLists: List[Vector[NodeT]]): List[Vector[NodeT]] = { - def makePermutations(next: Vector[NodeT], acc: List[Vector[NodeT]]): List[Vector[NodeT]] = { - next.permutations.toList.flatMap(i => acc.map(j => j ++ i)) - } + /** + * Finds all possible topological order permutations of a graph using layer information. This method considers all + * permutations of the topological order regarding the leaf nodes. + * + * First, for each permutation of leaf nodes, find the correct order of parent nodes w.r.t incoming edges. + * For example, consider a graph that its topological order has 3 layers; i.e. layer 0 contains root nodes, and layer 2 contains leaf nodes. + * Permutations of leaf nodes consist the possible topological orders. Iterate over these set of ordered nodes to + * add nodes of lower layers to each set by considering the edges. That means for nodes of each layer, e.g. layer 1, + * find the outgoing edges to layer 2. If there is an edge for node n in layer 1 to node m in layer 2, then add this + * node to the set of order. After adding these nodes that are origins of edges, add the remaining nodes of layer 1 + * to the set of order. Proceed to layer 0 and add nodes of it to the order in the same manner. + * + * @param layeredOrder the topological order of the graph with layer information i.e. (layer number, layer nodes). + * @return a list of all permutations of topological order. + */ + def findPermutations(layeredOrder: graph.LayeredTopologicalOrder[NodeT]): List[Vector[NodeT]] = { + val lastLayerNodes: Vector[NodeT] = layeredOrder.last._2.toVector + val allLowerLayers: Iterable[(Int, Iterable[NodeT])] = layeredOrder.dropRight(1) - @scala.annotation.tailrec - def makePermutationsRec(next: Vector[NodeT], - rest: List[Vector[NodeT]], - acc: List[Vector[NodeT]]): List[Vector[NodeT]] = { - if (rest.isEmpty) { - makePermutations(next, acc) - } else { - makePermutationsRec(rest.head, rest.tail, makePermutations(next, acc)) - } - } + // Find all permutations of last layer nodes; i.e leaf nodes. + val permutationsOfLastLayerNodes: List[Vector[NodeT]] = lastLayerNodes.permutations.toList - listOfLists match { - case Nil => Nil - case one :: Nil => one.permutations.toList - case one :: two :: tail => makePermutationsRec(two, tail, one.permutations.toList) + // For each permutation of last layer nodes, add nodes of lower layers in correct order. + val allPermutations: List[Vector[NodeT]] = permutationsOfLastLayerNodes.map { + lastLayerPermutation: Vector[NodeT] => + // Iterate over the previous layers to add the nodes into the order w.r.t. edges. + val orderedLowerLayerNodes: Vector[NodeT] = allLowerLayers.iterator.foldRight(lastLayerPermutation) { + (layer, acc) => + val layerNodes: Vector[NodeT] = layer._2.toVector + // Get those nodes within a layer that are origins of outgoing edges to the nodes already in set of ordered nodes. + val origins: Set[NodeT] = acc.foldRight(Set.empty[NodeT]) { (node, originsAcc) => + val maybeOriginNode: Option[NodeT] = + layerNodes.find(layerNode => graph.edges.contains(DiHyperEdge(layerNode, node))) + // Is there any edge which has its origin in this layer and target in already visited layers? + maybeOriginNode match { + // Yes. Add the origin node to the topological order + case Some(originNode) => Set(originNode) ++ originsAcc + // No. do nothing. + case None => originsAcc + } + } + // Find all nodes of this layer which are not origin of an edge + val notOriginNodes = layerNodes.diff(origins.toVector) + // Prepend the non-origin nodes and origin nodes to those found in higher layers. + notOriginNodes ++ origins ++ acc + } + orderedLowerLayerNodes } + allPermutations } // Accumulates topological orders. @@ -67,14 +95,7 @@ object TopologicalSortUtil { // Is there any topological order? case Right(topOrder) => // Yes. Find all valid permutations. - val nodesOfLayers: List[Vector[NodeT]] = - topOrder.toLayered.iterator.foldRight(List.empty[Vector[NodeT]]) { (layer, acc) => - val layerNodes: Vector[NodeT] = layer._2.toVector - layerNodes +: acc - } - - findPermutations(nodesOfLayers).toSet - + findPermutations(topOrder.toLayered).toSet case Left(_) => // No, The graph has a cycle, so don't try to sort it. Set.empty[Vector[NodeT]] diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index 89da8092e3..883fd58048 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -2694,14 +2694,14 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec StatementPattern( subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#hasRichtext".toSmartIri, + iri = "http://www.knora.org/ontology/0001/anything#hasInteger".toSmartIri, propertyPathOperator = None ), - obj = QueryVariable(variableName = "richtext"), + obj = QueryVariable(variableName = "int"), namedGraph = None ), StatementPattern( - subj = QueryVariable(variableName = "richtext"), + subj = QueryVariable(variableName = "int"), pred = IriRef( iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, propertyPathOperator = None @@ -2716,17 +2716,30 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec propertyPathOperator = None )) ), + StatementPattern( + subj = QueryVariable(variableName = "int"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#valueHasInteger".toSmartIri, + propertyPathOperator = None + ), + obj = QueryVariable(variableName = "int__valueHasInteger"), + namedGraph = Some( + IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), StatementPattern( subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#hasInteger".toSmartIri, + iri = "http://www.knora.org/ontology/0001/anything#hasRichtext".toSmartIri, propertyPathOperator = None ), - obj = QueryVariable(variableName = "int"), + obj = QueryVariable(variableName = "richtext"), namedGraph = None ), StatementPattern( - subj = QueryVariable(variableName = "int"), + subj = QueryVariable(variableName = "richtext"), pred = IriRef( iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, propertyPathOperator = None @@ -2741,19 +2754,6 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec propertyPathOperator = None )) ), - StatementPattern( - subj = QueryVariable(variableName = "int"), - pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#valueHasInteger".toSmartIri, - propertyPathOperator = None - ), - obj = QueryVariable(variableName = "int__valueHasInteger"), - namedGraph = Some( - IriRef( - iri = "http://www.knora.org/explicit".toSmartIri, - propertyPathOperator = None - )) - ), LuceneQueryPattern( subj = QueryVariable(variableName = "richtext"), obj = QueryVariable(variableName = "richtext__valueHasString"), @@ -2788,14 +2788,14 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec StatementPattern( subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#hasText".toSmartIri, + iri = "http://www.knora.org/ontology/0001/anything#hasInteger".toSmartIri, propertyPathOperator = None ), - obj = QueryVariable(variableName = "text"), + obj = QueryVariable(variableName = "int"), namedGraph = None ), StatementPattern( - subj = QueryVariable(variableName = "text"), + subj = QueryVariable(variableName = "int"), pred = IriRef( iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, propertyPathOperator = None @@ -2810,17 +2810,30 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec propertyPathOperator = None )) ), + StatementPattern( + subj = QueryVariable(variableName = "int"), + pred = IriRef( + iri = "http://www.knora.org/ontology/knora-base#valueHasInteger".toSmartIri, + propertyPathOperator = None + ), + obj = QueryVariable(variableName = "int__valueHasInteger"), + namedGraph = Some( + IriRef( + iri = "http://www.knora.org/explicit".toSmartIri, + propertyPathOperator = None + )) + ), StatementPattern( subj = QueryVariable(variableName = "thing"), pred = IriRef( - iri = "http://www.knora.org/ontology/0001/anything#hasInteger".toSmartIri, + iri = "http://www.knora.org/ontology/0001/anything#hasText".toSmartIri, propertyPathOperator = None ), - obj = QueryVariable(variableName = "int"), + obj = QueryVariable(variableName = "text"), namedGraph = None ), StatementPattern( - subj = QueryVariable(variableName = "int"), + subj = QueryVariable(variableName = "text"), pred = IriRef( iri = "http://www.knora.org/ontology/knora-base#isDeleted".toSmartIri, propertyPathOperator = None @@ -2835,19 +2848,6 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec propertyPathOperator = None )) ), - StatementPattern( - subj = QueryVariable(variableName = "int"), - pred = IriRef( - iri = "http://www.knora.org/ontology/knora-base#valueHasInteger".toSmartIri, - propertyPathOperator = None - ), - obj = QueryVariable(variableName = "int__valueHasInteger"), - namedGraph = Some( - IriRef( - iri = "http://www.knora.org/explicit".toSmartIri, - propertyPathOperator = None - )) - ), LuceneQueryPattern( subj = QueryVariable(variableName = "text"), obj = QueryVariable(variableName = "text__valueHasString"), diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtilSpec.scala index 5157e15268..0c8be810af 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/TopologicalSortUtilSpec.scala @@ -38,7 +38,7 @@ class TopologicalSortUtilSpec extends CoreSpec() { "TopologicalSortUtilSpec" should { - "return all topological orders of a graph" in { + "return all topological orders of a graph with one leaf" in { val graph: Graph[Int, DiHyperEdge] = Graph[Int, DiHyperEdge](DiHyperEdge[Int](2, 4), DiHyperEdge[Int](2, 7), DiHyperEdge[Int](4, 5)) @@ -47,13 +47,28 @@ class TopologicalSortUtilSpec extends CoreSpec() { .findAllTopologicalOrderPermutations(graph)) val expectedOrders = Set( - Vector(2, 4, 7, 5), Vector(2, 7, 4, 5) ) assert(allOrders == expectedOrders) } + "return all topological orders of a graph with multiple leaves" in { + val graph: Graph[Int, DiHyperEdge] = + Graph[Int, DiHyperEdge](DiHyperEdge[Int](2, 4), DiHyperEdge[Int](2, 7), DiHyperEdge[Int](2, 8), DiHyperEdge[Int](4, 5), DiHyperEdge[Int](7, 3)) + + val allOrders: Set[Vector[Int]] = nodesToValues( + TopologicalSortUtil + .findAllTopologicalOrderPermutations(graph)) + + val expectedOrders = Set( + Vector(2, 8, 4, 7, 5, 3), + Vector(2, 8, 7, 4, 3, 5) + ) + + assert(allOrders == expectedOrders) + } + "return an empty set of orders for an empty graph" in { val graph: Graph[Int, DiHyperEdge] = Graph[Int, DiHyperEdge]()