From a12d3c34d394172e0e0e8cd7401c8b640bf34d65 Mon Sep 17 00:00:00 2001
From: bright-starry-sky <56461666+bright-starry-sky@users.noreply.github.com>
Date: Thu, 26 Nov 2020 16:58:26 +0800
Subject: [PATCH] Fulltext integration (#415)

* fulltext

* remove create ft index

* fixed memory leaks

* Resolved conflict

* Resolve conflict

* addressed yee's comments
---
 src/context/test/CMakeLists.txt               |   1 +
 src/daemons/CMakeLists.txt                    |   1 +
 src/executor/CMakeLists.txt                   |   3 +
 src/executor/Executor.cpp                     |  12 +
 src/executor/admin/ShowTSClientsExecutor.cpp  |  43 ++++
 src/executor/admin/ShowTSClientsExecutor.h    |  29 +++
 .../admin/SignInTSServiceExecutor.cpp         |  33 +++
 src/executor/admin/SignInTSServiceExecutor.h  |  29 +++
 .../admin/SignOutTSServiceExecutor.cpp        |  32 +++
 src/executor/admin/SignOutTSServiceExecutor.h |  29 +++
 src/executor/query/IndexScanExecutor.cpp      |   4 +
 src/executor/test/CMakeLists.txt              |   1 +
 src/optimizer/rule/IndexScanRule.cpp          |  10 +-
 src/optimizer/rule/IndexScanRule.h            |   2 +
 src/optimizer/test/CMakeLists.txt             |   1 +
 src/parser/AdminSentences.cpp                 |  35 +++
 src/parser/AdminSentences.h                   |  54 +++++
 src/parser/Sentence.h                         |   4 +
 src/parser/parser.yy                          | 219 ++++++++++++++++-
 src/parser/scanner.lex                        |  12 +-
 src/parser/test/CMakeLists.txt                |   1 +
 src/parser/test/ParserTest.cpp                | 204 ++++++++++++++++
 src/planner/Admin.h                           |  46 ++++
 src/planner/PlanNode.cpp                      |   7 +
 src/planner/PlanNode.h                        |   4 +
 src/planner/Query.h                           |   9 +
 src/service/GraphFlags.cpp                    |   2 +
 src/service/PermissionCheck.cpp               |   9 +-
 src/util/ExpressionUtils.cpp                  |  14 ++
 src/util/ExpressionUtils.h                    |   3 +
 src/util/test/CMakeLists.txt                  |   1 +
 src/util/test/ExpressionUtilsTest.cpp         |  19 ++
 src/validator/AdminValidator.cpp              |  37 +++
 src/validator/AdminValidator.h                |  42 ++++
 src/validator/IndexScanValidator.cpp          | 224 +++++++++++++++---
 src/validator/IndexScanValidator.h            |  34 ++-
 src/validator/Validator.cpp                   |   6 +
 src/validator/test/CMakeLists.txt             |   1 +
 src/visitor/test/CMakeLists.txt               |   1 +
 39 files changed, 1173 insertions(+), 45 deletions(-)
 create mode 100644 src/executor/admin/ShowTSClientsExecutor.cpp
 create mode 100644 src/executor/admin/ShowTSClientsExecutor.h
 create mode 100644 src/executor/admin/SignInTSServiceExecutor.cpp
 create mode 100644 src/executor/admin/SignInTSServiceExecutor.h
 create mode 100644 src/executor/admin/SignOutTSServiceExecutor.cpp
 create mode 100644 src/executor/admin/SignOutTSServiceExecutor.h

diff --git a/src/context/test/CMakeLists.txt b/src/context/test/CMakeLists.txt
index 046c9a7e..d0d17267 100644
--- a/src/context/test/CMakeLists.txt
+++ b/src/context/test/CMakeLists.txt
@@ -28,6 +28,7 @@ SET(CONTEXT_TEST_LIBS
     $<TARGET_OBJECTS:common_process_obj>
     $<TARGET_OBJECTS:common_time_utils_obj>
     $<TARGET_OBJECTS:common_graph_obj>
+    $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
     $<TARGET_OBJECTS:util_obj>
     $<TARGET_OBJECTS:context_obj>
     $<TARGET_OBJECTS:expr_visitor_obj>
diff --git a/src/daemons/CMakeLists.txt b/src/daemons/CMakeLists.txt
index 7ecffcb9..e4593708 100644
--- a/src/daemons/CMakeLists.txt
+++ b/src/daemons/CMakeLists.txt
@@ -58,6 +58,7 @@ nebula_add_executable(
         $<TARGET_OBJECTS:common_conf_obj>
         $<TARGET_OBJECTS:common_time_utils_obj>
         $<TARGET_OBJECTS:common_graph_obj>
+        $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
     LIBRARIES
         proxygenhttpserver
         proxygenlib
diff --git a/src/executor/CMakeLists.txt b/src/executor/CMakeLists.txt
index a4f7f7f5..f98f916b 100644
--- a/src/executor/CMakeLists.txt
+++ b/src/executor/CMakeLists.txt
@@ -65,6 +65,9 @@ nebula_add_library(
     admin/ConfigExecutor.cpp
     admin/GroupExecutor.cpp
     admin/ZoneExecutor.cpp
+    admin/ShowTSClientsExecutor.cpp
+    admin/SignInTSServiceExecutor.cpp
+    admin/SignOutTSServiceExecutor.cpp
 )
 
 nebula_add_subdirectory(test)
diff --git a/src/executor/Executor.cpp b/src/executor/Executor.cpp
index 2afdec41..897d7d59 100644
--- a/src/executor/Executor.cpp
+++ b/src/executor/Executor.cpp
@@ -38,6 +38,9 @@
 #include "executor/admin/GroupExecutor.h"
 #include "executor/admin/ZoneExecutor.h"
 #include "executor/admin/ShowStatsExecutor.h"
+#include "executor/admin/ShowTSClientsExecutor.h"
+#include "executor/admin/SignInTSServiceExecutor.h"
+#include "executor/admin/SignOutTSServiceExecutor.h"
 #include "executor/algo/BFSShortestPathExecutor.h"
 #include "executor/algo/ProduceSemiShortestPathExecutor.h"
 #include "executor/algo/ConjunctPathExecutor.h"
@@ -449,6 +452,15 @@ Executor *Executor::makeExecutor(QueryContext *qctx, const PlanNode *node) {
         case PlanNode::Kind::kShowStats: {
             return pool->add(new ShowStatsExecutor(node, qctx));
         }
+        case PlanNode::Kind::kShowTSClients: {
+            return pool->add(new ShowTSClientsExecutor(node, qctx));
+        }
+        case PlanNode::Kind::kSignInTSService: {
+            return pool->add(new SignInTSServiceExecutor(node, qctx));
+        }
+        case PlanNode::Kind::kSignOutTSService: {
+            return pool->add(new SignOutTSServiceExecutor(node, qctx));
+        }
         case PlanNode::Kind::kUnknown: {
             LOG(FATAL) << "Unknown plan node kind " << static_cast<int32_t>(node->kind());
             break;
diff --git a/src/executor/admin/ShowTSClientsExecutor.cpp b/src/executor/admin/ShowTSClientsExecutor.cpp
new file mode 100644
index 00000000..a0e27398
--- /dev/null
+++ b/src/executor/admin/ShowTSClientsExecutor.cpp
@@ -0,0 +1,43 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "common/interface/gen-cpp2/meta_types.h"
+
+#include "context/QueryContext.h"
+#include "executor/admin/ShowTSClientsExecutor.h"
+#include "planner/Admin.h"
+#include "service/PermissionManager.h"
+
+namespace nebula {
+namespace graph {
+
+folly::Future<Status> ShowTSClientsExecutor::execute() {
+    SCOPED_TIMER(&execTime_);
+    return showTSClients();
+}
+
+folly::Future<Status> ShowTSClientsExecutor::showTSClients() {
+return qctx()
+        ->getMetaClient()
+        ->listFTClients()
+        .via(runner())
+        .then([this](auto &&resp) {
+            if (!resp.ok()) {
+                LOG(ERROR) << resp.status();
+                return resp.status();
+            }
+            auto value = std::move(resp).value();
+            DataSet v({"Host", "Port"});
+            for (const auto &client : value) {
+                nebula::Row r({client.host.host, client.host.port});
+                v.emplace_back(std::move(r));
+            }
+            return finish(std::move(v));
+        });
+}
+
+}   // namespace graph
+}   // namespace nebula
diff --git a/src/executor/admin/ShowTSClientsExecutor.h b/src/executor/admin/ShowTSClientsExecutor.h
new file mode 100644
index 00000000..f7e1fdbf
--- /dev/null
+++ b/src/executor/admin/ShowTSClientsExecutor.h
@@ -0,0 +1,29 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef EXECUTOR_ADMIN_SHOW_TS_CLIENTS_EXECUTOR_H_
+#define EXECUTOR_ADMIN_SHOW_TS_CLIENTS_EXECUTOR_H_
+
+#include "executor/Executor.h"
+
+namespace nebula {
+namespace graph {
+
+class ShowTSClientsExecutor final : public Executor {
+public:
+    ShowTSClientsExecutor(const PlanNode *node, QueryContext *qctx)
+        : Executor("ShowTSClientsExecutor", node, qctx) {}
+
+    folly::Future<Status> execute() override;
+
+private:
+    folly::Future<Status> showTSClients();
+};
+
+}  // namespace graph
+}  // namespace nebula
+
+#endif  // EXECUTOR_ADMIN_SHOW_TS_CLIENTS_EXECUTOR_H_
diff --git a/src/executor/admin/SignInTSServiceExecutor.cpp b/src/executor/admin/SignInTSServiceExecutor.cpp
new file mode 100644
index 00000000..07ad13db
--- /dev/null
+++ b/src/executor/admin/SignInTSServiceExecutor.cpp
@@ -0,0 +1,33 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "executor/admin/SignInTSServiceExecutor.h"
+#include "planner/Admin.h"
+
+namespace nebula {
+namespace graph {
+
+folly::Future<Status> SignInTSServiceExecutor::execute() {
+    SCOPED_TIMER(&execTime_);
+    return signInTSService();
+}
+
+folly::Future<Status> SignInTSServiceExecutor::signInTSService() {
+    auto *siNode = asNode<SignInTSService>(node());
+    return qctx()->getMetaClient()->signInFTService(siNode->type(), siNode->clients())
+        .via(runner())
+        .then([this](StatusOr<bool> resp) {
+            SCOPED_TIMER(&execTime_);
+            NG_RETURN_IF_ERROR(resp);
+            if (!resp.value()) {
+                return Status::Error("Sign in text service failed!");
+            }
+            return Status::OK();
+        });
+}
+
+}   // namespace graph
+}   // namespace nebula
diff --git a/src/executor/admin/SignInTSServiceExecutor.h b/src/executor/admin/SignInTSServiceExecutor.h
new file mode 100644
index 00000000..2cfca634
--- /dev/null
+++ b/src/executor/admin/SignInTSServiceExecutor.h
@@ -0,0 +1,29 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef EXECUTOR_ADMIN_SIGNINTSSERVICEEXECUTOR_H_
+#define EXECUTOR_ADMIN_SIGNINTSSERVICEEXECUTOR_H_
+
+#include "executor/Executor.h"
+
+namespace nebula {
+namespace graph {
+
+class SignInTSServiceExecutor final : public Executor {
+public:
+    SignInTSServiceExecutor(const PlanNode *node, QueryContext *qctx)
+        : Executor("SignInTSServiceExecutor", node, qctx) {}
+
+    folly::Future<Status> execute() override;
+
+private:
+    folly::Future<Status> signInTSService();
+};
+
+}   // namespace graph
+}   // namespace nebula
+
+#endif  // EXECUTOR_ADMIN_SIGNINTSSERVICEEXECUTOR_H_
diff --git a/src/executor/admin/SignOutTSServiceExecutor.cpp b/src/executor/admin/SignOutTSServiceExecutor.cpp
new file mode 100644
index 00000000..18cccaf2
--- /dev/null
+++ b/src/executor/admin/SignOutTSServiceExecutor.cpp
@@ -0,0 +1,32 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "executor/admin/SignOutTSServiceExecutor.h"
+#include "planner/Admin.h"
+
+namespace nebula {
+namespace graph {
+
+folly::Future<Status> SignOutTSServiceExecutor::execute() {
+    SCOPED_TIMER(&execTime_);
+    return signOutTSService();
+}
+
+folly::Future<Status> SignOutTSServiceExecutor::signOutTSService() {
+    return qctx()->getMetaClient()->signOutFTService()
+        .via(runner())
+        .then([this](StatusOr<bool> resp) {
+            SCOPED_TIMER(&execTime_);
+            NG_RETURN_IF_ERROR(resp);
+            if (!resp.value()) {
+                return Status::Error("Sign out text service failed!");
+            }
+            return Status::OK();
+        });
+}
+
+}   // namespace graph
+}   // namespace nebula
diff --git a/src/executor/admin/SignOutTSServiceExecutor.h b/src/executor/admin/SignOutTSServiceExecutor.h
new file mode 100644
index 00000000..cde08ea3
--- /dev/null
+++ b/src/executor/admin/SignOutTSServiceExecutor.h
@@ -0,0 +1,29 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef EXECUTOR_ADMIN_SIGNOUTTSSERVICEEXECUTOR_H_
+#define EXECUTOR_ADMIN_SIGNOUTTSSERVICEEXECUTOR_H_
+
+#include "executor/Executor.h"
+
+namespace nebula {
+namespace graph {
+
+class SignOutTSServiceExecutor final : public Executor {
+public:
+    SignOutTSServiceExecutor(const PlanNode *node, QueryContext *qctx)
+        : Executor("SignInTSServiceExecutor", node, qctx) {}
+
+    folly::Future<Status> execute() override;
+
+private:
+    folly::Future<Status> signOutTSService();
+};
+
+}   // namespace graph
+}   // namespace nebula
+
+#endif  // EXECUTOR_ADMIN_SIGNOUTTSSERVICEEXECUTOR_H_
diff --git a/src/executor/query/IndexScanExecutor.cpp b/src/executor/query/IndexScanExecutor.cpp
index 8156b5fe..69817576 100644
--- a/src/executor/query/IndexScanExecutor.cpp
+++ b/src/executor/query/IndexScanExecutor.cpp
@@ -23,6 +23,10 @@ folly::Future<Status> IndexScanExecutor::execute() {
 folly::Future<Status> IndexScanExecutor::indexScan() {
     GraphStorageClient* storageClient = qctx_->getStorageClient();
     auto *lookup = asNode<IndexScan>(node());
+    if (lookup->isEmptyResultSet()) {
+        DataSet dataSet({"dummy"});
+        return finish(ResultBuilder().value(Value(std::move(dataSet))).finish());
+    }
     return storageClient->lookupIndex(lookup->space(),
                                       *lookup->queryContext(),
                                       lookup->isEdge(),
diff --git a/src/executor/test/CMakeLists.txt b/src/executor/test/CMakeLists.txt
index 20920404..39fb0227 100644
--- a/src/executor/test/CMakeLists.txt
+++ b/src/executor/test/CMakeLists.txt
@@ -32,6 +32,7 @@ SET(EXEC_QUERY_TEST_OBJS
     $<TARGET_OBJECTS:common_encryption_obj>
     $<TARGET_OBJECTS:common_http_client_obj>
     $<TARGET_OBJECTS:common_time_utils_obj>
+    $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
     $<TARGET_OBJECTS:session_obj>
     $<TARGET_OBJECTS:graph_flags_obj>
     $<TARGET_OBJECTS:parser_obj>
diff --git a/src/optimizer/rule/IndexScanRule.cpp b/src/optimizer/rule/IndexScanRule.cpp
index 78fffec7..bce01b45 100644
--- a/src/optimizer/rule/IndexScanRule.cpp
+++ b/src/optimizer/rule/IndexScanRule.cpp
@@ -30,6 +30,10 @@ const Pattern& IndexScanRule::pattern() const {
 StatusOr<OptRule::TransformResult> IndexScanRule::transform(graph::QueryContext* qctx,
                                                             const MatchedResult& matched) const {
     auto groupNode = matched.node;
+    if (isEmptyResultSet(groupNode)) {
+        return TransformResult::noTransform();
+    }
+
     auto filter = filterExpr(groupNode);
     IndexQueryCtx iqctx = std::make_unique<std::vector<IndexQueryContext>>();
     if (filter == nullptr) {
@@ -174,7 +178,7 @@ Status IndexScanRule::appendIQCtx(const IndexItem& index,
 
 #define CHECK_BOUND_VALUE(v, name)                                                                 \
     do {                                                                                           \
-        if (v == Value::kNullBadType) {                                                      \
+        if (v == Value::kNullBadType) {                                                            \
             LOG(ERROR) << "Get bound value error. field : "  << name;                              \
             return Status::Error("Get bound value error. field : %s", name.c_str());               \
         }                                                                                          \
@@ -605,5 +609,9 @@ IndexScanRule::findIndexForRangeScan(const std::vector<IndexItem>& indexes,
     return priorityIdxs;
 }
 
+bool IndexScanRule::isEmptyResultSet(const OptGroupNode *groupNode) const {
+    auto in = static_cast<const IndexScan *>(groupNode->node());
+    return in->isEmptyResultSet();
+}
 }   // namespace opt
 }   // namespace nebula
diff --git a/src/optimizer/rule/IndexScanRule.h b/src/optimizer/rule/IndexScanRule.h
index 75814751..909c29cf 100644
--- a/src/optimizer/rule/IndexScanRule.h
+++ b/src/optimizer/rule/IndexScanRule.h
@@ -177,6 +177,8 @@ private:
 
     std::vector<IndexItem> findIndexForRangeScan(const std::vector<IndexItem>& indexes,
                                                  const FilterItems& items) const;
+
+    bool isEmptyResultSet(const OptGroupNode *groupNode) const;
 };
 
 }   // namespace opt
diff --git a/src/optimizer/test/CMakeLists.txt b/src/optimizer/test/CMakeLists.txt
index 9c135d06..64c6e743 100644
--- a/src/optimizer/test/CMakeLists.txt
+++ b/src/optimizer/test/CMakeLists.txt
@@ -30,6 +30,7 @@ set(OPTIMIZER_TEST_LIB
     $<TARGET_OBJECTS:common_agg_function_obj>
     $<TARGET_OBJECTS:common_time_utils_obj>
     $<TARGET_OBJECTS:common_graph_obj>
+    $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
     $<TARGET_OBJECTS:idgenerator_obj>
     $<TARGET_OBJECTS:expr_visitor_obj>
     $<TARGET_OBJECTS:session_obj>
diff --git a/src/parser/AdminSentences.cpp b/src/parser/AdminSentences.cpp
index 4cce5470..bd036555 100644
--- a/src/parser/AdminSentences.cpp
+++ b/src/parser/AdminSentences.cpp
@@ -208,4 +208,39 @@ std::string ShowStatsSentence::toString() const {
     return folly::stringPrintf("SHOW STATS");
 }
 
+std::string ShowTSClientsSentence::toString() const {
+    return "SHOW TEXT SEARCH CLIENTS";
+}
+
+std::string SignInTextServiceSentence::toString() const {
+    std::string buf;
+    buf.reserve(256);
+    buf += "SIGN IN TEXT SERVICE ";
+    for (auto &client : clients_->clients()) {
+        buf += "(";
+        buf += client.get_host().host;
+        buf += ":";
+        buf += std::to_string(client.get_host().port);
+        if (client.__isset.user && !client.get_user()->empty()) {
+            buf += ", \"";
+            buf += *client.get_user();
+            buf += "\"";
+        }
+        if (client.__isset.pwd && !client.get_pwd()->empty()) {
+            buf += ", \"";
+            buf += *client.get_pwd();
+            buf += "\"";
+        }
+        buf += ")";
+        buf += ",";
+    }
+    if (!buf.empty()) {
+        buf.resize(buf.size() - 1);
+    }
+    return buf;
+}
+
+std::string SignOutTextServiceSentence::toString() const {
+    return "SIGN OUT TEXT SERVICE";
+}
 }   // namespace nebula
diff --git a/src/parser/AdminSentences.h b/src/parser/AdminSentences.h
index 00ac019f..6bd545f6 100644
--- a/src/parser/AdminSentences.h
+++ b/src/parser/AdminSentences.h
@@ -585,6 +585,60 @@ public:
     std::string toString() const override;
 };
 
+class TSClientList final {
+public:
+    void addClient(nebula::meta::cpp2::FTClient *client) {
+        clients_.emplace_back(client);
+    }
+
+    std::string toString() const;
+
+    std::vector<nebula::meta::cpp2::FTClient> clients() const {
+        std::vector<nebula::meta::cpp2::FTClient> result;
+        result.reserve(clients_.size());
+        for (auto &client : clients_) {
+            result.emplace_back(*client);
+        }
+        return result;
+    }
+
+private:
+    std::vector<std::unique_ptr<nebula::meta::cpp2::FTClient>> clients_;
+};
+
+class ShowTSClientsSentence final : public Sentence {
+public:
+    ShowTSClientsSentence() {
+        kind_ = Kind::kShowTSClients;
+    }
+    std::string toString() const override;
+};
+
+class SignInTextServiceSentence final : public Sentence {
+public:
+    explicit SignInTextServiceSentence(TSClientList *clients) {
+        kind_ = Kind::kSignInTSService;
+        clients_.reset(clients);
+    }
+
+    std::string toString() const override;
+
+    TSClientList* clients() const {
+        return clients_.get();
+    }
+
+private:
+    std::unique_ptr<TSClientList>       clients_;
+};
+
+class SignOutTextServiceSentence final : public Sentence {
+public:
+    SignOutTextServiceSentence() {
+        kind_ = Kind::kSignOutTSService;
+    }
+
+    std::string toString() const override;
+};
 }   // namespace nebula
 
 #endif  // PARSER_ADMINSENTENCES_H_
diff --git a/src/parser/Sentence.h b/src/parser/Sentence.h
index a6241a8c..8a7392c7 100644
--- a/src/parser/Sentence.h
+++ b/src/parser/Sentence.h
@@ -19,6 +19,7 @@
 #include "common/expression/UUIDExpression.h"
 #include "common/expression/LabelExpression.h"
 #include "common/interface/gen-cpp2/meta_types.h"
+#include "common/expression/TextSearchExpression.h"
 
 namespace nebula {
 
@@ -77,6 +78,7 @@ public:
         kShowGroups,
         kShowZones,
         kShowStats,
+        kShowTSClients,
         kDeleteVertices,
         kDeleteEdges,
         kLookup,
@@ -122,6 +124,8 @@ public:
         kAddListener,
         kRemoveListener,
         kShowListener,
+        kSignInTSService,
+        kSignOutTSService,
     };
 
     Kind kind() const {
diff --git a/src/parser/parser.yy b/src/parser/parser.yy
index 20a9e140..cc57ce68 100644
--- a/src/parser/parser.yy
+++ b/src/parser/parser.yy
@@ -21,6 +21,7 @@
 #include "common/expression/LabelAttributeExpression.h"
 #include "common/expression/VariableExpression.h"
 #include "common/expression/CaseExpression.h"
+#include "common/expression/TextSearchExpression.h"
 #include "util/SchemaUtil.h"
 
 namespace nebula {
@@ -67,6 +68,7 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
     nebula::OverEdges                      *over_edges;
     nebula::OverClause                     *over_clause;
     nebula::WhereClause                    *where_clause;
+    nebula::WhereClause                    *lookup_where_clause;
     nebula::WhenClause                     *when_clause;
     nebula::YieldClause                    *yield_clause;
     nebula::YieldColumns                   *yield_columns;
@@ -121,6 +123,11 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
     nebula::meta::cpp2::IndexFieldDef      *index_field;
     nebula::IndexFieldList                 *index_field_list;
     CaseList                               *case_list;
+    nebula::TextSearchArgument             *text_search_argument;
+    nebula::TextSearchArgument             *base_text_search_argument;
+    nebula::TextSearchArgument             *fuzzy_text_search_argument;
+    nebula::meta::cpp2::FTClient           *text_search_client_item;
+    nebula::TSClientList                   *text_search_client_list;
 }
 
 /* destructors */
@@ -163,6 +170,8 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
 %token KW_CASE KW_THEN KW_ELSE KW_END
 %token KW_GROUP KW_ZONE KW_GROUPS KW_ZONES KW_INTO
 %token KW_LISTENER KW_ELASTICSEARCH
+%token KW_AUTO KW_FUZZY KW_PREFIX KW_REGEXP KW_WILDCARD
+%token KW_TEXT KW_SEARCH KW_CLIENTS KW_SIGN KW_SERVICE KW_TEXT_SEARCH
 
 /* symbols */
 %token L_PAREN R_PAREN L_BRACKET R_BRACKET L_BRACE R_BRACE COMMA
@@ -197,6 +206,7 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
 %type <expr> attribute_expression
 %type <expr> case_expression
 %type <expr> compound_expression
+%type <expr> text_search_expression
 %type <argument_list> argument_list opt_argument_list
 %type <type> type_spec
 %type <step_clause> step_clause
@@ -206,6 +216,7 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
 %type <over_edges> over_edges
 %type <over_clause> over_clause
 %type <where_clause> where_clause
+%type <lookup_where_clause> lookup_where_clause
 %type <when_clause> when_clause
 %type <yield_clause> yield_clause
 %type <yield_columns> yield_columns
@@ -265,6 +276,11 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
 %type <match_clause_list> reading_clauses reading_with_clause reading_with_clauses
 %type <match_step_range> match_step_range
 %type <order_factors> match_order_by
+%type <text_search_argument> text_search_argument
+%type <base_text_search_argument> base_text_search_argument
+%type <fuzzy_text_search_argument> fuzzy_text_search_argument
+%type <text_search_client_item> text_search_client_item
+%type <text_search_client_list> text_search_client_list
 
 %type <intval> legal_integer unary_integer rank port job_concurrency
 
@@ -321,6 +337,7 @@ static constexpr size_t MAX_ABS_INTEGER = 9223372036854775808ULL;
 %type <seq_sentences> seq_sentences
 %type <explain_sentence> explain_sentence
 %type <sentences> sentences
+%type <sentence> sign_in_text_search_service_sentence sign_out_text_search_service_sentence
 
 %type <boolval> opt_if_not_exists
 %type <boolval> opt_if_exists
@@ -450,6 +467,17 @@ unreserved_keyword
     | KW_ELASTICSEARCH      { $$ = new std::string("elasticsearch"); }
     | KW_STATS              { $$ = new std::string("stats"); }
     | KW_STATUS             { $$ = new std::string("status"); }
+    | KW_AUTO               { $$ = new std::string("auto"); }
+    | KW_FUZZY              { $$ = new std::string("fuzzy"); }
+    | KW_PREFIX             { $$ = new std::string("prefix"); }
+    | KW_REGEXP             { $$ = new std::string("regexp"); }
+    | KW_WILDCARD           { $$ = new std::string("wildcard"); }
+    | KW_TEXT               { $$ = new std::string("text"); }
+    | KW_SEARCH             { $$ = new std::string("search"); }
+    | KW_CLIENTS            { $$ = new std::string("clients"); }
+    | KW_SIGN               { $$ = new std::string("sign"); }
+    | KW_SERVICE            { $$ = new std::string("service"); }
+    | KW_TEXT_SEARCH        { $$ = new std::string("text_search"); }
     ;
 
 agg_function
@@ -1347,8 +1375,187 @@ match_limit
     }
     ;
 
+
+text_search_client_item
+    : L_PAREN host_item R_PAREN {
+        $$ = new nebula::meta::cpp2::FTClient();
+        $$->set_host(*$2);
+        delete $2;
+    }
+    | L_PAREN host_item COMMA STRING COMMA STRING R_PAREN {
+        $$ = new nebula::meta::cpp2::FTClient();
+        $$->set_host(*$2);
+        $$->set_user(*$4);
+        $$->set_pwd(*$6);
+        delete $2;
+        delete $4;
+        delete $6;
+    }
+    ;
+
+text_search_client_list
+    : text_search_client_item {
+        $$ = new TSClientList();
+        $$->addClient($1);
+    }
+    | text_search_client_list COMMA text_search_client_item {
+        $$ = $1;
+        $$->addClient($3);
+    }
+    | text_search_client_list COMMA {
+        $$ = $1;
+    }
+    ;
+
+sign_in_text_search_service_sentence
+    : KW_SIGN KW_IN KW_TEXT KW_SERVICE text_search_client_list {
+        $$ = new SignInTextServiceSentence($5);
+    }
+    ;
+
+sign_out_text_search_service_sentence
+    : KW_SIGN KW_OUT KW_TEXT KW_SERVICE {
+        $$ = new SignOutTextServiceSentence();
+    }
+    ;
+
+base_text_search_argument
+    : name_label DOT name_label COMMA STRING {
+        auto arg = new TextSearchArgument($1, $3, $5);
+        $$ = arg;
+    }
+    ;
+
+fuzzy_text_search_argument
+   : base_text_search_argument COMMA KW_AUTO COMMA KW_AND {
+        $$ = $1;
+        $$->setFuzziness(-1);
+        $$->setOP(new std::string("and"));
+   }
+   | base_text_search_argument COMMA KW_AUTO COMMA KW_OR {
+        $$ = $1;
+        $$->setFuzziness(-1);
+        $$->setOP(new std::string("or"));
+   }
+   | base_text_search_argument COMMA legal_integer COMMA KW_AND {
+        if ($3 != 0 && $3 != 1 && $3 != 2) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@3, "Out of range:");
+        }
+        $$ = $1;
+        $$->setFuzziness($3);
+        $$->setOP(new std::string("and"));
+   }
+   | base_text_search_argument COMMA legal_integer COMMA KW_OR {
+        if ($3 != 0 && $3 != 1 && $3 != 2) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@3, "Out of range:");
+        }
+        $$ = $1;
+        $$->setFuzziness($3);
+        $$->setOP(new std::string("or"));
+   }
+
+text_search_argument
+    : base_text_search_argument {
+        $$ = $1;
+    }
+    | fuzzy_text_search_argument {
+        $$ = $1;
+    }
+    | base_text_search_argument COMMA legal_integer {
+        if ($3 < 1) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@3, "Out of range:");
+        }
+        $$ = $1;
+        $$->setLimit($3);
+    }
+    | base_text_search_argument COMMA legal_integer COMMA legal_integer {
+        if ($3 < 1) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@3, "Out of range:");
+        }
+        if ($5 < 1) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@5, "Out of range:");
+        }
+        $$ = $1;
+        $$->setLimit($3);
+        $$->setTimeout($5);
+    }
+    | fuzzy_text_search_argument COMMA legal_integer {
+        if ($3 < 1) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@3, "Out of range:");
+        }
+        $$ = $1;
+        $$->setLimit($3);
+    }
+    | fuzzy_text_search_argument COMMA legal_integer COMMA legal_integer {
+        if ($3 < 1) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@3, "Out of range:");
+        }
+        if ($5 < 1) {
+            delete $1;
+            throw nebula::GraphParser::syntax_error(@5, "Out of range:");
+        }
+        $$ = $1;
+        $$->setLimit($3);
+        $$->setTimeout($5);
+    }
+    ;
+
+text_search_expression
+    : KW_PREFIX L_PAREN text_search_argument R_PAREN {
+        if ($3->op() != nullptr) {
+            delete $3;
+            throw nebula::GraphParser::syntax_error(@3, "argument error:");
+        }
+        if ($3->fuzziness() != -2) {
+            delete $3;
+            throw nebula::GraphParser::syntax_error(@3, "argument error:");
+        }
+        $$ = new TextSearchExpression(Expression::Kind::kTSPrefix, $3);
+    }
+    | KW_WILDCARD L_PAREN text_search_argument R_PAREN {
+        if ($3->op() != nullptr) {
+            delete $3;
+            throw nebula::GraphParser::syntax_error(@3, "argument error:");
+        }
+        if ($3->fuzziness() != -2) {
+            delete $3;
+            throw nebula::GraphParser::syntax_error(@3, "argument error:");
+        }
+        $$ = new TextSearchExpression(Expression::Kind::kTSWildcard, $3);
+    }
+    | KW_REGEXP L_PAREN text_search_argument R_PAREN {
+        if ($3->op() != nullptr) {
+            delete $3;
+            throw nebula::GraphParser::syntax_error(@3, "argument error:");
+        }
+        if ($3->fuzziness() != -2) {
+            delete $3;
+            throw nebula::GraphParser::syntax_error(@3, "argument error:");
+        }
+        $$ = new TextSearchExpression(Expression::Kind::kTSRegexp, $3);
+    }
+    | KW_FUZZY L_PAREN text_search_argument R_PAREN {
+        $$ = new TextSearchExpression(Expression::Kind::kTSFuzzy, $3);
+    }
+    ;
+
+    // TODO : unfiy the text_search_expression into expression in the future
+    // The current version only support independent text_search_expression for lookup_sentence
+lookup_where_clause
+    : %empty { $$ = nullptr; }
+    | KW_WHERE text_search_expression { $$ = new WhereClause($2); }
+    | KW_WHERE expression { $$ = new WhereClause($2); }
+    ;
+
 lookup_sentence
