Repository: alibaba/GraphScope (language: python)

Issue:
Enhanced Stored Procedures Development for GraphScope Interactive Graph Query Engine
GraphScope Interactive, a key component of the `GraphScope` project, focuses on querying and analyzing complex graph data, playing a crucial role in real-time data intelligence. It swiftly identifies anomalous nodes or anomaly patterns involving multiple entities based on relationship networks, supporting risk control and attack prevention in business systems. Traditionally, graph analysis relies on business users explicitly writing graph queries like Gremlin and Cypher, which increases the user's learning curve. Additionally, complex operations (like weighted shortest path calculations) are challenging to execute due to the declarative nature of graph queries.

To tackle these challenges, our project adopts the concept of stored procedures, commonly used in database systems, aiming to integrate this feature into GraphScope Interactive's graph querying system. We plan to encapsulate complex graph analysis logic through builtin stored procedures, so users can easily perform out-of-the-box graph analysis with just a few lines of code. This significantly simplifies the learning curve for users new to graph data analytics.

Project Goals and Outputs:
1. Develop at least three builtin stored procedures in C++ using GraphScope Interactive's Stored Procedure C++ API, including shortest paths, k-nearest neighbor traversal from a specified node, and PageRank value calculation.
2. Integrate these built-in stored procedures with the GraphScope Interactive engine and demonstrate their application in data intelligence case studies through the Interactive frontend tools.
3. Produce comprehensive technical documentation and a demo showcase report to enable users to efficiently utilize graph techniques in various application scenarios.


Discussion / hints:
项目名称：GraphScope Interactive 图查询引擎中的存储过程增强功能开发

描述：
GraphScope Interactive，作为GraphScope开源项目的重要组成部分，专注于解决复杂图数据的查询与分析问题，在实时数据智能领域承担关键任务，能够凭借实体间的关系网络实时锁定异常节点或挖掘由多个实体构成的异常模式，有力支撑了业务系统中风险控制与攻击防范的需求。传统图数据分析通常依赖业务人员显式编写如Gremlin和Cypher等图查询语句进行操作，但这无疑提高了用户的使用门槛；同时，受限于图查询的声明式特性，复杂操作逻辑（如带权最短路径计算）难以有效实施。

针对上述挑战，本项目借鉴数据库系统普遍采用的存储过程方法，旨在GraphScope Interactive图查询系统中引入并整合存储过程（Stored Procedure）支持。我们将通过预定义存储过程来封装复杂的图分析逻辑，使得用户仅需通过几行代码调用内置的存储过程，即可轻松实现开箱即用的图分析，大幅降低用户对图数据分析技术的入门难度。

项目目标及产出如下：
1. 利用GraphScope Interactive提供的Stored Procedure C++编程接口，采用C++语言开发至少包含三点间最短路径、指定节点出发的k度邻居遍历以及PageRank值计算在内的三个内置存储过程实现。
2. 完成上述内置存储过程与GraphScope Interactive引擎的集成，并借助Interactive的前端工具，实现在具体的数据智能案例分析中的应用演示。
3. 编撰详尽的技术文档及Demo展示报告，以便用户在更多应用场景中便捷高效地运用图技术。
项目难度：基础
技术要求：熟练使用C++编程语言
/cc @yecol @sighingnow, this issus/pr has had no activity for a long time, please help to review the status and assign people to work on it.

Fix patch:
diff --git a/flex/engines/graph_db/app/builtin/k_hop_neighbors.cc b/flex/engines/graph_db/app/builtin/k_hop_neighbors.cc
new file mode 100644
index 000000000000..6d34ed1d72f1
--- /dev/null
+++ b/flex/engines/graph_db/app/builtin/k_hop_neighbors.cc
@@ -0,0 +1,135 @@
+/** Copyright 2020 Alibaba Group Holding Limited.
+ *
+ * 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.
+ */
+#include "flex/engines/graph_db/app/builtin/k_hop_neighbors.h"
+
+namespace gs {
+
+bool KNeighbors::DoQuery(GraphDBSession& sess, Decoder& input,
+                         Encoder& output) {
+  auto txn = sess.GetReadTransaction();
+  Schema schema_ = txn.schema();
+  if (input.empty()) {
+    return false;
+  }
+  int64_t vertex_id_ = input.get_long();
+  std::string label_name{input.get_string()};
+  int k = input.get_int();
+
+  if (k <= 0) {
+    output.put_string_view("k must be greater than 0.");
+    return false;
+  }
+  if (!schema_.has_vertex_label(label_name)) {
+    output.put_string_view("The requested label doesn't exits.");
+    return false;  // The requested label doesn't exits.
+  }
+  label_t vertex_label_ = schema_.get_vertex_label_id(label_name);
+  struct pair_hash {
+    std::size_t operator()(const std::pair<label_t, vid_t>& p) const {
+      auto hash1 = std::hash<label_t>{}(p.first);
+      auto hash2 = std::hash<vid_t>{}(p.second);
+      return hash1 ^ (hash2 << 1);
+    }
+  };
+  std::unordered_set<std::pair<label_t, vid_t>, pair_hash> k_neighbors;
+
+  int vertex_size_ = (int) schema_.vertex_label_num();
+  int edge_size_ = (int) schema_.edge_label_num();
+
+  std::vector<vid_t> nei_index_;
+  std::vector<label_t> nei_label_;
+  std::vector<vid_t> next_nei_indexs_;
+  std::vector<label_t> next_nei_label_;
+
+  nei_label_.push_back(vertex_label_);
+  vid_t vertex_index{};
+  if (!txn.GetVertexIndex(vertex_label_, (int64_t) vertex_id_, vertex_index)) {
+    output.put_string_view("get index fail.");
+    return false;
+  }
+  nei_index_.push_back(vertex_index);
+  // get k hop neighbors
+  while (!nei_index_.empty() && k > 0) {
+    for (long unsigned int i = 0; i < nei_index_.size(); i++) {
+      for (int j = 0; j < vertex_size_; j++) {
+        for (int k = 0; k < edge_size_; k++) {
+          if (schema_.has_edge_label(label_t(nei_label_[i]), label_t(j),
+                                     label_t(k))) {
+            auto outedges = txn.GetOutEdgeIterator(
+                nei_label_[i], nei_index_[i], j,
+                k);  // 1.self_label 2.self_index 3.edge_label 4.nei_label
+            while (outedges.IsValid()) {
+              auto neighbor = outedges.GetNeighbor();
+              if (k_neighbors.find(std::make_pair(j, neighbor)) ==
+                  k_neighbors.end()) {
+                next_nei_label_.push_back(j);
+                next_nei_indexs_.push_back(neighbor);
+                k_neighbors.insert(std::make_pair(j, neighbor));
+              }
+              outedges.Next();
+            }
+          }
+          if (schema_.has_edge_label(label_t(j), label_t(nei_label_[i]),
+                                     label_t(k))) {
+            auto inedges = txn.GetInEdgeIterator(
+                nei_label_[i], nei_index_[i], j,
+                k);  // 1.self_label 2.self_index 3.edge_label 4.nei_label
+            while (inedges.IsValid()) {
+              auto neighbor = inedges.GetNeighbor();
+              if (k_neighbors.find(std::make_pair(j, neighbor)) ==
+                  k_neighbors.end()) {
+                next_nei_label_.push_back(j);
+                next_nei_indexs_.push_back(neighbor);
+                k_neighbors.insert(std::make_pair(j, neighbor));
+              }
+              inedges.Next();
+            }
+          }
+        }
+      }
+    }
+    nei_index_ = next_nei_indexs_;
+    nei_label_ = next_nei_label_;
+    next_nei_label_.clear();
+    next_nei_indexs_.clear();
+    k--;
+  }
+  results::CollectiveResults results;
+  for (auto vertex_ : k_neighbors) {
+    std::string label_name_ = schema_.get_vertex_label_name(vertex_.first);
+    auto result = results.add_results();
+    result->mutable_record()
+        ->add_columns()
+        ->mutable_entry()
+        ->mutable_element()
+        ->mutable_object()
+        ->set_str(label_name_);
+    result->mutable_record()
+        ->add_columns()
+        ->mutable_entry()
+        ->mutable_element()
+        ->mutable_object()
+        ->set_i64(txn.GetVertexId(vertex_.first, vertex_.second).AsInt64());
+  }
+  output.put_string_view(results.SerializeAsString());
+
+  txn.Commit();
+  return true;
+}
+
+AppWrapper KNeighborsFactory::CreateApp(const GraphDB& db) {
+  return AppWrapper(new KNeighbors(), NULL);
+}
+}  // namespace gs
diff --git a/flex/engines/graph_db/app/builtin/k_hop_neighbors.h b/flex/engines/graph_db/app/builtin/k_hop_neighbors.h
new file mode 100644
index 000000000000..81d58dc264ae
--- /dev/null
+++ b/flex/engines/graph_db/app/builtin/k_hop_neighbors.h
@@ -0,0 +1,38 @@
+/** Copyright 2020 Alibaba Group Holding Limited.
+ *
+ * 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.
+ */
+
+#ifndef ENGINES_GRAPH_DB_APP_BUILDIN_K_HOP_NEIGHBORS_
+#define ENGINES_GRAPH_DB_APP_BUILDIN_K_HOP_NEIGHBORS_
+#include "flex/engines/graph_db/database/graph_db_session.h"
+#include "flex/engines/hqps_db/app/interactive_app_base.h"
+
+namespace gs {
+class KNeighbors : public CypherInternalPbWriteAppBase {
+ public:
+  KNeighbors() {}
+  bool DoQuery(GraphDBSession& sess, Decoder& input, Encoder& output) override;
+};
+
+class KNeighborsFactory : public AppFactoryBase {
+ public:
+  KNeighborsFactory() = default;
+  ~KNeighborsFactory() = default;
+
+  AppWrapper CreateApp(const GraphDB& db) override;
+};
+
+}  // namespace gs
+
+#endif  // ENGINES_GRAPH_DB_APP_BUILDIN_K_HOP_NEIGHBORS_
\ No newline at end of file
diff --git a/flex/engines/graph_db/app/builtin/pagerank.cc b/flex/engines/graph_db/app/builtin/pagerank.cc
new file mode 100644
index 000000000000..5de165984896
--- /dev/null
+++ b/flex/engines/graph_db/app/builtin/pagerank.cc
@@ -0,0 +1,153 @@
+/** Copyright 2020 Alibaba Group Holding Limited.
+ *
+ * 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.
+ */
+#include "flex/engines/graph_db/app/builtin/pagerank.h"
+
+namespace gs {
+
+bool PageRank::DoQuery(GraphDBSession& sess, Decoder& input, Encoder& output) {
+  auto txn = sess.GetReadTransaction();
+  if (input.empty()) {
+    output.put_string_view(
+        "Arguments required(vertex_label, edge_label, damping_factor_, max_iterations_, epsilon_)\n \
+    for example:(\"person\", \"knows\", 0.85, 100, 0.000001)");
+    return false;
+  }
+
+  std::string vertex_label{input.get_string()};
+  std::string edge_label{input.get_string()};
+
+  damping_factor_ = input.get_double();
+  max_iterations_ = input.get_int();
+  epsilon_ = input.get_double();
+
+  if (!sess.schema().has_vertex_label(vertex_label)) {
+    output.put_string_view("The requested vertex label doesn't exits.");
+    return false;
+  }
+  if (!sess.schema().has_edge_label(vertex_label, vertex_label, edge_label)) {
+    output.put_string_view("The requested edge label doesn't exits.");
+    return false;
+  }
+  if (damping_factor_ < 0 || damping_factor_ >= 1) {
+    output.put_string_view(
+        "The value of the damping_factor_ is between 0 and 1.");
+    return false;
+  }
+  if (max_iterations_ <= 0) {
+    output.put_string_view("max_iterations_ must be greater than 0.");
+    return false;
+  }
+  if (epsilon_ < 0 || epsilon_ >= 1) {
+    output.put_string_view("The value of the epsilon_ is between 0 and 1.");
+    return false;
+  }
+
+  vertex_label_id_ = sess.schema().get_vertex_label_id(vertex_label);
+  edge_label_id_ = sess.schema().get_edge_label_id(edge_label);
+
+  auto num_vertices = txn.GetVertexNum(vertex_label_id_);
+
+  std::unordered_map<vid_t, double> pagerank;
+  std::unordered_map<vid_t, double> new_pagerank;
+
+  auto vertex_iter = txn.GetVertexIterator(vertex_label_id_);
+
+  while (vertex_iter.IsValid()) {
+    vid_t vid = vertex_iter.GetIndex();
+    pagerank[vid] = 1.0 / num_vertices;
+    new_pagerank[vid] = 0.0;
+    vertex_iter.Next();
+  }
+
+  std::unordered_map<vid_t, double> outdegree;
+
+  for (int iter = 0; iter < max_iterations_; ++iter) {
+    for (auto& kv : new_pagerank) {
+      kv.second = 0.0;
+    }
+
+    auto vertex_iter = txn.GetVertexIterator(vertex_label_id_);
+    while (vertex_iter.IsValid()) {
+      vid_t v = vertex_iter.GetIndex();
+
+      double sum = 0.0;
+      auto edges = txn.GetInEdgeIterator(vertex_label_id_, v, vertex_label_id_,
+                                         edge_label_id_);
+      while (edges.IsValid()) {
+        auto neighbor = edges.GetNeighbor();
+        if (outdegree[neighbor] == 0) {
+          auto out_edges = txn.GetOutEdgeIterator(
+              vertex_label_id_, neighbor, vertex_label_id_, edge_label_id_);
+          while (out_edges.IsValid()) {
+            outdegree[neighbor]++;
+            out_edges.Next();
+          }
+        }
+        sum += pagerank[neighbor] / outdegree[neighbor];
+        edges.Next();
+      }
+
+      new_pagerank[v] =
+          damping_factor_ * sum + (1.0 - damping_factor_) / num_vertices;
+      vertex_iter.Next();
+    }
+
+    double diff = 0.0;
+    for (const auto& kv : pagerank) {
+      diff += std::abs(new_pagerank[kv.first] - kv.second);
+    }
+
+    if (diff < epsilon_) {
+      break;
+    }
+
+    std::swap(pagerank, new_pagerank);
+  }
+
+  results::CollectiveResults results;
+
+  for (auto kv : pagerank) {
+    int64_t oid_ = txn.GetVertexId(vertex_label_id_, kv.first).AsInt64();
+    auto result = results.add_results();
+    result->mutable_record()
+        ->add_columns()
+        ->mutable_entry()
+        ->mutable_element()
+        ->mutable_object()
+        ->set_str(vertex_label);
+    result->mutable_record()
+        ->add_columns()
+        ->mutable_entry()
+        ->mutable_element()
+        ->mutable_object()
+        ->set_i64(oid_);
+    result->mutable_record()
+        ->add_columns()
+        ->mutable_entry()
+        ->mutable_element()
+        ->mutable_object()
+        ->set_f64(kv.second);
+  }
+
+  output.put_string_view(results.SerializeAsString());
+
+  txn.Commit();
+  return true;
+}
+
+AppWrapper PageRankFactory::CreateApp(const GraphDB& db) {
+  return AppWrapper(new PageRank(), NULL);
+}
+}  // namespace gs
diff --git a/flex/engines/graph_db/app/builtin/pagerank.h b/flex/engines/graph_db/app/builtin/pagerank.h
new file mode 100644
index 000000000000..abc6afc471db
--- /dev/null
+++ b/flex/engines/graph_db/app/builtin/pagerank.h
@@ -0,0 +1,51 @@
+/** Copyright 2020 Alibaba Group Holding Limited.
+ *
+ * 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.
+ */
+
+#ifndef ENGINES_GRAPH_DB_APP_BUILDIN_PAGERANK_H_
+#define ENGINES_GRAPH_DB_APP_BUILDIN_PAGERANK_H_
+#include "flex/engines/graph_db/database/graph_db_session.h"
+#include "flex/engines/hqps_db/app/interactive_app_base.h"
+
+namespace gs {
+class PageRank : public CypherInternalPbWriteAppBase {
+ public:
+  PageRank()
+      : damping_factor_(0.85),
+        max_iterations_(100),
+        epsilon_(1e-6),
+        vertex_label_id_(0),
+        edge_label_id_(0) {}
+  bool DoQuery(GraphDBSession& sess, Decoder& input, Encoder& output) override;
+
+ private:
+  double damping_factor_;
+  int max_iterations_;
+  double epsilon_;
+
+  label_t vertex_label_id_;
+  label_t edge_label_id_;
+};
+
+class PageRankFactory : public AppFactoryBase {
+ public:
+  PageRankFactory() = default;
+  ~PageRankFactory() = default;
+
+  AppWrapper CreateApp(const GraphDB& db) override;
+};
+
+}  // namespace gs
+
+#endif
\ No newline at end of file
diff --git a/flex/engines/graph_db/database/graph_db.cc b/flex/engines/graph_db/database/graph_db.cc
index 0aa50c6b0624..2f3ad08306c8 100644
--- a/flex/engines/graph_db/database/graph_db.cc
+++ b/flex/engines/graph_db/database/graph_db.cc
@@ -16,6 +16,9 @@
 #include "flex/engines/graph_db/database/graph_db.h"
 #include "flex/engines/graph_db/app/adhoc_app.h"
 #include "flex/engines/graph_db/app/builtin/count_vertices.h"