-    : KW_LOOKUP KW_ON name_label where_clause yield_clause {
+    : KW_LOOKUP KW_ON name_label lookup_where_clause yield_clause {
         auto sentence = new LookupSentence($3);
         sentence->setWhereClause($4);
         sentence->setYieldClause($5);
@@ -2383,6 +2590,9 @@ show_sentence
     | KW_SHOW KW_STATS {
         $$ = new ShowStatsSentence();
     }
+    | KW_SHOW KW_TEXT KW_SEARCH KW_CLIENTS {
+        $$ = new ShowTSClientsSentence();
+    }
     ;
 
 config_module_enum
@@ -2720,7 +2930,6 @@ maintain_sentence
     | add_host_into_zone_sentence { $$ = $1; }
     | drop_host_from_zone_sentence { $$ = $1; }
     | show_sentence { $$ = $1; }
-    ;
     | create_user_sentence { $$ = $1; }
     | alter_user_sentence { $$ = $1; }
     | drop_user_sentence { $$ = $1; }
@@ -2730,11 +2939,13 @@ maintain_sentence
     | get_config_sentence { $$ = $1; }
     | set_config_sentence { $$ = $1; }
     | balance_sentence { $$ = $1; }
-    | create_snapshot_sentence { $$ = $1; };
-    | drop_snapshot_sentence { $$ = $1; };
     | add_listener_sentence { $$ = $1; }
     | remove_listener_sentence { $$ = $1; }
     | list_listener_sentence { $$ = $1; }
+    | create_snapshot_sentence { $$ = $1; }
+    | drop_snapshot_sentence { $$ = $1; }
+    | sign_in_text_search_service_sentence { $$ = $1; }
+    | sign_out_text_search_service_sentence { $$ = $1; }
     ;
 
 return_sentence
diff --git a/src/parser/scanner.lex b/src/parser/scanner.lex
index c9c1d701..8d18a436 100644
--- a/src/parser/scanner.lex
+++ b/src/parser/scanner.lex
@@ -222,7 +222,17 @@ IP_OCTET                    ([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])
 "INTO"                      { return TokenType::KW_INTO; }
 "LISTENER"                  { return TokenType::KW_LISTENER; }
 "ELASTICSEARCH"             { return TokenType::KW_ELASTICSEARCH; }
-
+"AUTO"                      { return TokenType::KW_AUTO; }
+"FUZZY"                     { return TokenType::KW_FUZZY; }
+"PREFIX"                    { return TokenType::KW_PREFIX; }
+"REGEXP"                    { return TokenType::KW_REGEXP; }
+"WILDCARD"                  { return TokenType::KW_WILDCARD; }
+"TEXT"                      { return TokenType::KW_TEXT; }
+"SEARCH"                    { return TokenType::KW_SEARCH; }
+"CLIENTS"                   { return TokenType::KW_CLIENTS; }
+"SIGN"                      { return TokenType::KW_SIGN; }
+"SERVICE"                   { return TokenType::KW_SERVICE; }
+"TEXT_SEARCH"               { return TokenType::KW_TEXT_SEARCH; }
 "TRUE"                      { yylval->boolval = true; return TokenType::BOOL; }
 "FALSE"                     { yylval->boolval = false; return TokenType::BOOL; }
 
diff --git a/src/parser/test/CMakeLists.txt b/src/parser/test/CMakeLists.txt
index db33e1d2..5c3776cf 100644
--- a/src/parser/test/CMakeLists.txt
+++ b/src/parser/test/CMakeLists.txt
@@ -30,6 +30,7 @@ set(PARSER_TEST_LIBS
     $<TARGET_OBJECTS:common_file_based_cluster_id_man_obj>
     $<TARGET_OBJECTS:common_process_obj>
     $<TARGET_OBJECTS:common_time_utils_obj>
+    $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
     $<TARGET_OBJECTS:session_obj>
     $<TARGET_OBJECTS:graph_flags_obj>
     $<TARGET_OBJECTS:graph_auth_obj>
diff --git a/src/parser/test/ParserTest.cpp b/src/parser/test/ParserTest.cpp
index 7831bb8d..1cde51b3 100644
--- a/src/parser/test/ParserTest.cpp
+++ b/src/parser/test/ParserTest.cpp
@@ -2719,4 +2719,208 @@ TEST(Parser, Zone) {
     }
 }
 
+TEST(Parser, FullText) {
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE PREFIX(t1.c1, \"a\")";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE WILDCARD(t1.c1, \"a\")";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE REGEXP(t1.c1, \"a\")";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\")";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE PREFIX(t1.c1, \"a\", 1)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE WILDCARD(t1.c1, \"a\", 1)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE REGEXP(t1.c1, \"a\", 1)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 1)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE PREFIX(t1.c1, \"a\", 1, 2)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE WILDCARD(t1.c1, \"a\", 1, 2)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE REGEXP(t1.c1, \"a\", 1, 2)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 1, 2)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", AUTO, AND)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", AUTO, OR)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 0, AND)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 0, OR)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 0, OR, 1)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 0, OR, 1, 1)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 0, OR, -1, 1)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 0, OR, 1, -1)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", -1, OR, 1, 1)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE FUZZY(t1.c1, \"a\", 4, OR, 1, 1)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE PREFIX(t1.c1, \"a\", -1, 2)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE PREFIX(t1.c1, \"a\", 1, -2)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE PREFIX(t1.c1, \"a\", AUTO, AND)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE WILDCARD(t1.c1, \"a\", AUTO, AND)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+    {
+        GQLParser parser;
+        std::string query = "LOOKUP ON t1 WHERE REGEXP(t1.c1, \"a\", AUTO, AND)";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+}
+
+TEST(Parser, FullTextServiceTest) {
+    {
+        GQLParser parser;
+        std::string query = "SIGN IN TEXT SERVICE (127.0.0.1:9200)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "SIGN IN TEXT SERVICE (127.0.0.1:9200), (127.0.0.1:9300)";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "SIGN IN TEXT SERVICE (127.0.0.1:9200, \"user\", \"password\")";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "SIGN IN TEXT SERVICE (127.0.0.1:9200, \"user\", \"password\"), "
+                            "(127.0.0.1:9200, \"user\", \"password\")";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "SIGN OUT TEXT SERVICE";
+        auto result = parser.parse(query);
+        ASSERT_TRUE(result.ok()) << result.status();
+    }
+    {
+        GQLParser parser;
+        std::string query = "SIGN IN TEXT SERVICE (127.0.0.1:9200, \"user\")";
+        auto result = parser.parse(query);
+        ASSERT_FALSE(result.ok());
+    }
+}
 }   // namespace nebula
diff --git a/src/planner/Admin.h b/src/planner/Admin.h
index 3ed9dd32..6ec6396a 100644
--- a/src/planner/Admin.h
+++ b/src/planner/Admin.h
@@ -1213,6 +1213,52 @@ private:
         : SingleInputNode(qctx, Kind::kShowStats, input) {}
 };
 
+class ShowTSClients final : public SingleInputNode {
+public:
+    static ShowTSClients* make(QueryContext* qctx, PlanNode* input) {
+        return qctx->objPool()->add(new ShowTSClients(qctx, input));
+    }
+
+private:
+    ShowTSClients(QueryContext* qctx, PlanNode* input)
+        : SingleInputNode(qctx, Kind::kShowTSClients, input) {}
+};
+
+class SignInTSService final : public SingleInputNode {
+public:
+    static SignInTSService* make(QueryContext* qctx,
+                                  PlanNode* input,
+                                  std::vector<meta::cpp2::FTClient> clients) {
+        return qctx->objPool()->add(new SignInTSService(qctx, input, std::move(clients)));
+    }
+
+    const std::vector<meta::cpp2::FTClient> &clients() const {
+        return clients_;
+    }
+
+    meta::cpp2::FTServiceType type() const {
+        return meta::cpp2::FTServiceType::ELASTICSEARCH;
+    }
+
+private:
+    SignInTSService(QueryContext* qctx, PlanNode* input, std::vector<meta::cpp2::FTClient> clients)
+        : SingleInputNode(qctx, Kind::kSignInTSService, input),
+          clients_(std::move(clients)) {}
+
+    std::vector<meta::cpp2::FTClient> clients_;
+};
+
+class SignOutTSService final : public SingleInputNode {
+public:
+    static SignOutTSService* make(QueryContext* qctx,
+                                  PlanNode* input) {
+        return qctx->objPool()->add(new SignOutTSService(qctx, input));
+    }
+
+private:
+    SignOutTSService(QueryContext* qctx, PlanNode* input)
+        : SingleInputNode(qctx, Kind::kSignOutTSService, input) {}
+};
 }  // namespace graph
 }  // namespace nebula
 #endif  // PLANNER_ADMIN_H_
diff --git a/src/planner/PlanNode.cpp b/src/planner/PlanNode.cpp
index c550faa9..c8813e33 100644
--- a/src/planner/PlanNode.cpp
+++ b/src/planner/PlanNode.cpp
@@ -234,6 +234,13 @@ const char* PlanNode::toString(PlanNode::Kind kind) {
             return "ShowListener";
         case Kind::kShowStats:
             return "ShowStats";
+        // text search
+        case Kind::kShowTSClients:
+            return "ShowTSClients";
+        case Kind::kSignInTSService:
+            return "SignInTSService";
+        case Kind::kSignOutTSService:
+            return "SignOutTSService";
         // no default so the compiler will warning when lack
     }
     LOG(FATAL) << "Impossible kind plan node " << static_cast<int>(kind);
diff --git a/src/planner/PlanNode.h b/src/planner/PlanNode.h
index 9c21aa3b..3e76f6a2 100644
--- a/src/planner/PlanNode.h
+++ b/src/planner/PlanNode.h
@@ -130,6 +130,10 @@ public:
         kAddListener,
         kRemoveListener,
         kShowListener,
+        // text service related
+        kShowTSClients,
+        kSignInTSService,
+        kSignOutTSService,
     };
 
     PlanNode(QueryContext* qctx, Kind kind);
diff --git a/src/planner/Query.h b/src/planner/Query.h
index 86e7589d..6ac8b068 100644
--- a/src/planner/Query.h
+++ b/src/planner/Query.h
@@ -415,6 +415,7 @@ public:
                            IndexReturnCols&& returnCols,
                            bool isEdge,
                            int32_t schemaId,
+                           bool isEmptyResultSet = false,
                            bool dedup = false,
                            std::vector<storage::cpp2::OrderBy> orderBy = {},
                            int64_t limit = std::numeric_limits<int64_t>::max(),
@@ -426,6 +427,7 @@ public:
                                                   std::move(returnCols),
                                                   isEdge,
                                                   schemaId,
+                                                  isEmptyResultSet,
                                                   dedup,
                                                   std::move(orderBy),
                                                   limit,
@@ -452,6 +454,10 @@ public:
         return schemaId_;
     }
 
+    bool isEmptyResultSet() const {
+        return isEmptyResultSet_;
+    }
+
     void setIndexQueryContext(IndexQueryCtx contexts) {
         contexts_ = std::move(contexts);
     }
@@ -476,6 +482,7 @@ private:
               IndexReturnCols&& returnCols,
               bool isEdge,
               int32_t schemaId,
+              bool isEmptyResultSet,
               bool dedup,
               std::vector<storage::cpp2::OrderBy> orderBy,
               int64_t limit,
@@ -492,6 +499,7 @@ private:
         returnCols_ = std::move(returnCols);
         isEdge_ = isEdge;
         schemaId_ = schemaId;
+        isEmptyResultSet_ = isEmptyResultSet;
     }
 
 private:
@@ -499,6 +507,7 @@ private:
     IndexReturnCols                               returnCols_;
     bool                                          isEdge_;
     int32_t                                       schemaId_;
+    bool                                          isEmptyResultSet_;
 };
 
 /**
diff --git a/src/service/GraphFlags.cpp b/src/service/GraphFlags.cpp
index f72ee4a4..b65b8142 100644
--- a/src/service/GraphFlags.cpp
+++ b/src/service/GraphFlags.cpp
@@ -48,3 +48,5 @@ DEFINE_string(cloud_http_url, "", "cloud http url including ip, port, url path")
 DEFINE_uint32(max_allowed_statements, 512, "Max allowed sequential statements");
 
 DEFINE_bool(enable_optimizer, false, "Whether to enable optimizer");
+
+DEFINE_uint32(ft_request_retry_times, 3, "Retry times if fulltext request failed");
diff --git a/src/service/PermissionCheck.cpp b/src/service/PermissionCheck.cpp
index 376cae08..c998992c 100644
--- a/src/service/PermissionCheck.cpp
+++ b/src/service/PermissionCheck.cpp
@@ -73,7 +73,9 @@ Status PermissionCheck::permissionCheck(Session *session,
         case Sentence::Kind::kSetConfig:
         case Sentence::Kind::kGetConfig:
         case Sentence::Kind::kIngest:
-        case Sentence::Kind::kDownload: {
+        case Sentence::Kind::kDownload:
+        case Sentence::Kind::kSignOutTSService:
+        case Sentence::Kind::kSignInTSService: {
             return PermissionManager::canWriteSpace(session);
         }
         case Sentence::Kind::kCreateTag:
@@ -177,14 +179,15 @@ Status PermissionCheck::permissionCheck(Session *session,
             return PermissionManager::canReadSpace(session, targetSpace);
         }
         case Sentence::Kind::kShowUsers:
-        case Sentence::Kind::kShowSnapshots: {
+        case Sentence::Kind::kShowSnapshots:
+        case Sentence::Kind::kShowTSClients: {
             /**
              * Only GOD role can be show.
              */
             if (session->isGod()) {
                 return Status::OK();
             } else {
-                return Status::PermissionError("No permission to show users/snapshots");
+                return Status::PermissionError("No permission to show users/snapshots/textClients");
             }
         }
         case Sentence::Kind::kChangePassword: {
diff --git a/src/util/ExpressionUtils.cpp b/src/util/ExpressionUtils.cpp
index 8b6a20c7..6a99e207 100644
--- a/src/util/ExpressionUtils.cpp
+++ b/src/util/ExpressionUtils.cpp
@@ -75,5 +75,19 @@ std::unique_ptr<InputPropertyExpression> ExpressionUtils::inputPropExpr(const st
     return std::make_unique<InputPropertyExpression>(new std::string(prop));
 }
 
+std::unique_ptr<Expression>
+ExpressionUtils::pushOrs(const std::vector<std::unique_ptr<RelationalExpression>>& rels) {
+    DCHECK_GT(rels.size(), 1);
+    auto root = std::make_unique<LogicalExpression>(Expression::Kind::kLogicalOr);
+    root->addOperand(rels[0]->clone().release());
+    root->addOperand(rels[1]->clone().release());
+    for (size_t i = 2; i < rels.size(); i++) {
+        auto l = std::make_unique<LogicalExpression>(Expression::Kind::kLogicalOr);
+        l->addOperand(root->clone().release());
+        l->addOperand(rels[i]->clone().release());
+        root = std::move(l);
+    }
+    return root;
+}
 }   // namespace graph
 }   // namespace nebula