+#include "flex/engines/graph_db/app/builtin/k_hop_neighbors.h"
+#include "flex/engines/graph_db/app/builtin/pagerank.h"
+#include "flex/engines/graph_db/app/builtin/shortest_path_among_three.h"
 #include "flex/engines/graph_db/app/hqps_app.h"
 #include "flex/engines/graph_db/app/server_app.h"
 #include "flex/engines/graph_db/database/graph_db_session.h"
@@ -408,6 +411,13 @@ void GraphDB::initApps(
   app_factories_[0] = std::make_shared<ServerAppFactory>();
   app_factories_[Schema::BUILTIN_COUNT_VERTICES_PLUGIN_ID] =
       std::make_shared<CountVerticesFactory>();
+  app_factories_[Schema::BUILTIN_PAGERANK_PLUGIN_ID] =
+      std::make_shared<PageRankFactory>();
+  app_factories_[Schema::BUILTIN_K_DEGREE_NEIGHBORS_PLUGIN_ID] =
+      std::make_shared<KNeighborsFactory>();
+  app_factories_[Schema::BUILTIN_TVSP_PLUGIN_ID] =
+      std::make_shared<ShortestPathAmongThreeFactory>();
+
   app_factories_[Schema::HQPS_ADHOC_READ_PLUGIN_ID] =
       std::make_shared<HQPSAdhocReadAppFactory>();
   app_factories_[Schema::HQPS_ADHOC_WRITE_PLUGIN_ID] =
diff --git a/flex/interactive/sdk/python/requirements.txt b/flex/interactive/sdk/python/requirements.txt
index f76feea10ccb..2cff043b845f 100644
--- a/flex/interactive/sdk/python/requirements.txt
+++ b/flex/interactive/sdk/python/requirements.txt
@@ -1,7 +1,7 @@
 python_dateutil >= 2.5.3
 setuptools >= 21.0.0
 urllib3 >= 1.25.3, < 2.1.0
-pydantic >= 2
+pydantic >= 2, <= 2.8.2
 typing-extensions >= 4.7.1
 neo4j >= 4.4.19
 gremlinpython >= 3.4.10
diff --git a/flex/interactive/sdk/python/setup.py b/flex/interactive/sdk/python/setup.py
index 9861cd1c0ba2..e8b3e9ca9541 100644
--- a/flex/interactive/sdk/python/setup.py
+++ b/flex/interactive/sdk/python/setup.py
@@ -28,7 +28,7 @@
 REQUIRES = [
     "urllib3 >= 1.25.3, < 2.1.0",
     "python-dateutil",
-    "pydantic >= 2",
+    "pydantic >= 2, <= 2.8.2",
     "typing-extensions >= 4.7.1",
     "neo4j >= 4.4.19",
     "gremlinpython >= 3.4.10",
diff --git a/flex/storages/metadata/graph_meta_store.cc b/flex/storages/metadata/graph_meta_store.cc
index 8a9041c272b2..3d668f35cc91 100644
--- a/flex/storages/metadata/graph_meta_store.cc
+++ b/flex/storages/metadata/graph_meta_store.cc
@@ -56,6 +56,7 @@ const std::vector<PluginMeta>& get_builtin_plugin_metas() {
   static std::vector<PluginMeta> builtin_plugins;
   static bool initialized = false;
   if (!initialized) {
+    // count_vertices
     PluginMeta count_vertices;
     count_vertices.id = "count_vertices";
     count_vertices.name = "count_vertices";
@@ -67,8 +68,72 @@ const std::vector<PluginMeta>& get_builtin_plugin_metas() {
     count_vertices.update_time = GetCurrentTimeStamp();
     count_vertices.params.push_back({"labelName", PropertyType::kString});
     count_vertices.returns.push_back({"count", PropertyType::kInt32});
-    initialized = true;
     builtin_plugins.push_back(count_vertices);
+
+    // pagerank
+    PluginMeta pagerank;
+    pagerank.id = "pagerank";
+    pagerank.name = "pagerank";
+    pagerank.description = "A builtin plugin to calculate pagerank";
+    pagerank.enable = true;
+    pagerank.runnable = true;
+    pagerank.type = "cypher";
+    pagerank.creation_time = GetCurrentTimeStamp();
+    pagerank.update_time = GetCurrentTimeStamp();
+    pagerank.params.push_back({"vertex_label", PropertyType::kString});
+    pagerank.params.push_back({"edge_label", PropertyType::kString});
+    pagerank.params.push_back({"damping_factor_", PropertyType::kDouble});
+    pagerank.params.push_back({"max_iterations_", PropertyType::kInt32});
+    pagerank.params.push_back({"epsilon_", PropertyType::kDouble});
+    pagerank.returns.push_back({"label name", PropertyType::kString});
+    pagerank.returns.push_back({"vertex oid", PropertyType::kInt64});
+    pagerank.returns.push_back({"pagerank", PropertyType::kDouble});
+    builtin_plugins.push_back(pagerank);
+
+    // k_neighbors
+    PluginMeta k_neighbors;
+    k_neighbors.id = "k_neighbors";
+    k_neighbors.name = "k_neighbors";
+    k_neighbors.description = "A builtin plugin to calculate k_neighbors";
+    k_neighbors.enable = true;
+    k_neighbors.runnable = true;
+    k_neighbors.type = "cypher";
+    k_neighbors.creation_time = GetCurrentTimeStamp();
+    k_neighbors.update_time = GetCurrentTimeStamp();
+    k_neighbors.params.push_back({"vid", PropertyType::kInt64});
+    k_neighbors.params.push_back({"label_name", PropertyType::kString});
+    k_neighbors.params.push_back({"k", PropertyType::kInt32});
+    k_neighbors.returns.push_back({"label name", PropertyType::kString});
+    k_neighbors.returns.push_back({"vertex oid", PropertyType::kInt64});
+    builtin_plugins.push_back(k_neighbors);
+
+    // shortest_path_among_three
+    PluginMeta shortest_path_among_three;
+    shortest_path_among_three.id = "shortest_path_among_three";
+    shortest_path_among_three.name = "shortest_path_among_three";
+    shortest_path_among_three.description =
+        "A builtin plugin to calculate shortest_path_among_three";
+    shortest_path_among_three.enable = true;
+    shortest_path_among_three.runnable = true;
+    shortest_path_among_three.type = "cypher";
+    shortest_path_among_three.creation_time = GetCurrentTimeStamp();
+    shortest_path_among_three.update_time = GetCurrentTimeStamp();
+    shortest_path_among_three.params.push_back(
+        {"label_name1", PropertyType::kString});
+    shortest_path_among_three.params.push_back({"vid1", PropertyType::kInt64});
+    shortest_path_among_three.params.push_back(
+        {"label_name2", PropertyType::kString});
+    shortest_path_among_three.params.push_back({"vid2", PropertyType::kInt64});
+    shortest_path_among_three.params.push_back(
+        {"label_name3", PropertyType::kString});
+    shortest_path_among_three.params.push_back({"vid3", PropertyType::kInt64});
+    shortest_path_among_three.returns.push_back(
+        {"shortest_path_among_three (label name, vertex oid)",
+         PropertyType::kString});
+    initialized = true;
+    builtin_plugins.push_back(shortest_path_among_three);
+
+    initialized = true;
   }
   return builtin_plugins;
 }
diff --git a/flex/storages/rt_mutable_graph/schema.h b/flex/storages/rt_mutable_graph/schema.h
index 21ef5a68f62e..1f696aabd921 100644
--- a/flex/storages/rt_mutable_graph/schema.h
+++ b/flex/storages/rt_mutable_graph/schema.h
@@ -31,7 +31,7 @@ class Schema {
   // How many built-in plugins are there.
   // Currently only one builtin plugin, SERVER_APP is supported.
   static constexpr uint8_t RESERVED_PLUGIN_NUM = 1;
-  static constexpr uint8_t MAX_PLUGIN_ID = 251;
+  static constexpr uint8_t MAX_PLUGIN_ID = 248;
   static constexpr uint8_t ADHOC_READ_PLUGIN_ID = 253;
   static constexpr uint8_t HQPS_ADHOC_READ_PLUGIN_ID = 254;
   static constexpr uint8_t HQPS_ADHOC_WRITE_PLUGIN_ID = 255;
@@ -44,14 +44,25 @@ class Schema {
   static constexpr const uint16_t STRING_DEFAULT_MAX_LENGTH = 256;
 
   // The builtin plugins are reserved for the system.
-  static constexpr uint8_t BUILTIN_PLUGIN_NUM = 1;
+  static constexpr uint8_t BUILTIN_PLUGIN_NUM = 4;
+
   static constexpr uint8_t BUILTIN_COUNT_VERTICES_PLUGIN_ID = 252;
   static constexpr const char* BUILTIN_COUNT_VERTICES_PLUGIN_NAME =
       "count_vertices";
+  static constexpr uint8_t BUILTIN_PAGERANK_PLUGIN_ID = 251;
+  static constexpr const char* BUILTIN_PAGERANK_PLUGIN_NAME = "pagerank";
+  static constexpr uint8_t BUILTIN_K_DEGREE_NEIGHBORS_PLUGIN_ID = 250;
+  static constexpr const char* BUILTIN_K_DEGREE_NEIGHBORS_PLUGIN_NAME =
+      "k_neighbors";
+  static constexpr uint8_t BUILTIN_TVSP_PLUGIN_ID = 249;
+  static constexpr const char* BUILTIN_TVSP_PLUGIN_NAME =
+      "shortest_path_among_three";
   static constexpr const char* BUILTIN_PLUGIN_NAMES[BUILTIN_PLUGIN_NUM] = {
-      BUILTIN_COUNT_VERTICES_PLUGIN_NAME};
+      BUILTIN_COUNT_VERTICES_PLUGIN_NAME, BUILTIN_PAGERANK_PLUGIN_NAME,
+      BUILTIN_K_DEGREE_NEIGHBORS_PLUGIN_NAME, BUILTIN_TVSP_PLUGIN_NAME};
   static constexpr uint8_t BUILTIN_PLUGIN_IDS[BUILTIN_PLUGIN_NUM] = {
-      BUILTIN_COUNT_VERTICES_PLUGIN_ID};
+      BUILTIN_COUNT_VERTICES_PLUGIN_ID, BUILTIN_PAGERANK_PLUGIN_ID,
+      BUILTIN_K_DEGREE_NEIGHBORS_PLUGIN_ID, BUILTIN_TVSP_PLUGIN_ID};
 
   // An array containing all compatible versions of schema.
   static const std::vector<std::string> COMPATIBLE_VERSIONS;


Test patch:
diff --git a/flex/engines/graph_db/app/builtin/shortest_path_among_three.cc b/flex/engines/graph_db/app/builtin/shortest_path_among_three.cc
new file mode 100644
index 000000000000..15b6c4381df5
--- /dev/null
+++ b/flex/engines/graph_db/app/builtin/shortest_path_among_three.cc
@@ -0,0 +1,248 @@
+/** Copyright 2020 Alibaba Group Holding Limited.
+ *
+ * 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.
+ */
+#include "flex/engines/graph_db/app/builtin/shortest_path_among_three.h"
+
+namespace gs {
+
+bool ShortestPathAmongThree::DoQuery(GraphDBSession& sess, Decoder& input,
+                                     Encoder& output) {
+  ReadTransaction txn = sess.GetReadTransaction();
+  if (input.empty()) {
+    return false;
+  }
+  Schema schema_ = txn.schema();
+  std::string label_name1{input.get_string()};
+  int64_t vid1 = input.get_long();
+  std::string label_name2{input.get_string()};
+  int64_t vid2 = input.get_long();
+  std::string label_name3{input.get_string()};
+  int64_t vid3 = input.get_long();
+
+  if (!schema_.has_vertex_label(label_name1) ||
+      !schema_.has_vertex_label(label_name2) ||
+      !schema_.has_vertex_label(label_name3)) {
+    output.put_string_view("The requested label doesn't exits.");
+    return false;
+  }
+  label_t label_v1 = schema_.get_vertex_label_id(label_name1);
+  label_t label_v2 = schema_.get_vertex_label_id(label_name2);
+  label_t label_v3 = schema_.get_vertex_label_id(label_name3);
+  vid_t index_v1{};
+  vid_t index_v2{};
+  vid_t index_v3{};
+  if (!txn.GetVertexIndex(label_v1, (int64_t) vid1, index_v1) ||
+      !txn.GetVertexIndex(label_v2, (int64_t) vid2, index_v2) ||
+      !txn.GetVertexIndex(label_v3, (int64_t) vid3, index_v3)) {
+    output.put_string_view("get index fail.");
+    return false;
+  }
+  // get the three shortest paths
+  std::vector<std::pair<label_t, vid_t>> v1v2result_;
+  std::vector<std::pair<label_t, vid_t>> v2v3result_;
+  std::vector<std::pair<label_t, vid_t>> v1v3result_;
+
+  bool find_flag = true;
+  if (!ShortestPath(txn, label_v1, index_v1, label_v2, index_v2, v1v2result_)) {
+    find_flag = false;
+  }
+  if (find_flag &&
+      !ShortestPath(txn, label_v2, index_v2, label_v3, index_v3, v2v3result_)) {
+    find_flag = false;
+  }
+  if (find_flag &&
+      !ShortestPath(txn, label_v1, index_v1, label_v3, index_v3, v1v3result_)) {
+    find_flag = false;
+  }
+  std::string result_path = "";
+  if (find_flag) {
+    // connect the two shortest paths among three
+    std::vector<std::pair<label_t, vid_t>> TSP =
+        ConnectPath(v1v2result_, v2v3result_, v1v3result_);
+
+    // construct return result
+    for (auto it = TSP.begin(); it != TSP.end(); ++it) {
+      if (std::next(it) != TSP.end()) {
+        result_path +=
+            "(" + schema_.get_vertex_label_name(it->first) + "," +
+            std::to_string(txn.GetVertexId(it->first, it->second).AsInt64()) +
+            ")" + "--";
+      } else {
+        result_path +=
+            "(" + schema_.get_vertex_label_name(it->first) + "," +
+            std::to_string(txn.GetVertexId(it->first, it->second).AsInt64()) +
+            ")";
+      }
+    }
+  } else {
+    result_path = "no path find!";
+  }
+
+  // create result string
+  results::CollectiveResults results;
+  auto result = results.add_results();
+  result->mutable_record()
+      ->add_columns()
+      ->mutable_entry()
+      ->mutable_element()
+      ->mutable_object()
+      ->set_str(result_path);
+
+  output.put_string_view(results.SerializeAsString());
+  txn.Commit();
+  return true;
+}
+
+bool ShortestPathAmongThree::ShortestPath(
+    const gs::ReadTransaction& txn, label_t v1_l, vid_t v1_index, label_t v2_l,
+    vid_t v2_index, std::vector<std::pair<label_t, vid_t>>& result_) {
+  Schema schema_ = txn.schema();
+  label_t vertex_size_ = (int) schema_.vertex_label_num();
+  label_t edge_size_ = (int) schema_.edge_label_num();
+  struct pair_hash {
+    std::size_t operator()(const std::pair<label_t, vid_t>& p) const {
+      auto hash1 = std::hash<label_t>{}(p.first);
+      auto hash2 = std::hash<vid_t>{}(p.second);
+      return hash1 ^ (hash2 << 1);
+    }
+  };
+
+  std::unordered_map<std::pair<label_t, vid_t>, std::pair<label_t, vid_t>,
+                     pair_hash>
+      parent;
+  std::vector<label_t> nei_label_;
+  std::vector<vid_t> nei_index_;
+
+  parent[std::make_pair(v1_l, v1_index)] =
+      std::make_pair((label_t) UINT8_MAX, (vid_t) UINT32_MAX);
+  nei_label_.push_back(v1_l);
+  nei_index_.push_back(v1_index);
+  std::unordered_set<std::pair<label_t, vid_t>, pair_hash> visit;
+  visit.insert(std::make_pair(v1_l, v1_index));
+
+  std::vector<label_t> next_nei_labels_;
+  std::vector<vid_t> next_nei_indexs_;
+  bool find = false;
+  while (!nei_label_.empty() && !find) {
+    for (long unsigned int i = 0; i < nei_index_.size(); i++) {
+      for (label_t j = 0; j < vertex_size_; j++) {
+        for (label_t k = 0; k < edge_size_; k++) {
+          if (schema_.has_edge_label(label_t(nei_label_[i]), label_t(j),
+                                     label_t(k))) {
+            auto outedges = txn.GetOutEdgeIterator(
+                nei_label_[i], nei_index_[i], j,
+                k);  // 1.self_label 2.self_index 3.edge_label 4.nei_label
+            while (outedges.IsValid()) {
+              auto neighbor = outedges.GetNeighbor();
+              if (visit.find(std::make_pair(j, neighbor)) == visit.end()) {
+                next_nei_labels_.push_back(j);
+                next_nei_indexs_.push_back(neighbor);
+                visit.insert(std::make_pair(j, neighbor));
+                parent[std::make_pair(j, neighbor)] =
+                    std::make_pair(nei_label_[i], nei_index_[i]);
+                if (std::make_pair(j, neighbor) ==
+                    std::make_pair(v2_l, v2_index)) {
+                  find = true;
+                  break;
+                }
+              }
+              outedges.Next();
+            }
+          }
+          if (schema_.has_edge_label(label_t(j), label_t(nei_label_[i]),
+                                     label_t(k))) {
+            auto inedges = txn.GetInEdgeIterator(
+                nei_label_[i], nei_index_[i], j,
+                k);  // 1.self_label 2.self_index 3.edge_label 4.nei_label
+            while (inedges.IsValid()) {
+              auto neighbor = inedges.GetNeighbor();
+              if (visit.find(std::make_pair(j, neighbor)) == visit.end()) {
+                next_nei_labels_.push_back(j);
+                next_nei_indexs_.push_back(neighbor);
+                visit.insert(std::make_pair(j, neighbor));
+                parent[std::make_pair(j, neighbor)] =
+                    std::make_pair(nei_label_[i], nei_index_[i]);
+                if (std::make_pair(j, neighbor) ==
+                    std::make_pair(v2_l, v2_index)) {
+                  find = true;
+                  break;
+                }
+              }
+              inedges.Next();
+            }
+          }
+        }
+        if (find)
+          break;
+      }
+      if (find)
+        break;
+    }
+    if (find)
+      break;
+    nei_label_ = next_nei_labels_;
+    nei_index_ = next_nei_indexs_;
+    next_nei_labels_.clear();
+    next_nei_indexs_.clear();
+  }
+  if (find) {
+    std::pair<label_t, vid_t> vertex_v = std::make_pair(v2_l, v2_index);
+    while (vertex_v !=
+           std::make_pair((label_t) UINT8_MAX, (vid_t) UINT32_MAX)) {
+      result_.push_back(vertex_v);
+      vertex_v = parent[vertex_v];
+    }
+    std::reverse(result_.begin(), result_.end());
+    return true;
+  } else {
+    return false;
+  }
+}
+
+std::vector<std::pair<label_t, vid_t>> ShortestPathAmongThree::ConnectPath(
+    const std::vector<std::pair<label_t, vid_t>>& path1,
+    const std::vector<std::pair<label_t, vid_t>>& path2,
+    const std::vector<std::pair<label_t, vid_t>>& path3) {
+  std::vector<std::pair<label_t, vid_t>> TSP;
+  size_t v1v2size = path1.size();
+  size_t v2v3size = path2.size();
+  size_t v1v3size = path3.size();
+  if (v1v2size <= v2v3size && v1v3size <= v2v3size) {
+    for (size_t i = v1v2size; i > 0; i--) {
+      TSP.push_back(path1[i - 1]);
+    }
+    for (size_t i = 1; i < v1v3size; i++) {
+      TSP.push_back(path3[i]);
+    }
+  } else if (v1v2size <= v1v3size && v2v3size <= v1v3size) {
+    for (size_t i = 0; i < v1v2size; i++) {
+      TSP.push_back(path1[i]);
+    }
+    for (size_t i = 1; i < v2v3size; i++) {
+      TSP.push_back(path2[i]);
+    }
+  } else if (v2v3size <= v1v2size && v1v3size <= v1v2size) {
+    for (size_t i = 0; i < v2v3size; i++) {
+      TSP.push_back(path2[i]);
+    }
+    for (size_t i = v1v3size - 1; i > 0; i--) {
+      TSP.push_back(path3[i - 1]);
+    }
+  }
+  return TSP;
+}
+AppWrapper ShortestPathAmongThreeFactory::CreateApp(const GraphDB& db) {
+  return AppWrapper(new ShortestPathAmongThree(), NULL);
+}
+}  // namespace gs
diff --git a/flex/engines/graph_db/app/builtin/shortest_path_among_three.h b/flex/engines/graph_db/app/builtin/shortest_path_among_three.h
new file mode 100644
index 000000000000..71235c2e1b35
--- /dev/null
+++ b/flex/engines/graph_db/app/builtin/shortest_path_among_three.h
@@ -0,0 +1,47 @@
+/** Copyright 2020 Alibaba Group Holding Limited.
+ *
+ * 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.
+ */
+
+#ifndef ENGINES_GRAPH_DB_APP_BUILDIN_SHORTEST_PATH_AMONG_THREE_H_
+#define ENGINES_GRAPH_DB_APP_BUILDIN_SHORTEST_PATH_AMONG_THREE_H_
+#include "flex/engines/graph_db/database/graph_db_session.h"
+#include "flex/engines/hqps_db/app/interactive_app_base.h"
+
+namespace gs {
+class ShortestPathAmongThree : public CypherInternalPbWriteAppBase {
+ public:
+  ShortestPathAmongThree() {}
+  bool DoQuery(GraphDBSession& sess, Decoder& input, Encoder& output) override;
+
+ private:
+  bool ShortestPath(const gs::ReadTransaction& txn, label_t v1_l,
+                    vid_t v1_index, label_t v2_l, vid_t v2_index,
+                    std::vector<std::pair<label_t, vid_t>>& result_);
+  std::vector<std::pair<label_t, vid_t>> ConnectPath(
+      const std::vector<std::pair<label_t, vid_t>>& path1,
+      const std::vector<std::pair<label_t, vid_t>>& path2,
+      const std::vector<std::pair<label_t, vid_t>>& path3);
+};
+
+class ShortestPathAmongThreeFactory : public AppFactoryBase {
+ public:
+  ShortestPathAmongThreeFactory() = default;
+  ~ShortestPathAmongThreeFactory() = default;
+
+  AppWrapper CreateApp(const GraphDB& db) override;
+};
+
+}  // namespace gs
+
+#endif
\ No newline at end of file
diff --git a/flex/interactive/sdk/python/gs_interactive/tests/conftest.py b/flex/interactive/sdk/python/gs_interactive/tests/conftest.py
index bb7b15f3a40c..469bab8c3dd0 100644
--- a/flex/interactive/sdk/python/gs_interactive/tests/conftest.py
+++ b/flex/interactive/sdk/python/gs_interactive/tests/conftest.py
@@ -391,7 +391,7 @@ def run_cypher_test_suite(neo4j_sess: Neo4jSession, graph_id: str, queries: list
 
 
 def call_procedure(neo4j_sess: Neo4jSession, graph_id: str, proc_name: str, *args):
-    query = "CALL " + proc_name + "(" + ",".join(args) + ")"
+    query = "CALL " + proc_name + "(" + ",".join([str(item) for item in args]) + ")"
     result = neo4j_sess.run(query)
     for record in result:
         print(record)
diff --git a/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py b/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py
index 32e31023f06e..0dfd26fcbad3 100644
--- a/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py
+++ b/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py
@@ -176,6 +176,7 @@ def test_procedure_creation(interactive_session, neo4j_session, create_modern_gr
 
 def test_builtin_procedure(interactive_session, neo4j_session, create_modern_graph):
     print("[Test builtin procedure]")
+    import_data_to_full_modern_graph(interactive_session, create_modern_graph)
     # Delete the builtin procedure should fail
     with pytest.raises(Exception):
         delete_procedure(interactive_session, create_modern_graph, "count_vertices")
@@ -197,12 +198,41 @@ def test_builtin_procedure(interactive_session, neo4j_session, create_modern_gra
         )
     # Call the builtin procedure
     start_service_on_graph(interactive_session, create_modern_graph)
-    call_procedure(neo4j_session, create_modern_graph, "count_vertices", '"person"')
+    call_procedure(
+        neo4j_session,
+        create_modern_graph,
+        "count_vertices",
+        '"person"',
+    )
 
+    call_procedure(
+        neo4j_session,
+        create_modern_graph,
+        "pagerank",
+        '"person"',
+        '"knows"',
+        "0.85",
+        "100",
+        "0.000001",
+    )
 
-def test_stop_service(interactive_session, create_modern_graph):
-    print("[Test stop service]")
-    start_service_on_graph(interactive_session, create_modern_graph)
-    # stop the service
-    stop_res = interactive_session.stop_service(graph_id="A Invalid graph id")
-    assert not stop_res.is_ok()
+    call_procedure(
+        neo4j_session,
+        create_modern_graph,
+        "k_neighbors",
+        "1L",
+        '"person"',
+        "2",
+    )
+
+    call_procedure(
+        neo4j_session,
+        create_modern_graph,
+        "shortest_path_among_three",
+        '"person"',
+        "1L",
+        '"person"',
+        "2L",
+        '"person"',
+        "4L",
+    )