diff --git a/src/util/ExpressionUtils.h b/src/util/ExpressionUtils.h
index a4c20f8d..98015b49 100644
--- a/src/util/ExpressionUtils.h
+++ b/src/util/ExpressionUtils.h
@@ -122,6 +122,9 @@ public:
                                                       const std::string& var = "");
 
     static std::unique_ptr<InputPropertyExpression> inputPropExpr(const std::string& prop);
+
+    static std::unique_ptr<Expression> pushOrs(
+        const std::vector<std::unique_ptr<RelationalExpression>>& rels);
 };
 
 }   // namespace graph
diff --git a/src/util/test/CMakeLists.txt b/src/util/test/CMakeLists.txt
index 8cadd108..ac1b024c 100644
--- a/src/util/test/CMakeLists.txt
+++ b/src/util/test/CMakeLists.txt
@@ -30,6 +30,7 @@ nebula_add_test(
         $<TARGET_OBJECTS:common_agg_function_obj>
         $<TARGET_OBJECTS:common_time_utils_obj>
         $<TARGET_OBJECTS:common_graph_obj>
+        $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
         $<TARGET_OBJECTS:idgenerator_obj>
         $<TARGET_OBJECTS:expr_visitor_obj>
         $<TARGET_OBJECTS:session_obj>
diff --git a/src/util/test/ExpressionUtilsTest.cpp b/src/util/test/ExpressionUtilsTest.cpp
index 72949309..a9f3c09f 100644
--- a/src/util/test/ExpressionUtilsTest.cpp
+++ b/src/util/test/ExpressionUtilsTest.cpp
@@ -362,5 +362,24 @@ TEST_F(ExpressionUtilsTest, PullOrs) {
     }
 }
 
+TEST_F(ExpressionUtilsTest, pushOrs) {
+    std::vector<std::unique_ptr<RelationalExpression>> rels;
+    for (int16_t i = 0; i < 5; i++) {
+        auto r = std::make_unique<RelationalExpression>(
+            Expression::Kind::kRelEQ,
+            new LabelAttributeExpression(new LabelExpression(folly::stringPrintf("tag%d", i)),
+                                         new LabelExpression(folly::stringPrintf("col%d", i))),
+            new ConstantExpression(Value(folly::stringPrintf("val%d", i))));
+        rels.emplace_back(std::move(r));
+    }
+    auto t = ExpressionUtils::pushOrs(rels);
+    auto expected = std::string("(((((tag0.col0==val0) OR "
+                                "(tag1.col1==val1)) OR "
+                                "(tag2.col2==val2)) OR "
+                                "(tag3.col3==val3)) OR "
+                                "(tag4.col4==val4))");
+    ASSERT_EQ(expected, t->toString());
+}
+
 }   // namespace graph
 }   // namespace nebula
diff --git a/src/validator/AdminValidator.cpp b/src/validator/AdminValidator.cpp
index 3dc2f2a9..25ca7e5e 100644
--- a/src/validator/AdminValidator.cpp
+++ b/src/validator/AdminValidator.cpp
@@ -426,5 +426,42 @@ Status ShowStatusValidator::toPlan() {
     return Status::OK();
 }
 
+Status ShowTSClientsValidator::validateImpl() {
+    return Status::OK();
+}
+
+Status ShowTSClientsValidator::toPlan() {
+    auto *doNode = ShowTSClients::make(qctx_, nullptr);
+    root_ = doNode;
+    tail_ = root_;
+    return Status::OK();
+}
+
+Status SignInTSServiceValidator::validateImpl() {
+    return Status::OK();
+}
+
+Status SignInTSServiceValidator::toPlan() {
+    auto sentence = static_cast<SignInTextServiceSentence*>(sentence_);
+    std::vector<meta::cpp2::FTClient> clients;
+    if (sentence->clients() != nullptr) {
+        clients = sentence->clients()->clients();
+    }
+    auto *node = SignInTSService::make(qctx_, nullptr, std::move(clients));
+    root_ = node;
+    tail_ = root_;
+    return Status::OK();
+}
+
+Status SignOutTSServiceValidator::validateImpl() {
+    return Status::OK();
+}
+
+Status SignOutTSServiceValidator::toPlan() {
+    auto *node = SignOutTSService::make(qctx_, nullptr);
+    root_ = node;
+    tail_ = root_;
+    return Status::OK();
+}
 }  // namespace graph
 }  // namespace nebula
diff --git a/src/validator/AdminValidator.h b/src/validator/AdminValidator.h
index dc6bc891..85337f3b 100644
--- a/src/validator/AdminValidator.h
+++ b/src/validator/AdminValidator.h
@@ -12,6 +12,7 @@
 #include "parser/MaintainSentences.h"
 #include "parser/AdminSentences.h"
 #include "common/clients/meta/MetaClient.h"
+#include "common/plugin/fulltext/elasticsearch/ESGraphAdapter.h"
 
 namespace nebula {
 namespace graph {
@@ -27,6 +28,9 @@ private:
 
     Status toPlan() override;
 
+    bool checkTSIndex(const std::vector<meta::cpp2::FTClient>& clients,
+                      const std::string& index);
+
 private:
     meta::cpp2::SpaceDesc              spaceDesc_;
     bool                               ifNotExist_;
@@ -271,6 +275,44 @@ private:
     Status toPlan() override;
 };
 
+class ShowTSClientsValidator final : public Validator {
+public:
+    ShowTSClientsValidator(Sentence* sentence, QueryContext* context)
+        :Validator(sentence, context) {
+            setNoSpaceRequired();
+        }
+
+private:
+    Status validateImpl() override;
+
+    Status toPlan() override;
+};
+
+class SignInTSServiceValidator final : public Validator {
+public:
+    SignInTSServiceValidator(Sentence* sentence, QueryContext* context)
+        :Validator(sentence, context) {
+            setNoSpaceRequired();
+        }
+
+private:
+    Status validateImpl() override;
+
+    Status toPlan() override;
+};
+
+class SignOutTSServiceValidator final : public Validator {
+public:
+    SignOutTSServiceValidator(Sentence* sentence, QueryContext* context)
+        :Validator(sentence, context) {
+            setNoSpaceRequired();
+        }
+
+private:
+    Status validateImpl() override;
+
+    Status toPlan() override;
+};
 }  // namespace graph
 }  // namespace nebula
 #endif  // VALIDATOR_ADMINVALIDATOR_H_
diff --git a/src/validator/IndexScanValidator.cpp b/src/validator/IndexScanValidator.cpp
index c814fb83..4b8b7e42 100644
--- a/src/validator/IndexScanValidator.cpp
+++ b/src/validator/IndexScanValidator.cpp
@@ -9,6 +9,8 @@
 #include "util/ExpressionUtils.h"
 #include "util/SchemaUtil.h"
 
+DECLARE_uint32(ft_request_retry_times);
+
 namespace nebula {
 namespace graph {
 
@@ -24,14 +26,15 @@ Status IndexScanValidator::toPlan() {
                                         std::move(contexts_),
                                         std::move(returnCols_),
                                         isEdge_,
-                                        schemaId_);
+                                        schemaId_,
+                                        isEmptyResultSet_);
 }
 
 Status IndexScanValidator::prepareFrom() {
     auto *sentence = static_cast<const LookupSentence *>(sentence_);
     spaceId_ = vctx_->whichSpace().id;
-    const auto* from = sentence->from();
-    auto ret = qctx_->schemaMng()->getSchemaIDByName(spaceId_, *from);
+    from_ = *sentence->from();
+    auto ret = qctx_->schemaMng()->getSchemaIDByName(spaceId_, from_);
     if (!ret.ok()) {
         return ret.status();
     }
@@ -56,11 +59,10 @@ Status IndexScanValidator::prepareYield() {
     auto schema = isEdge_
                   ? qctx_->schemaMng()->getEdgeSchema(spaceId_, schemaId_)
                   : qctx_->schemaMng()->getTagSchema(spaceId_, schemaId_);
-    const auto* from = sentence->from();
     if (schema == nullptr) {
         return isEdge_
-               ? Status::EdgeNotFound("Edge schema not found : %s", from->c_str())
-               : Status::TagNotFound("Tag schema not found : %s", from->c_str());
+               ? Status::EdgeNotFound("Edge schema not found : %s", from_.c_str())
+               : Status::TagNotFound("Tag schema not found : %s", from_.c_str());
     }
     for (auto col : columns) {
         std::string schemaName, colName;
@@ -73,13 +75,13 @@ Status IndexScanValidator::prepareYield() {
                                          col->expr()->toString().c_str());
         }
 
-        if (schemaName != *from) {
+        if (schemaName != from_) {
             return Status::SemanticError("Schema name error : %s", schemaName.c_str());
         }
         auto ret = schema->getFieldType(colName);
         if (ret == meta::cpp2::PropertyType::UNKNOWN) {
             return Status::SemanticError("Column %s not found in schema %s",
-                                         colName.c_str(), from->c_str());
+                                         colName.c_str(), from_.c_str());
         }
         returnCols_->emplace_back(colName);
     }
@@ -93,16 +95,147 @@ Status IndexScanValidator::prepareFilter() {
     }
 
     auto *filter = sentence->whereClause()->filter();
-    auto ret = checkFilter(filter, *sentence->from());
-    NG_RETURN_IF_ERROR(ret);
     storage::cpp2::IndexQueryContext ctx;
-    ctx.set_filter(Expression::encode(*filter));
+    if (needTextSearch(filter)) {
+        NG_RETURN_IF_ERROR(checkTSService());
+        if (!textSearchReady_) {
+            return Status::Error("Text search service not ready");
+        }
+        auto retFilter = rewriteTSFilter(filter);
+        if (!retFilter.ok()) {
+            return retFilter.status();
+        }
+        if (isEmptyResultSet_) {
+            // return empty result direct.
+            return Status::OK();
+        }
+        ctx.set_filter(std::move(retFilter).value());
+    } else {
+        auto ret = checkFilter(filter);
+        NG_RETURN_IF_ERROR(ret);
+        ctx.set_filter(Expression::encode(*filter));
+    }
     contexts_ = std::make_unique<std::vector<storage::cpp2::IndexQueryContext>>();
     contexts_->emplace_back(std::move(ctx));
     return Status::OK();
 }
 
-Status IndexScanValidator::checkFilter(Expression* expr, const std::string& from) {
+StatusOr<std::string>
+IndexScanValidator::rewriteTSFilter(Expression* expr) {
+    std::vector<std::string> values;
+    auto tsExpr = static_cast<TextSearchExpression*>(expr);
+    auto vRet = textSearch(tsExpr);
+    if (!vRet.ok()) {
+        return Status::Error("Text search error.");
+    }
+    if (vRet.value().empty()) {
+        isEmptyResultSet_ = true;
+        return Status::OK();
+    }
+    std::vector<std::unique_ptr<RelationalExpression>> rels;
+    for (const auto& row : vRet.value()) {
+        std::unique_ptr<RelationalExpression> r;
+        if (isEdge_) {
+            r = std::make_unique<RelationalExpression>(
+                Expression::Kind::kRelEQ,
+                new EdgePropertyExpression(new std::string(*tsExpr->arg()->from()),
+                                           new std::string(*tsExpr->arg()->prop())),
+                new ConstantExpression(Value(row)));
+        } else {
+            r = std::make_unique<RelationalExpression>(
+                Expression::Kind::kRelEQ,
+                new TagPropertyExpression(new std::string(*tsExpr->arg()->from()),
+                                          new std::string(*tsExpr->arg()->prop())),
+                new ConstantExpression(Value(row)));
+        }
+        rels.emplace_back(std::move(r));
+    }
+    if (rels.size() == 1) {
+        return rels[0]->encode();
+    }
+    auto newExpr = ExpressionUtils::pushOrs(rels);
+    return newExpr->encode();
+}
+
+StatusOr<std::vector<std::string>> IndexScanValidator::textSearch(TextSearchExpression* expr) {
+    if (*expr->arg()->from() != from_) {
+        return Status::SemanticError("Schema name error : %s", expr->arg()->from()->c_str());
+    }
+    auto index = nebula::plugin::IndexTraits::indexName(space_.spaceDesc.space_name, isEdge_);
+    nebula::plugin::DocItem doc(index, *expr->arg()->prop(), schemaId_, *expr->arg()->val());
+    nebula::plugin::LimitItem limit(expr->arg()->timeout(), expr->arg()->limit());
+    std::vector<std::string> result;
+    // TODO (sky) : External index load balancing
+    auto retryCnt = FLAGS_ft_request_retry_times;
+    while (--retryCnt > 0) {
+        StatusOr<bool> ret = Status::Error();
+        switch (expr->kind()) {
+            case Expression::Kind::kTSFuzzy: {
+                folly::dynamic fuzz = folly::dynamic::object();
+                if (expr->arg()->fuzziness() < 0) {
+                    fuzz = "AUTO";
+                } else {
+                    fuzz = expr->arg()->fuzziness();
+                }
+                std::string op = (expr->arg()->op() == nullptr) ? "or" : *expr->arg()->op();
+                ret = nebula::plugin::ESGraphAdapter::kAdapter->fuzzy(randomFTClient(),
+                                                                      doc,
+                                                                      limit,
+                                                                      fuzz,
+                                                                      op,
+                                                                      result);
+                break;
+            }
+            case Expression::Kind::kTSPrefix: {
+                ret = nebula::plugin::ESGraphAdapter::kAdapter->prefix(randomFTClient(),
+                                                                       doc,
+                                                                       limit,
+                                                                       result);
+                break;
+            }
+            case Expression::Kind::kTSRegexp: {
+                ret = nebula::plugin::ESGraphAdapter::kAdapter->regexp(randomFTClient(),
+                                                                       doc,
+                                                                       limit,
+                                                                       result);
+                break;
+            }
+            case Expression::Kind::kTSWildcard: {
+                ret = nebula::plugin::ESGraphAdapter::kAdapter->wildcard(randomFTClient(),
+                                                                         doc,
+                                                                         limit,
+                                                                         result);
+                break;
+            }
+            default:
+                return Status::Error("text search expression error");
+        }
+        if (!ret.ok()) {
+            continue;
+        } else if (ret.value()) {
+            return result;
+        } else {
+            return Status::Error("External index error. "
+                                 "please check the status of fulltext cluster");
+        }
+    }
+    return Status::Error("scan external index failed");
+}
+
+bool IndexScanValidator::needTextSearch(Expression* expr) {
+    switch (expr->kind()) {
+        case Expression::Kind::kTSFuzzy:
+        case Expression::Kind::kTSPrefix:
+        case Expression::Kind::kTSRegexp:
+        case Expression::Kind::kTSWildcard: {
+            return true;
+        }
+        default:
+            return false;
+    }
+}
+
+Status IndexScanValidator::checkFilter(Expression* expr) {
     // TODO (sky) : Rewrite simple expressions,
     //              for example rewrite expr from col1 > 1 + 2 to col > 3
     switch (expr->kind()) {
@@ -110,9 +243,9 @@ Status IndexScanValidator::checkFilter(Expression* expr, const std::string& from
         case Expression::Kind::kLogicalAnd : {
             // TODO(dutor) Deal with n-ary operands
             auto lExpr = static_cast<LogicalExpression*>(expr);
-            auto ret = checkFilter(lExpr->operand(0), from);
+            auto ret = checkFilter(lExpr->operand(0));
             NG_RETURN_IF_ERROR(ret);
-            ret = checkFilter(lExpr->operand(1), from);
+            ret = checkFilter(lExpr->operand(1));
             NG_RETURN_IF_ERROR(ret);
             break;
         }
@@ -123,7 +256,7 @@ Status IndexScanValidator::checkFilter(Expression* expr, const std::string& from
         case Expression::Kind::kRelGT:
         case Expression::Kind::kRelNE: {
             auto* rExpr = static_cast<RelationalExpression*>(expr);
-            return checkRelExpr(rExpr, from);
+            return checkRelExpr(rExpr);
         }
         default: {
             return Status::NotSupported("Expression %s not supported yet",
@@ -133,8 +266,7 @@ Status IndexScanValidator::checkFilter(Expression* expr, const std::string& from
     return Status::OK();
 }
 
-Status IndexScanValidator::checkRelExpr(RelationalExpression* expr,
-                                        const std::string& from) {
+Status IndexScanValidator::checkRelExpr(RelationalExpression* expr) {
     auto* left = expr->left();
     auto* right = expr->right();
     // Does not support filter : schema.col1 > schema.col2
@@ -144,7 +276,7 @@ Status IndexScanValidator::checkRelExpr(RelationalExpression* expr,
                                     expr->toString().c_str());
     } else if (left->kind() == Expression::Kind::kLabelAttribute ||
                right->kind() == Expression::Kind::kLabelAttribute) {
-        auto ret = rewriteRelExpr(expr, from);
+        auto ret = rewriteRelExpr(expr);
         NG_RETURN_IF_ERROR(ret);
     } else {
         return Status::NotSupported("Expression %s not supported yet",
@@ -153,24 +285,20 @@ Status IndexScanValidator::checkRelExpr(RelationalExpression* expr,
     return Status::OK();
 }
 
-Status IndexScanValidator::rewriteRelExpr(RelationalExpression* expr,
-                                          const std::string& from) {
+Status IndexScanValidator::rewriteRelExpr(RelationalExpression* expr) {
     auto* left = expr->left();
     auto* right = expr->right();
     auto leftIsAE = left->kind() == Expression::Kind::kLabelAttribute;
 
-    std::string ref, prop;
     auto* la = leftIsAE
                ? static_cast<LabelAttributeExpression *>(left)
                : static_cast<LabelAttributeExpression *>(right);
-    if (*la->left()->name() != from) {
+    if (*la->left()->name() != from_) {
         return Status::SemanticError("Schema name error : %s",
                                      la->left()->name()->c_str());
     }
 
-    ref = *la->left()->name();
-    prop = *la->right()->name();
-
+    std::string prop = *la->right()->name();
     // rewrite ConstantExpression
     auto c = leftIsAE
              ? checkConstExpr(right, prop)
@@ -217,5 +345,47 @@ StatusOr<Value> IndexScanValidator::checkConstExpr(Expression* expr,
     return v;
 }
 
-}   // namespace graph
-}   // namespace nebula
+Status IndexScanValidator::checkTSService() {
+    auto tcs = qctx_->getMetaClient()->getFTClientsFromCache();
+    if (!tcs.ok()) {
+        return tcs.status();
+    }
+    if (tcs.value().empty()) {
+        return Status::Error("No full text client found");
+    }
+    textSearchReady_ = true;
+    for (const auto& c : tcs.value()) {
+        nebula::plugin::HttpClient hc;
+        hc.host = c.host;
+        if (c.__isset.user && c.__isset.pwd) {
+            hc.user = c.user;
+            hc.password = c.pwd;
+        }
+        esClients_.emplace_back(std::move(hc));
+    }
+    return checkTSIndex();
+}
+
+Status IndexScanValidator::checkTSIndex() {
+    auto ftIndex = nebula::plugin::IndexTraits::indexName(space_.name, isEdge_);
+    auto retryCnt = FLAGS_ft_request_retry_times;
+    StatusOr<bool> ret = Status::Error("fulltext index not found : %s", ftIndex.c_str());
+    while (--retryCnt > 0) {
+        ret = nebula::plugin::ESGraphAdapter::kAdapter->indexExists(randomFTClient(), ftIndex);
+        if (!ret.ok()) {
+            continue;
+        } else if (ret.value()) {
+            return Status::OK();
+        } else {
+            return Status::Error("fulltext index not found : %s", ftIndex.c_str());
+        }
+    }
+    return ret.status();
+}
+
+const nebula::plugin::HttpClient& IndexScanValidator::randomFTClient() const {
+    auto i = folly::Random::rand32(esClients_.size() - 1);
+    return esClients_[i];
+}
+}  // namespace graph
+}  // namespace nebula
diff --git a/src/validator/IndexScanValidator.h b/src/validator/IndexScanValidator.h
index 4d27eed3..839544e1 100644
--- a/src/validator/IndexScanValidator.h
+++ b/src/validator/IndexScanValidator.h
@@ -9,10 +9,10 @@
 #include <planner/Query.h>
 #include "common/base/Base.h"
 #include "common/interface/gen-cpp2/storage_types.h"
+#include "common/plugin/fulltext/elasticsearch/ESGraphAdapter.h"
 #include "parser/TraverseSentences.h"
 #include "validator/Validator.h"
 
-
 namespace nebula {
 namespace graph {
 
@@ -32,20 +32,36 @@ private:
 
     Status prepareFilter();
 
-    Status checkFilter(Expression* expr, const std::string& from);
+    StatusOr<std::string> rewriteTSFilter(Expression* expr);
+
+    StatusOr<std::vector<std::string>> textSearch(TextSearchExpression* expr);
+
+    bool needTextSearch(Expression* expr);
+
+    Status checkFilter(Expression* expr);
 
-    Status checkRelExpr(RelationalExpression* expr, const std::string& from);
+    Status checkRelExpr(RelationalExpression* expr);
 
-    Status rewriteRelExpr(RelationalExpression* expr, const std::string& from);
+    Status rewriteRelExpr(RelationalExpression* expr);
 
     StatusOr<Value> checkConstExpr(Expression* expr, const std::string& prop);
 
+    Status checkTSService();
+
+    Status checkTSIndex();
+
+    const nebula::plugin::HttpClient& randomFTClient() const;
+
 private:
-    GraphSpaceID               spaceId_{0};
-    IndexScan::IndexQueryCtx   contexts_{nullptr};
-    IndexScan::IndexReturnCols returnCols_{};
-    bool                       isEdge_{false};
-    int32_t                    schemaId_;
+    GraphSpaceID                      spaceId_{0};
+    IndexScan::IndexQueryCtx          contexts_{};
+    IndexScan::IndexReturnCols        returnCols_{};
+    bool                              isEdge_{false};
+    int32_t                           schemaId_;
+    bool                              isEmptyResultSet_{false};
+    bool                              textSearchReady_{false};
+    std::string                       from_;
+    std::vector<nebula::plugin::HttpClient> esClients_;
 };
 
 }   // namespace graph
diff --git a/src/validator/Validator.cpp b/src/validator/Validator.cpp
index b51b02c8..63aae3fa 100644
--- a/src/validator/Validator.cpp
+++ b/src/validator/Validator.cpp
@@ -227,6 +227,12 @@ std::unique_ptr<Validator> Validator::makeValidator(Sentence* sentence, QueryCon
             return std::make_unique<ShowListenerValidator>(sentence, context);
         case Sentence::Kind::kShowStats:
             return std::make_unique<ShowStatusValidator>(sentence, context);
+        case Sentence::Kind::kShowTSClients:
+            return std::make_unique<ShowTSClientsValidator>(sentence, context);
+        case Sentence::Kind::kSignInTSService:
+            return std::make_unique<SignInTSServiceValidator>(sentence, context);
+        case Sentence::Kind::kSignOutTSService:
+            return std::make_unique<SignOutTSServiceValidator>(sentence, context);
         case Sentence::Kind::kShowGroups:
         case Sentence::Kind::kShowZones:
         case Sentence::Kind::kUnknown:
diff --git a/src/validator/test/CMakeLists.txt b/src/validator/test/CMakeLists.txt
index 5a9f7fb2..49b3fe95 100644
--- a/src/validator/test/CMakeLists.txt
+++ b/src/validator/test/CMakeLists.txt
@@ -48,6 +48,7 @@ set(VALIDATOR_TEST_LIBS
     $<TARGET_OBJECTS:common_process_obj>
     $<TARGET_OBJECTS:common_time_utils_obj>
     $<TARGET_OBJECTS:common_graph_obj>
+    $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
 )
 
 nebula_add_test(
diff --git a/src/visitor/test/CMakeLists.txt b/src/visitor/test/CMakeLists.txt
index 8d9caf64..7c8e71f6 100644
--- a/src/visitor/test/CMakeLists.txt
+++ b/src/visitor/test/CMakeLists.txt
@@ -50,6 +50,7 @@ nebula_add_test(
         $<TARGET_OBJECTS:common_process_obj>
         $<TARGET_OBJECTS:common_time_utils_obj>
         $<TARGET_OBJECTS:common_graph_obj>
+        $<TARGET_OBJECTS:common_ft_es_graph_adapter_obj>
     LIBRARIES
         gtest
         ${THRIFT_LIBRARIES}
-- 
GitLab