diff --git a/src/parser/Clauses.h b/src/parser/Clauses.h
index a76c5170a151fa5a42c301ba1ec5b8c76fc083e0..c4a47f2d7e96af2ece61a75a4ceaf69ed9ff8bc3 100644
--- a/src/parser/Clauses.h
+++ b/src/parser/Clauses.h
@@ -313,6 +313,14 @@ public:
 
     std::string toString() const;
 
+    const YieldColumn* back() const {
+        return columns_.back().get();
+    }
+
+    YieldColumn* back() {
+        return columns_.back().get();
+    }
+
 private:
     std::vector<std::unique_ptr<YieldColumn>>   columns_;
 };
diff --git a/src/validator/CMakeLists.txt b/src/validator/CMakeLists.txt
index 3baa16bd11814eb6507698ed7435653c63d0b427..ef915f1da5fe02c2e22d8df3baa349750f4e5d1a 100644
--- a/src/validator/CMakeLists.txt
+++ b/src/validator/CMakeLists.txt
@@ -28,7 +28,7 @@ nebula_add_library(
     ExplainValidator.cpp
     GroupByValidator.cpp
     FindPathValidator.cpp
-    IndexScanValidator.cpp
+    LookupValidator.cpp
     MatchValidator.cpp
 )
 
diff --git a/src/validator/IndexScanValidator.cpp b/src/validator/LookupValidator.cpp
similarity index 62%
rename from src/validator/IndexScanValidator.cpp
rename to src/validator/LookupValidator.cpp
index 42ea9877dae7e5822adb2a57ec00f88ae9c34c1e..19e1f865b40569f519f3eea17e0e7500fe580851 100644
--- a/src/validator/IndexScanValidator.cpp
+++ b/src/validator/LookupValidator.cpp
@@ -4,7 +4,7 @@
  * attached with Common Clause Condition 1.0, found in the LICENSES directory.
  */
 
-#include "validator/IndexScanValidator.h"
+#include "validator/LookupValidator.h"
 #include "planner/Query.h"
 #include "util/ExpressionUtils.h"
 #include "util/SchemaUtil.h"
@@ -14,21 +14,21 @@ DECLARE_uint32(ft_request_retry_times);
 namespace nebula {
 namespace graph {
 
-/*static*/ constexpr char IndexScanValidator::kSrcVID[];
-/*static*/ constexpr char IndexScanValidator::kDstVID[];
-/*static*/ constexpr char IndexScanValidator::kRanking[];
+/*static*/ constexpr char LookupValidator::kSrcVID[];
+/*static*/ constexpr char LookupValidator::kDstVID[];
+/*static*/ constexpr char LookupValidator::kRanking[];
 
-/*static*/ constexpr char IndexScanValidator::kVertexID[];
+/*static*/ constexpr char LookupValidator::kVertexID[];
 
-Status IndexScanValidator::validateImpl() {
+Status LookupValidator::validateImpl() {
     NG_RETURN_IF_ERROR(prepareFrom());
     NG_RETURN_IF_ERROR(prepareYield());
     NG_RETURN_IF_ERROR(prepareFilter());
     return Status::OK();
 }
 
-Status IndexScanValidator::toPlan() {
-    auto *is = IndexScan::make(qctx_,
+Status LookupValidator::toPlan() {
+    auto* is = IndexScan::make(qctx_,
                                nullptr,
                                spaceId_,
                                std::move(contexts_),
@@ -36,14 +36,33 @@ Status IndexScanValidator::toPlan() {
                                isEdge_,
                                schemaId_,
                                isEmptyResultSet_);
-    is->setColNames(std::move(colNames_));
-    root_ = is;
+    is->setColNames(std::move(idxScanColNames_));
+    PlanNode* current = is;
+
+    if (withProject_) {
+        auto* projectNode = Project::make(qctx_, current, newYieldColumns_);
+        projectNode->setInputVar(current->outputVar());
+        projectNode->setColNames(colNames_);
+        current = projectNode;
+    }
+
+    if (dedup_) {
+        auto* dedupNode = Dedup::make(qctx_, current);
+        dedupNode->setInputVar(current->outputVar());
+        dedupNode->setColNames(colNames_);
+        current = dedupNode;
+
+        // the framework will add data collect to collect the result
+        // if the result is required
+    }
+
+    root_ = current;
     tail_ = is;
     return Status::OK();
 }
 
-Status IndexScanValidator::prepareFrom() {
-    auto *sentence = static_cast<const LookupSentence *>(sentence_);
+Status LookupValidator::prepareFrom() {
+    auto* sentence = static_cast<const LookupSentence*>(sentence_);
     spaceId_ = vctx_->whichSpace().id;
     from_ = *sentence->from();
     auto ret = qctx_->schemaMng()->getSchemaIDByName(spaceId_, from_);
@@ -55,72 +74,100 @@ Status IndexScanValidator::prepareFrom() {
     return Status::OK();
 }
 
-Status IndexScanValidator::prepareYield() {
-    auto *sentence = static_cast<const LookupSentence *>(sentence_);
+Status LookupValidator::prepareYield() {
+    auto* sentence = static_cast<const LookupSentence*>(sentence_);
     returnCols_ = std::make_unique<std::vector<std::string>>();
     // always return
     if (isEdge_) {
         returnCols_->emplace_back(kSrc);
-        colNames_.emplace_back(kSrcVID);
+        idxScanColNames_.emplace_back(kSrcVID);
+        colNames_.emplace_back(idxScanColNames_.back());
+        outputs_.emplace_back(colNames_.back(), Value::Type::STRING);
         returnCols_->emplace_back(kDst);
-        colNames_.emplace_back(kDstVID);
+        idxScanColNames_.emplace_back(kDstVID);
+        colNames_.emplace_back(idxScanColNames_.back());
+        outputs_.emplace_back(colNames_.back(), Value::Type::STRING);
         returnCols_->emplace_back(kRank);
-        colNames_.emplace_back(kRanking);
+        idxScanColNames_.emplace_back(kRanking);
+        colNames_.emplace_back(idxScanColNames_.back());
+        outputs_.emplace_back(colNames_.back(), Value::Type::INT);
     } else {
         returnCols_->emplace_back(kVid);
-        colNames_.emplace_back(kVertexID);
+        idxScanColNames_.emplace_back(kVertexID);
+        colNames_.emplace_back(idxScanColNames_.back());
+        outputs_.emplace_back(colNames_.back(), Value::Type::STRING);
     }
     if (sentence->yieldClause() == nullptr) {
         return Status::OK();
     }
-    // When whereClause is nullptr, yieldClause is not nullptr,
-    // only return vid for tag. return src, ranking, dst for edge
-    if (sentence->whereClause() == nullptr) {
-        return Status::SemanticError("Yield clauses are not supported "
-                                     "when WHERE clause does not exist");
+    withProject_ = true;
+    if (sentence->yieldClause()->isDistinct()) {
+        dedup_ = true;
+    }
+    newYieldColumns_ = qctx_->objPool()->makeAndAdd<YieldColumns>();
+    if (isEdge_) {
+        // default columns
+        newYieldColumns_->addColumn(new YieldColumn(
+            new InputPropertyExpression(new std::string(kSrcVID)), new std::string(kSrcVID)));
+        newYieldColumns_->addColumn(new YieldColumn(
+            new InputPropertyExpression(new std::string(kDstVID)), new std::string(kDstVID)));
+        newYieldColumns_->addColumn(new YieldColumn(
+            new InputPropertyExpression(new std::string(kRanking)), new std::string(kRanking)));
+    } else {
+        newYieldColumns_->addColumn(new YieldColumn(
+            new InputPropertyExpression(new std::string(kVertexID)), new std::string(kVertexID)));
     }
     auto columns = sentence->yieldClause()->columns();
-    auto schema = isEdge_
-                  ? qctx_->schemaMng()->getEdgeSchema(spaceId_, schemaId_)
-                  : qctx_->schemaMng()->getTagSchema(spaceId_, schemaId_);
+    auto schema = isEdge_ ? qctx_->schemaMng()->getEdgeSchema(spaceId_, schemaId_)
+                          : qctx_->schemaMng()->getTagSchema(spaceId_, schemaId_);
     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());
+        return isEdge_ ? 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;
+        // TODO(shylock) support more expr
         if (col->expr()->kind() == Expression::Kind::kLabelAttribute) {
-            auto la = static_cast<LabelAttributeExpression *>(col->expr());
-            schemaName = *la->left()->name();
-            const auto &value = la->right()->value();
-            colName = value.getStr();
+            auto la = static_cast<LabelAttributeExpression*>(col->expr());
+            const std::string &schemaName = *la->left()->name();
+            const auto& value = la->right()->value();
+            const std::string &colName = value.getStr();
+            if (isEdge_) {
+                newYieldColumns_->addColumn(new YieldColumn(new EdgePropertyExpression(
+                    new std::string(schemaName), new std::string(colName))));
+            } else {
+                newYieldColumns_->addColumn(new YieldColumn(new TagPropertyExpression(
+                    new std::string(schemaName), new std::string(colName))));
+            }
+            if (col->alias() != nullptr) {
+                newYieldColumns_->back()->setAlias(new std::string(*col->alias()));
+            }
+            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());
+            }
+            returnCols_->emplace_back(colName);
+            idxScanColNames_.emplace_back(from_ + "." + colName);
+            colNames_.emplace_back(deduceColName(newYieldColumns_->back()));
+            outputs_.emplace_back(colNames_.back(), SchemaUtil::propTypeToValueType(ret));
         } else {
             return Status::SemanticError("Yield clauses are not supported : %s",
                                          col->expr()->toString().c_str());
         }
-
-        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());
-        }
-        returnCols_->emplace_back(colName);
-        colNames_.emplace_back(from_ + "." + colName);
     }
     return Status::OK();
 }
 
-Status IndexScanValidator::prepareFilter() {
-    auto *sentence = static_cast<const LookupSentence *>(sentence_);
+Status LookupValidator::prepareFilter() {
+    auto* sentence = static_cast<const LookupSentence*>(sentence_);
     if (sentence->whereClause() == nullptr) {
         return Status::OK();
     }
 
-    auto *filter = sentence->whereClause()->filter();
+    auto* filter = sentence->whereClause()->filter();
     storage::cpp2::IndexQueryContext ctx;
     if (needTextSearch(filter)) {
         NG_RETURN_IF_ERROR(checkTSService());
@@ -146,8 +193,7 @@ Status IndexScanValidator::prepareFilter() {
     return Status::OK();
 }
 
-StatusOr<std::string>
-IndexScanValidator::rewriteTSFilter(Expression* expr) {
+StatusOr<std::string> LookupValidator::rewriteTSFilter(Expression* expr) {
     std::vector<std::string> values;
     auto tsExpr = static_cast<TextSearchExpression*>(expr);
     auto vRet = textSearch(tsExpr);
@@ -183,7 +229,7 @@ IndexScanValidator::rewriteTSFilter(Expression* expr) {
     return newExpr->encode();
 }
 
-StatusOr<std::vector<std::string>> IndexScanValidator::textSearch(TextSearchExpression* expr) {
+StatusOr<std::vector<std::string>> LookupValidator::textSearch(TextSearchExpression* expr) {
     if (*expr->arg()->from() != from_) {
         return Status::SemanticError("Schema name error : %s", expr->arg()->from()->c_str());
     }
@@ -204,33 +250,23 @@ StatusOr<std::vector<std::string>> IndexScanValidator::textSearch(TextSearchExpr
                     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);
+                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);
+                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);
+                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);
+                ret = nebula::plugin::ESGraphAdapter::kAdapter->wildcard(
+                    randomFTClient(), doc, limit, result);
                 break;
             }
             default:
@@ -248,7 +284,7 @@ StatusOr<std::vector<std::string>> IndexScanValidator::textSearch(TextSearchExpr
     return Status::Error("scan external index failed");
 }
 
-bool IndexScanValidator::needTextSearch(Expression* expr) {
+bool LookupValidator::needTextSearch(Expression* expr) {
     switch (expr->kind()) {
         case Expression::Kind::kTSFuzzy:
         case Expression::Kind::kTSPrefix:
@@ -261,12 +297,12 @@ bool IndexScanValidator::needTextSearch(Expression* expr) {
     }
 }
 
-Status IndexScanValidator::checkFilter(Expression* expr) {
+Status LookupValidator::checkFilter(Expression* expr) {
     // TODO (sky) : Rewrite simple expressions,
     //              for example rewrite expr from col1 > 1 + 2 to col > 3
     switch (expr->kind()) {
-        case Expression::Kind::kLogicalOr :
-        case Expression::Kind::kLogicalAnd : {
+        case Expression::Kind::kLogicalOr:
+        case Expression::Kind::kLogicalAnd: {
             // TODO(dutor) Deal with n-ary operands
             auto lExpr = static_cast<LogicalExpression*>(expr);
             auto ret = checkFilter(lExpr->operand(0));
@@ -292,43 +328,37 @@ Status IndexScanValidator::checkFilter(Expression* expr) {
     return Status::OK();
 }
 
-Status IndexScanValidator::checkRelExpr(RelationalExpression* expr) {
+Status LookupValidator::checkRelExpr(RelationalExpression* expr) {
     auto* left = expr->left();
     auto* right = expr->right();
     // Does not support filter : schema.col1 > schema.col2
     if (left->kind() == Expression::Kind::kLabelAttribute &&
         right->kind() == Expression::Kind::kLabelAttribute) {
-        return Status::NotSupported("Expression %s not supported yet",
-                                    expr->toString().c_str());
+        return Status::NotSupported("Expression %s not supported yet", expr->toString().c_str());
     } else if (left->kind() == Expression::Kind::kLabelAttribute ||
                right->kind() == Expression::Kind::kLabelAttribute) {
         auto ret = rewriteRelExpr(expr);
         NG_RETURN_IF_ERROR(ret);
     } else {
-        return Status::NotSupported("Expression %s not supported yet",
-                                    expr->toString().c_str());
+        return Status::NotSupported("Expression %s not supported yet", expr->toString().c_str());
     }
     return Status::OK();
 }
 
-Status IndexScanValidator::rewriteRelExpr(RelationalExpression* expr) {
+Status LookupValidator::rewriteRelExpr(RelationalExpression* expr) {
     auto* left = expr->left();
     auto* right = expr->right();
     auto leftIsAE = left->kind() == Expression::Kind::kLabelAttribute;
 
-    auto* la = leftIsAE
-               ? static_cast<LabelAttributeExpression *>(left)
-               : static_cast<LabelAttributeExpression *>(right);
+    auto* la = leftIsAE ? static_cast<LabelAttributeExpression*>(left)
+                        : static_cast<LabelAttributeExpression*>(right);
     if (*la->left()->name() != from_) {
-        return Status::SemanticError("Schema name error : %s",
-                                     la->left()->name()->c_str());
+        return Status::SemanticError("Schema name error : %s", la->left()->name()->c_str());
     }
 
     std::string prop = la->right()->value().getStr();
     // rewrite ConstantExpression
-    auto c = leftIsAE
-             ? checkConstExpr(right, prop)
-             : checkConstExpr(left, prop);
+    auto c = leftIsAE ? checkConstExpr(right, prop) : checkConstExpr(left, prop);
 
     if (!c.ok()) {
         return Status::SemanticError("expression error : %s", left->toString().c_str());
@@ -357,11 +387,9 @@ Status IndexScanValidator::rewriteRelExpr(RelationalExpression* expr) {
     return Status::OK();
 }
 
-StatusOr<Value> IndexScanValidator::checkConstExpr(Expression* expr,
-                                                   const std::string& prop) {
-    auto schema = isEdge_
-                  ? qctx_->schemaMng()->getEdgeSchema(spaceId_, schemaId_)
-                  : qctx_->schemaMng()->getTagSchema(spaceId_, schemaId_);
+StatusOr<Value> LookupValidator::checkConstExpr(Expression* expr, const std::string& prop) {
+    auto schema = isEdge_ ? qctx_->schemaMng()->getEdgeSchema(spaceId_, schemaId_)
+                          : qctx_->schemaMng()->getTagSchema(spaceId_, schemaId_);
     auto type = schema->getFieldType(prop);
     QueryExpressionContext dummy(nullptr);
     auto v = Expression::eval(expr, dummy);
@@ -371,7 +399,7 @@ StatusOr<Value> IndexScanValidator::checkConstExpr(Expression* expr,
     return v;
 }
 
-Status IndexScanValidator::checkTSService() {
+Status LookupValidator::checkTSService() {
     auto tcs = qctx_->getMetaClient()->getFTClientsFromCache();
     if (!tcs.ok()) {
         return tcs.status();
@@ -392,7 +420,7 @@ Status IndexScanValidator::checkTSService() {
     return checkTSIndex();
 }
 
-Status IndexScanValidator::checkTSIndex() {
+Status LookupValidator::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());
@@ -409,9 +437,9 @@ Status IndexScanValidator::checkTSIndex() {
     return ret.status();
 }
 
-const nebula::plugin::HttpClient& IndexScanValidator::randomFTClient() const {
+const nebula::plugin::HttpClient& LookupValidator::randomFTClient() const {
     auto i = folly::Random::rand32(esClients_.size() - 1);
     return esClients_[i];
 }
-}  // namespace graph
-}  // namespace nebula
+}   // namespace graph
+}   // namespace nebula
diff --git a/src/validator/IndexScanValidator.h b/src/validator/LookupValidator.h
similarity index 86%
rename from src/validator/IndexScanValidator.h
rename to src/validator/LookupValidator.h
index c26e0531b13a9ec62cfe916f065a0cf574696bbb..0699455327e21a6bc87b8c9e3530817ff2c11751 100644
--- a/src/validator/IndexScanValidator.h
+++ b/src/validator/LookupValidator.h
@@ -16,9 +16,9 @@
 namespace nebula {
 namespace graph {
 
-class IndexScanValidator final : public Validator {
+class LookupValidator final : public Validator {
 public:
-    IndexScanValidator(Sentence* sentence, QueryContext* context)
+    LookupValidator(Sentence* sentence, QueryContext* context)
         : Validator(sentence, context) {}
 
 private:
@@ -68,7 +68,11 @@ private:
     bool                              textSearchReady_{false};
     std::string                       from_;
     std::vector<nebula::plugin::HttpClient> esClients_;
+    std::vector<std::string>          idxScanColNames_;
     std::vector<std::string>          colNames_;
+    bool                              withProject_{false};
+    bool                              dedup_{false};
+    YieldColumns                     *newYieldColumns_{nullptr};
 };
 
 }   // namespace graph
diff --git a/src/validator/Validator.cpp b/src/validator/Validator.cpp
index 63aae3fae8a02aa8f74886830d625436b648a9b7..6976f1be1cf28fdf6ddba42a3daf12e2a7c6c927 100644
--- a/src/validator/Validator.cpp
+++ b/src/validator/Validator.cpp
@@ -38,7 +38,7 @@
 #include "validator/GroupByValidator.h"
 #include "validator/MatchValidator.h"
 #include "visitor/EvaluableExprVisitor.h"
-#include "validator/IndexScanValidator.h"
+#include "validator/LookupValidator.h"
 
 namespace nebula {
 namespace graph {
@@ -194,7 +194,7 @@ std::unique_ptr<Validator> Validator::makeValidator(Sentence* sentence, QueryCon
         case Sentence::Kind::kDropEdgeIndex:
             return std::make_unique<DropEdgeIndexValidator>(sentence, context);
         case Sentence::Kind::kLookup:
-            return std::make_unique<IndexScanValidator>(sentence, context);
+            return std::make_unique<LookupValidator>(sentence, context);
         case Sentence::Kind::kAddGroup:
             return std::make_unique<AddGroupValidator>(sentence, context);
         case Sentence::Kind::kDropGroup:
diff --git a/src/validator/test/CMakeLists.txt b/src/validator/test/CMakeLists.txt
index 49b3fe95869ffe6357a116ca478c8e89e8ea966f..cb410652516bb3fa6a1c210622429064e7864cce 100644
--- a/src/validator/test/CMakeLists.txt
+++ b/src/validator/test/CMakeLists.txt
@@ -69,6 +69,7 @@ nebula_add_test(
         ValidatorTestBase.cpp
         ExplainValidatorTest.cpp
         GroupByValidatorTest.cpp
+        LookupValidatorTest.cpp
         SymbolsTest.cpp
     OBJECTS
         ${VALIDATOR_TEST_LIBS}
diff --git a/src/validator/test/LookupValidatorTest.cpp b/src/validator/test/LookupValidatorTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..83fcc80cc08ad992441d3399a6b65b30a3d92658
--- /dev/null
+++ b/src/validator/test/LookupValidatorTest.cpp
@@ -0,0 +1,84 @@
+/* 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/base/ObjectPool.h"
+#include "planner/Logic.h"
+#include "planner/Query.h"
+#include "validator/LookupValidator.h"
+#include "validator/test/ValidatorTestBase.h"
+
+namespace nebula {
+namespace graph {
+
+class LookupValidatorTest : public ValidatorTestBase {};
+
+TEST_F(LookupValidatorTest, InputOutput) {
+    // pipe
+    {
+        const std::string query = "LOOKUP ON person where person.age == 35 | "
+                                  "FETCH PROP ON person $-.VertexID";
+        EXPECT_TRUE(checkResult(query,
+                                {
+                                    PlanNode::Kind::kGetVertices,
+                                    PlanNode::Kind::kIndexScan,
+                                    PlanNode::Kind::kStart,
+                                }));
+    }
+    // pipe with yield
+    {
+        const std::string query =
+            "LOOKUP ON person where person.age == 35 YIELD person.name AS name | "
+            "FETCH PROP ON person $-.name";
+        EXPECT_TRUE(checkResult(query,
+                                {
+                                    PlanNode::Kind::kGetVertices,
+                                    PlanNode::Kind::kProject,
+                                    PlanNode::Kind::kIndexScan,
+                                    PlanNode::Kind::kStart,
+                                }));
+    }
+    // variable
+    {
+        const std::string query = "$a = LOOKUP ON person where person.age == 35; "
+                                  "FETCH PROP ON person $a.VertexID";
+        EXPECT_TRUE(checkResult(query,
+                                {
+                                    PlanNode::Kind::kGetVertices,
+                                    PlanNode::Kind::kIndexScan,
+                                    PlanNode::Kind::kStart,
+                                }));
+    }
+    // var with yield
+    {
+        const std::string query =
+            "$a = LOOKUP ON person where person.age == 35 YIELD person.name AS name;"
+            "FETCH PROP ON person $a.name";
+        EXPECT_TRUE(checkResult(query,
+                                {
+                                    PlanNode::Kind::kGetVertices,
+                                    PlanNode::Kind::kProject,
+                                    PlanNode::Kind::kIndexScan,
+                                    PlanNode::Kind::kStart,
+                                }));
+    }
+}
+
+TEST_F(LookupValidatorTest, InvalidYieldExpression) {
+    // TODO(shylock)
+    {
+        const std::string query =
+            "LOOKUP ON person where person.age == 35 YIELD person.age + 1 AS age;";
+        EXPECT_FALSE(checkResult(query,
+                                 {
+                                     PlanNode::Kind::kProject,
+                                     PlanNode::Kind::kIndexScan,
+                                     PlanNode::Kind::kStart,
+                                 }));
+    }
+}
+
+}   // namespace graph
+}   // namespace nebula
diff --git a/tests/data/nba/config.yaml b/tests/data/nba/config.yaml
index a509970220035466165f24f69b2b79fde8470d50..fbea950c3cca2163f27449d150369aaada6640b6 100644
--- a/tests/data/nba/config.yaml
+++ b/tests/data/nba/config.yaml
@@ -15,6 +15,7 @@ schema: |
   CREATE TAG INDEX IF NOT EXISTS player_name_index ON player(name(64));
   CREATE TAG INDEX IF NOT EXISTS player_age_index ON player(age);
   CREATE TAG INDEX IF NOT EXISTS team_name_index ON team(name(64));
+  CREATE EDGE INDEX IF NOT EXISTS serve_start_end_index ON serve(start_year, end_year);
 files:
   - path: ./player.csv
     withHeader: true
diff --git a/tests/data/nba_int_vid/config.yaml b/tests/data/nba_int_vid/config.yaml
index 939869b6066df0c67f995e90d397733e53441113..45611b4e4d0270a9e3a8c2d7fc12c1e714386a87 100644
--- a/tests/data/nba_int_vid/config.yaml
+++ b/tests/data/nba_int_vid/config.yaml
@@ -15,6 +15,7 @@ schema: |
   CREATE TAG INDEX IF NOT EXISTS player_name_index ON player(name(64));
   CREATE TAG INDEX IF NOT EXISTS player_age_index ON player(age);
   CREATE TAG INDEX IF NOT EXISTS team_name_index ON team(name(64));
+  CREATE EDGE INDEX IF NOT EXISTS serve_start_end_index ON serve(start_year, end_year);
 files:
   - path: ../nba/player.csv
     withHeader: true
diff --git a/tests/query/stateless/test_lookup.py b/tests/query/stateless/test_lookup.py
index 334b17d7616cc53b98cb1710272ee3c44603ee72..16a1e8437d6b598c71c59dcb03cb8403fad31d04 100644
--- a/tests/query/stateless/test_lookup.py
+++ b/tests/query/stateless/test_lookup.py
@@ -17,7 +17,6 @@ class TestIndex(NebulaTestSuite):
                     replica_factor=self.replica_factor))
         self.check_resp_succeeded(resp)
 
-    def test_edge_index(self):
         time.sleep(self.delay)
         resp = self.execute('USE nbaLookup')
         self.check_resp_succeeded(resp)
@@ -60,30 +59,6 @@ class TestIndex(NebulaTestSuite):
         resp = self.execute('INSERT EDGE serve(start_year, end_year) VALUES "121" -> "201":(1999, 2018)')
         self.check_resp_succeeded(resp)
 
-        resp = self.execute('LOOKUP ON serve where serve.start_year > 0')
-        self.check_resp_succeeded(resp)
-        assert resp.row_size() == 6
-
-        resp = self.execute('LOOKUP ON serve where serve.start_year > 1997 and serve.end_year < 2020')
-        self.check_resp_succeeded(resp)
-        assert resp.row_size() == 3
-
-        resp = self.execute('LOOKUP ON serve where serve.start_year > 2000 and serve.end_year < 2020')
-        self.check_resp_succeeded(resp)
-        self.check_empty_result(resp)
-
-        resp = self.execute('LOOKUP ON like where like.likeness > 89')
-        self.check_resp_succeeded(resp)
-        assert resp.row_size() == 3
-
-        resp = self.execute('LOOKUP ON like where like.likeness < 39')
-        self.check_resp_succeeded(resp)
-        self.check_empty_result(resp)
-
-    def test_tag_index(self):
-        time.sleep(self.delay)
-        resp = self.execute('USE nbaLookup')
-        self.check_resp_succeeded(resp)
         resp = self.execute("CREATE TAG player (name FIXED_STRING(30), age INT)")
         self.check_resp_succeeded(resp)
         resp = self.execute("CREATE TAG team (name FIXED_STRING(30))")
@@ -127,33 +102,113 @@ class TestIndex(NebulaTestSuite):
         resp = self.execute('INSERT VERTEX team(name) VALUES "204":("opl")')
         self.check_resp_succeeded(resp)
 
+
+    def test_edge_index(self):
+        resp = self.execute('LOOKUP ON serve where serve.start_year > 0')
+        self.check_resp_succeeded(resp)
+        col_names = ['SrcVID', 'DstVID', 'Ranking']
+        self.check_column_names(resp, col_names)
+        expected_result = [['100', '200', 0],
+                           ['101', '201', 0],
+                           ['102', '202', 0],
+                           ['103', '203', 0],
+                           ['105', '204', 0],
+                           ['121', '201', 0]]
+        self.check_out_of_order_result(resp, expected_result)
+
+        resp = self.execute('LOOKUP ON serve where serve.start_year > 1997 and serve.end_year < 2020')
+        self.check_resp_succeeded(resp)
+        col_names = ['SrcVID', 'DstVID', 'Ranking']
+        self.check_column_names(resp, col_names)
+        expected_result = [['101', '201', 0],
+                           ['103', '203', 0],
+                           ['121', '201', 0]]
+        self.check_out_of_order_result(resp, expected_result)
+
+        resp = self.execute('LOOKUP ON serve where serve.start_year > 2000 and serve.end_year < 2020')
+        self.check_resp_succeeded(resp)
+        col_names = ['SrcVID', 'DstVID', 'Ranking']
+        self.check_column_names(resp, col_names)
+        self.check_empty_result(resp)
+
+        resp = self.execute('LOOKUP ON like where like.likeness > 89')
+        self.check_resp_succeeded(resp)
+        col_names = ['SrcVID', 'DstVID', 'Ranking']
+        self.check_column_names(resp, col_names)
+        expected_result = [['100', '101', 0],
+                           ['101', '102', 0],
+                           ['105', '106', 0]]
+        self.check_out_of_order_result(resp, expected_result)
+
+        resp = self.execute('LOOKUP ON like where like.likeness < 39')
+        self.check_resp_succeeded(resp)
+        col_names = ['SrcVID', 'DstVID', 'Ranking']
+        self.check_column_names(resp, col_names)
+        self.check_empty_result(resp)
+
+    def test_tag_index(self):
         resp = self.execute('LOOKUP ON player where player.age == 35')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 1
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['103']]
+        self.check_out_of_order_result(resp, expected_result)
 
         resp = self.execute('LOOKUP ON player where player.age > 0')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 8
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['100'],
+                           ['101'],
+                           ['102'],
+                           ['103'],
+                           ['104'],
+                           ['105'],
+                           ['106'],
+                           ['121']]
+        self.check_out_of_order_result(resp, expected_result)
 
         resp = self.execute('LOOKUP ON player where player.age < 100')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 8
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['100'],
+                           ['101'],
+                           ['102'],
+                           ['103'],
+                           ['104'],
+                           ['105'],
+                           ['106'],
+                           ['121']]
+        self.check_out_of_order_result(resp, expected_result)
 
         resp = self.execute('LOOKUP ON player where player.name == "Useless"')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 1
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['121']]
+        self.check_out_of_order_result(resp, expected_result)
 
         resp = self.execute('LOOKUP ON player where player.name == "Useless" and player.age < 30')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 1
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['121']]
+        self.check_out_of_order_result(resp, expected_result)
 
         resp = self.execute('LOOKUP ON team where team.name == "Warriors"')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 1
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['200']]
+        self.check_out_of_order_result(resp, expected_result)
 
         resp = self.execute('LOOKUP ON team where team.name == "oopp"')
         self.check_resp_succeeded(resp)
-        assert resp.row_size() == 1
+        col_names = ['VertexID']
+        self.check_column_names(resp, col_names)
+        expected_result = [['202']]
+        self.check_out_of_order_result(resp, expected_result)
 
     @classmethod
     def cleanup(self):
diff --git a/tests/tck/features/lookup/ByIndex.feature b/tests/tck/features/lookup/ByIndex.feature
new file mode 100644
index 0000000000000000000000000000000000000000..f033ea93a25bfec56dedd3d3656f47081b976f19
--- /dev/null
+++ b/tests/tck/features/lookup/ByIndex.feature
@@ -0,0 +1,396 @@
+Feature: Lookup by index itself
+
+  Background: Prepare space
+    Given a graph with space named "nba"
+
+  Scenario: [1] tag index
+    When executing query:
+      """
+      LOOKUP ON team
+      """
+    Then the result should be, in any order:
+      | VertexID        |
+      | 'Nets'          |
+      | 'Pistons'       |
+      | 'Bucks'         |
+      | 'Mavericks'     |
+      | 'Clippers'      |
+      | 'Thunders'      |
+      | 'Lakers'        |
+      | 'Jazz'          |
+      | 'Nuggets'       |
+      | 'Wizards'       |
+      | 'Pacers'        |
+      | 'Timberwolves'  |
+      | 'Hawks'         |
+      | 'Warriors'      |
+      | 'Magic'         |
+      | 'Rockets'       |
+      | 'Pelicans'      |
+      | 'Raptors'       |
+      | 'Spurs'         |
+      | 'Heat'          |
+      | 'Grizzlies'     |
+      | 'Knicks'        |
+      | 'Suns'          |
+      | 'Hornets'       |
+      | 'Cavaliers'     |
+      | 'Kings'         |
+      | 'Celtics'       |
+      | '76ers'         |
+      | 'Trail Blazers' |
+      | 'Bulls'         |
+    When executing query:
+      """
+      LOOKUP ON team YIELD team.name AS Name
+      """
+    Then the result should be, in any order:
+      | VertexID        | Name            |
+      | 'Nets'          | 'Nets'          |
+      | 'Pistons'       | 'Pistons'       |
+      | 'Bucks'         | 'Bucks'         |
+      | 'Mavericks'     | 'Mavericks'     |
+      | 'Clippers'      | 'Clippers'      |
+      | 'Thunders'      | 'Thunders'      |
+      | 'Lakers'        | 'Lakers'        |
+      | 'Jazz'          | 'Jazz'          |
+      | 'Nuggets'       | 'Nuggets'       |
+      | 'Wizards'       | 'Wizards'       |
+      | 'Pacers'        | 'Pacers'        |
+      | 'Timberwolves'  | 'Timberwolves'  |
+      | 'Hawks'         | 'Hawks'         |
+      | 'Warriors'      | 'Warriors'      |
+      | 'Magic'         | 'Magic'         |
+      | 'Rockets'       | 'Rockets'       |
+      | 'Pelicans'      | 'Pelicans'      |
+      | 'Raptors'       | 'Raptors'       |
+      | 'Spurs'         | 'Spurs'         |
+      | 'Heat'          | 'Heat'          |
+      | 'Grizzlies'     | 'Grizzlies'     |
+      | 'Knicks'        | 'Knicks'        |
+      | 'Suns'          | 'Suns'          |
+      | 'Hornets'       | 'Hornets'       |
+      | 'Cavaliers'     | 'Cavaliers'     |
+      | 'Kings'         | 'Kings'         |
+      | 'Celtics'       | 'Celtics'       |
+      | '76ers'         | '76ers'         |
+      | 'Trail Blazers' | 'Trail Blazers' |
+      | 'Bulls'         | 'Bulls'         |
+
+  Scenario: [2] edge index
+    When executing query:
+      """
+      LOOKUP ON serve
+      """
+    Then the result should be, in any order:
+      | SrcVID                  | DstVID          | Ranking |
+      | "Amar'e Stoudemire"     | 'Suns'          | 0       |
+      | "Amar'e Stoudemire"     | 'Knicks'        | 0       |
+      | "Amar'e Stoudemire"     | 'Heat'          | 0       |
+      | 'Russell Westbrook'     | 'Thunders'      | 0       |
+      | 'James Harden'          | 'Thunders'      | 0       |
+      | 'James Harden'          | 'Rockets'       | 0       |
+      | 'Kobe Bryant'           | 'Lakers'        | 0       |
+      | 'Tracy McGrady'         | 'Raptors'       | 0       |
+      | 'Tracy McGrady'         | 'Magic'         | 0       |
+      | 'Tracy McGrady'         | 'Rockets'       | 0       |
+      | 'Tracy McGrady'         | 'Spurs'         | 0       |
+      | 'Chris Paul'            | 'Hornets'       | 0       |
+      | 'Chris Paul'            | 'Clippers'      | 0       |
+      | 'Chris Paul'            | 'Rockets'       | 0       |
+      | 'Boris Diaw'            | 'Hawks'         | 0       |
+      | 'Boris Diaw'            | 'Suns'          | 0       |
+      | 'Boris Diaw'            | 'Hornets'       | 0       |
+      | 'Boris Diaw'            | 'Spurs'         | 0       |
+      | 'Boris Diaw'            | 'Jazz'          | 0       |
+      | 'LeBron James'          | 'Cavaliers'     | 0       |
+      | 'LeBron James'          | 'Heat'          | 0       |
+      | 'LeBron James'          | 'Cavaliers'     | 1       |
+      | 'LeBron James'          | 'Lakers'        | 0       |
+      | 'Klay Thompson'         | 'Warriors'      | 0       |
+      | 'Kristaps Porzingis'    | 'Knicks'        | 0       |
+      | 'Kristaps Porzingis'    | 'Mavericks'     | 0       |
+      | 'Jonathon Simmons'      | 'Spurs'         | 0       |
+      | 'Jonathon Simmons'      | 'Magic'         | 0       |
+      | 'Jonathon Simmons'      | '76ers'         | 0       |
+      | 'Marco Belinelli'       | 'Warriors'      | 0       |
+      | 'Marco Belinelli'       | 'Raptors'       | 0       |
+      | 'Marco Belinelli'       | 'Hornets'       | 0       |
+      | 'Marco Belinelli'       | 'Bulls'         | 0       |
+      | 'Marco Belinelli'       | 'Spurs'         | 0       |
+      | 'Marco Belinelli'       | 'Kings'         | 0       |
+      | 'Marco Belinelli'       | 'Hornets'       | 1       |
+      | 'Marco Belinelli'       | 'Hawks'         | 0       |
+      | 'Marco Belinelli'       | '76ers'         | 0       |
+      | 'Marco Belinelli'       | 'Spurs'         | 1       |
+      | 'Luka Doncic'           | 'Mavericks'     | 0       |
+      | 'David West'            | 'Hornets'       | 0       |
+      | 'David West'            | 'Pacers'        | 0       |
+      | 'David West'            | 'Spurs'         | 0       |
+      | 'David West'            | 'Warriors'      | 0       |
+      | 'Tony Parker'           | 'Spurs'         | 0       |
+      | 'Tony Parker'           | 'Hornets'       | 0       |
+      | 'Danny Green'           | 'Cavaliers'     | 0       |
+      | 'Danny Green'           | 'Spurs'         | 0       |
+      | 'Danny Green'           | 'Raptors'       | 0       |
+      | 'Rudy Gay'              | 'Grizzlies'     | 0       |
+      | 'Rudy Gay'              | 'Raptors'       | 0       |
+      | 'Rudy Gay'              | 'Kings'         | 0       |
+      | 'Rudy Gay'              | 'Spurs'         | 0       |
+      | 'LaMarcus Aldridge'     | 'Trail Blazers' | 0       |
+      | 'LaMarcus Aldridge'     | 'Spurs'         | 0       |
+      | 'Tim Duncan'            | 'Spurs'         | 0       |
+      | 'Kevin Durant'          | 'Thunders'      | 0       |
+      | 'Kevin Durant'          | 'Warriors'      | 0       |
+      | 'Stephen Curry'         | 'Warriors'      | 0       |
+      | 'Ray Allen'             | 'Bucks'         | 0       |
+      | 'Ray Allen'             | 'Thunders'      | 0       |
+      | 'Ray Allen'             | 'Celtics'       | 0       |
+      | 'Ray Allen'             | 'Heat'          | 0       |
+      | 'Tiago Splitter'        | 'Spurs'         | 0       |
+      | 'Tiago Splitter'        | 'Hawks'         | 0       |
+      | 'Tiago Splitter'        | '76ers'         | 0       |
+      | 'DeAndre Jordan'        | 'Clippers'      | 0       |
+      | 'DeAndre Jordan'        | 'Mavericks'     | 0       |
+      | 'DeAndre Jordan'        | 'Knicks'        | 0       |
+      | 'Paul Gasol'            | 'Grizzlies'     | 0       |
+      | 'Paul Gasol'            | 'Lakers'        | 0       |
+      | 'Paul Gasol'            | 'Bulls'         | 0       |
+      | 'Paul Gasol'            | 'Spurs'         | 0       |
+      | 'Paul Gasol'            | 'Bucks'         | 0       |
+      | 'Aron Baynes'           | 'Spurs'         | 0       |
+      | 'Aron Baynes'           | 'Pistons'       | 0       |
+      | 'Aron Baynes'           | 'Celtics'       | 0       |
+      | 'Cory Joseph'           | 'Spurs'         | 0       |
+      | 'Cory Joseph'           | 'Raptors'       | 0       |
+      | 'Cory Joseph'           | 'Pacers'        | 0       |
+      | 'Vince Carter'          | 'Raptors'       | 0       |
+      | 'Vince Carter'          | 'Nets'          | 0       |
+      | 'Vince Carter'          | 'Magic'         | 0       |
+      | 'Vince Carter'          | 'Suns'          | 0       |
+      | 'Vince Carter'          | 'Mavericks'     | 0       |
+      | 'Vince Carter'          | 'Grizzlies'     | 0       |
+      | 'Vince Carter'          | 'Kings'         | 0       |
+      | 'Vince Carter'          | 'Hawks'         | 0       |
+      | 'Marc Gasol'            | 'Grizzlies'     | 0       |
+      | 'Marc Gasol'            | 'Raptors'       | 0       |
+      | 'Ricky Rubio'           | 'Timberwolves'  | 0       |
+      | 'Ricky Rubio'           | 'Jazz'          | 0       |
+      | 'Ben Simmons'           | '76ers'         | 0       |
+      | 'Giannis Antetokounmpo' | 'Bucks'         | 0       |
+      | 'Rajon Rondo'           | 'Celtics'       | 0       |
+      | 'Rajon Rondo'           | 'Mavericks'     | 0       |
+      | 'Rajon Rondo'           | 'Kings'         | 0       |
+      | 'Rajon Rondo'           | 'Bulls'         | 0       |
+      | 'Rajon Rondo'           | 'Pelicans'      | 0       |
+      | 'Rajon Rondo'           | 'Lakers'        | 0       |
+      | 'Manu Ginobili'         | 'Spurs'         | 0       |
+      | 'Kyrie Irving'          | 'Cavaliers'     | 0       |
+      | 'Kyrie Irving'          | 'Celtics'       | 0       |
+      | 'Carmelo Anthony'       | 'Nuggets'       | 0       |
+      | 'Carmelo Anthony'       | 'Knicks'        | 0       |
+      | 'Carmelo Anthony'       | 'Thunders'      | 0       |
+      | 'Carmelo Anthony'       | 'Rockets'       | 0       |
+      | 'Dwyane Wade'           | 'Heat'          | 0       |
+      | 'Dwyane Wade'           | 'Bulls'         | 0       |
+      | 'Dwyane Wade'           | 'Cavaliers'     | 0       |
+      | 'Dwyane Wade'           | 'Heat'          | 1       |
+      | 'Joel Embiid'           | '76ers'         | 0       |
+      | 'Damian Lillard'        | 'Trail Blazers' | 0       |
+      | 'Yao Ming'              | 'Rockets'       | 0       |
+      | 'Kyle Anderson'         | 'Spurs'         | 0       |
+      | 'Kyle Anderson'         | 'Grizzlies'     | 0       |
+      | 'Dejounte Murray'       | 'Spurs'         | 0       |
+      | 'Blake Griffin'         | 'Clippers'      | 0       |
+      | 'Blake Griffin'         | 'Pistons'       | 0       |
+      | 'Steve Nash'            | 'Suns'          | 0       |
+      | 'Steve Nash'            | 'Mavericks'     | 0       |
+      | 'Steve Nash'            | 'Suns'          | 1       |
+      | 'Steve Nash'            | 'Lakers'        | 0       |
+      | 'Jason Kidd'            | 'Mavericks'     | 0       |
+      | 'Jason Kidd'            | 'Suns'          | 0       |
+      | 'Jason Kidd'            | 'Nets'          | 0       |
+      | 'Jason Kidd'            | 'Mavericks'     | 1       |
+      | 'Jason Kidd'            | 'Knicks'        | 0       |
+      | 'Dirk Nowitzki'         | 'Mavericks'     | 0       |
+      | 'Paul George'           | 'Pacers'        | 0       |
+      | 'Paul George'           | 'Thunders'      | 0       |
+      | 'Grant Hill'            | 'Pistons'       | 0       |
+      | 'Grant Hill'            | 'Magic'         | 0       |
+      | 'Grant Hill'            | 'Suns'          | 0       |
+      | 'Grant Hill'            | 'Clippers'      | 0       |
+      | "Shaquile O'Neal"       | 'Magic'         | 0       |
+      | "Shaquile O'Neal"       | 'Lakers'        | 0       |
+      | "Shaquile O'Neal"       | 'Heat'          | 0       |
+      | "Shaquile O'Neal"       | 'Suns'          | 0       |
+      | "Shaquile O'Neal"       | 'Cavaliers'     | 0       |
+      | "Shaquile O'Neal"       | 'Celtics'       | 0       |
+      | 'JaVale McGee'          | 'Wizards'       | 0       |
+      | 'JaVale McGee'          | 'Nuggets'       | 0       |
+      | 'JaVale McGee'          | 'Mavericks'     | 0       |
+      | 'JaVale McGee'          | 'Warriors'      | 0       |
+      | 'JaVale McGee'          | 'Lakers'        | 0       |
+      | 'Dwight Howard'         | 'Magic'         | 0       |
+      | 'Dwight Howard'         | 'Lakers'        | 0       |
+      | 'Dwight Howard'         | 'Rockets'       | 0       |
+      | 'Dwight Howard'         | 'Hawks'         | 0       |
+      | 'Dwight Howard'         | 'Hornets'       | 0       |
+      | 'Dwight Howard'         | 'Wizards'       | 0       |
+    When executing query:
+      """
+      LOOKUP ON serve YIELD serve.start_year AS startYear
+      """
+    Then the result should be, in any order:
+      | SrcVID                  | DstVID          | Ranking | startYear |
+      | "Amar'e Stoudemire"     | 'Suns'          | 0       | 2002      |
+      | "Amar'e Stoudemire"     | 'Knicks'        | 0       | 2010      |
+      | "Amar'e Stoudemire"     | 'Heat'          | 0       | 2015      |
+      | 'Russell Westbrook'     | 'Thunders'      | 0       | 2008      |
+      | 'James Harden'          | 'Thunders'      | 0       | 2009      |
+      | 'James Harden'          | 'Rockets'       | 0       | 2012      |
+      | 'Kobe Bryant'           | 'Lakers'        | 0       | 1996      |
+      | 'Tracy McGrady'         | 'Raptors'       | 0       | 1997      |
+      | 'Tracy McGrady'         | 'Magic'         | 0       | 2000      |
+      | 'Tracy McGrady'         | 'Rockets'       | 0       | 2004      |
+      | 'Tracy McGrady'         | 'Spurs'         | 0       | 2013      |
+      | 'Chris Paul'            | 'Hornets'       | 0       | 2005      |
+      | 'Chris Paul'            | 'Clippers'      | 0       | 2011      |
+      | 'Chris Paul'            | 'Rockets'       | 0       | 2017      |
+      | 'Boris Diaw'            | 'Hawks'         | 0       | 2003      |
+      | 'Boris Diaw'            | 'Suns'          | 0       | 2005      |
+      | 'Boris Diaw'            | 'Hornets'       | 0       | 2008      |
+      | 'Boris Diaw'            | 'Spurs'         | 0       | 2012      |
+      | 'Boris Diaw'            | 'Jazz'          | 0       | 2016      |
+      | 'LeBron James'          | 'Cavaliers'     | 0       | 2003      |
+      | 'LeBron James'          | 'Heat'          | 0       | 2010      |
+      | 'LeBron James'          | 'Cavaliers'     | 1       | 2014      |
+      | 'LeBron James'          | 'Lakers'        | 0       | 2018      |
+      | 'Klay Thompson'         | 'Warriors'      | 0       | 2011      |
+      | 'Kristaps Porzingis'    | 'Knicks'        | 0       | 2015      |
+      | 'Kristaps Porzingis'    | 'Mavericks'     | 0       | 2019      |
+      | 'Jonathon Simmons'      | 'Spurs'         | 0       | 2015      |
+      | 'Jonathon Simmons'      | 'Magic'         | 0       | 2017      |
+      | 'Jonathon Simmons'      | '76ers'         | 0       | 2019      |
+      | 'Marco Belinelli'       | 'Warriors'      | 0       | 2007      |
+      | 'Marco Belinelli'       | 'Raptors'       | 0       | 2009      |
+      | 'Marco Belinelli'       | 'Hornets'       | 0       | 2010      |
+      | 'Marco Belinelli'       | 'Bulls'         | 0       | 2012      |
+      | 'Marco Belinelli'       | 'Spurs'         | 0       | 2013      |
+      | 'Marco Belinelli'       | 'Kings'         | 0       | 2015      |
+      | 'Marco Belinelli'       | 'Hornets'       | 1       | 2016      |
+      | 'Marco Belinelli'       | 'Hawks'         | 0       | 2017      |
+      | 'Marco Belinelli'       | '76ers'         | 0       | 2018      |
+      | 'Marco Belinelli'       | 'Spurs'         | 1       | 2018      |
+      | 'Luka Doncic'           | 'Mavericks'     | 0       | 2018      |
+      | 'David West'            | 'Hornets'       | 0       | 2003      |
+      | 'David West'            | 'Pacers'        | 0       | 2011      |
+      | 'David West'            | 'Spurs'         | 0       | 2015      |
+      | 'David West'            | 'Warriors'      | 0       | 2016      |
+      | 'Tony Parker'           | 'Spurs'         | 0       | 1999      |
+      | 'Tony Parker'           | 'Hornets'       | 0       | 2018      |
+      | 'Danny Green'           | 'Cavaliers'     | 0       | 2009      |
+      | 'Danny Green'           | 'Spurs'         | 0       | 2010      |
+      | 'Danny Green'           | 'Raptors'       | 0       | 2018      |
+      | 'Rudy Gay'              | 'Grizzlies'     | 0       | 2006      |
+      | 'Rudy Gay'              | 'Raptors'       | 0       | 2013      |
+      | 'Rudy Gay'              | 'Kings'         | 0       | 2013      |
+      | 'Rudy Gay'              | 'Spurs'         | 0       | 2017      |
+      | 'LaMarcus Aldridge'     | 'Trail Blazers' | 0       | 2006      |
+      | 'LaMarcus Aldridge'     | 'Spurs'         | 0       | 2015      |
+      | 'Tim Duncan'            | 'Spurs'         | 0       | 1997      |
+      | 'Kevin Durant'          | 'Thunders'      | 0       | 2007      |
+      | 'Kevin Durant'          | 'Warriors'      | 0       | 2016      |
+      | 'Stephen Curry'         | 'Warriors'      | 0       | 2009      |
+      | 'Ray Allen'             | 'Bucks'         | 0       | 1996      |
+      | 'Ray Allen'             | 'Thunders'      | 0       | 2003      |
+      | 'Ray Allen'             | 'Celtics'       | 0       | 2007      |
+      | 'Ray Allen'             | 'Heat'          | 0       | 2012      |
+      | 'Tiago Splitter'        | 'Spurs'         | 0       | 2010      |
+      | 'Tiago Splitter'        | 'Hawks'         | 0       | 2015      |
+      | 'Tiago Splitter'        | '76ers'         | 0       | 2017      |
+      | 'DeAndre Jordan'        | 'Clippers'      | 0       | 2008      |
+      | 'DeAndre Jordan'        | 'Mavericks'     | 0       | 2018      |
+      | 'DeAndre Jordan'        | 'Knicks'        | 0       | 2019      |
+      | 'Paul Gasol'            | 'Grizzlies'     | 0       | 2001      |
+      | 'Paul Gasol'            | 'Lakers'        | 0       | 2008      |
+      | 'Paul Gasol'            | 'Bulls'         | 0       | 2014      |
+      | 'Paul Gasol'            | 'Spurs'         | 0       | 2016      |
+      | 'Paul Gasol'            | 'Bucks'         | 0       | 2019      |
+      | 'Aron Baynes'           | 'Spurs'         | 0       | 2013      |
+      | 'Aron Baynes'           | 'Pistons'       | 0       | 2015      |
+      | 'Aron Baynes'           | 'Celtics'       | 0       | 2017      |
+      | 'Cory Joseph'           | 'Spurs'         | 0       | 2011      |
+      | 'Cory Joseph'           | 'Raptors'       | 0       | 2015      |
+      | 'Cory Joseph'           | 'Pacers'        | 0       | 2017      |
+      | 'Vince Carter'          | 'Raptors'       | 0       | 1998      |
+      | 'Vince Carter'          | 'Nets'          | 0       | 2004      |
+      | 'Vince Carter'          | 'Magic'         | 0       | 2009      |
+      | 'Vince Carter'          | 'Suns'          | 0       | 2010      |
+      | 'Vince Carter'          | 'Mavericks'     | 0       | 2011      |
+      | 'Vince Carter'          | 'Grizzlies'     | 0       | 2014      |
+      | 'Vince Carter'          | 'Kings'         | 0       | 2017      |
+      | 'Vince Carter'          | 'Hawks'         | 0       | 2018      |
+      | 'Marc Gasol'            | 'Grizzlies'     | 0       | 2008      |
+      | 'Marc Gasol'            | 'Raptors'       | 0       | 2019      |
+      | 'Ricky Rubio'           | 'Timberwolves'  | 0       | 2011      |
+      | 'Ricky Rubio'           | 'Jazz'          | 0       | 2017      |
+      | 'Ben Simmons'           | '76ers'         | 0       | 2016      |
+      | 'Giannis Antetokounmpo' | 'Bucks'         | 0       | 2013      |
+      | 'Rajon Rondo'           | 'Celtics'       | 0       | 2006      |
+      | 'Rajon Rondo'           | 'Mavericks'     | 0       | 2014      |
+      | 'Rajon Rondo'           | 'Kings'         | 0       | 2015      |
+      | 'Rajon Rondo'           | 'Bulls'         | 0       | 2016      |
+      | 'Rajon Rondo'           | 'Pelicans'      | 0       | 2017      |
+      | 'Rajon Rondo'           | 'Lakers'        | 0       | 2018      |
+      | 'Manu Ginobili'         | 'Spurs'         | 0       | 2002      |
+      | 'Kyrie Irving'          | 'Cavaliers'     | 0       | 2011      |
+      | 'Kyrie Irving'          | 'Celtics'       | 0       | 2017      |
+      | 'Carmelo Anthony'       | 'Nuggets'       | 0       | 2003      |
+      | 'Carmelo Anthony'       | 'Knicks'        | 0       | 2011      |
+      | 'Carmelo Anthony'       | 'Thunders'      | 0       | 2017      |
+      | 'Carmelo Anthony'       | 'Rockets'       | 0       | 2018      |
+      | 'Dwyane Wade'           | 'Heat'          | 0       | 2003      |
+      | 'Dwyane Wade'           | 'Bulls'         | 0       | 2016      |
+      | 'Dwyane Wade'           | 'Cavaliers'     | 0       | 2017      |
+      | 'Dwyane Wade'           | 'Heat'          | 1       | 2018      |
+      | 'Joel Embiid'           | '76ers'         | 0       | 2014      |
+      | 'Damian Lillard'        | 'Trail Blazers' | 0       | 2012      |
+      | 'Yao Ming'              | 'Rockets'       | 0       | 2002      |
+      | 'Kyle Anderson'         | 'Spurs'         | 0       | 2014      |
+      | 'Kyle Anderson'         | 'Grizzlies'     | 0       | 2018      |
+      | 'Dejounte Murray'       | 'Spurs'         | 0       | 2016      |
+      | 'Blake Griffin'         | 'Clippers'      | 0       | 2009      |
+      | 'Blake Griffin'         | 'Pistons'       | 0       | 2018      |
+      | 'Steve Nash'            | 'Suns'          | 0       | 1996      |
+      | 'Steve Nash'            | 'Mavericks'     | 0       | 1998      |
+      | 'Steve Nash'            | 'Suns'          | 1       | 2004      |
+      | 'Steve Nash'            | 'Lakers'        | 0       | 2012      |
+      | 'Jason Kidd'            | 'Mavericks'     | 0       | 1994      |
+      | 'Jason Kidd'            | 'Suns'          | 0       | 1996      |
+      | 'Jason Kidd'            | 'Nets'          | 0       | 2001      |
+      | 'Jason Kidd'            | 'Mavericks'     | 1       | 2008      |
+      | 'Jason Kidd'            | 'Knicks'        | 0       | 2012      |
+      | 'Dirk Nowitzki'         | 'Mavericks'     | 0       | 1998      |
+      | 'Paul George'           | 'Pacers'        | 0       | 2010      |
+      | 'Paul George'           | 'Thunders'      | 0       | 2017      |
+      | 'Grant Hill'            | 'Pistons'       | 0       | 1994      |
+      | 'Grant Hill'            | 'Magic'         | 0       | 2000      |
+      | 'Grant Hill'            | 'Suns'          | 0       | 2007      |
+      | 'Grant Hill'            | 'Clippers'      | 0       | 2012      |
+      | "Shaquile O'Neal"       | 'Magic'         | 0       | 1992      |
+      | "Shaquile O'Neal"       | 'Lakers'        | 0       | 1996      |
+      | "Shaquile O'Neal"       | 'Heat'          | 0       | 2004      |
+      | "Shaquile O'Neal"       | 'Suns'          | 0       | 2008      |
+      | "Shaquile O'Neal"       | 'Cavaliers'     | 0       | 2009      |
+      | "Shaquile O'Neal"       | 'Celtics'       | 0       | 2010      |
+      | 'JaVale McGee'          | 'Wizards'       | 0       | 2008      |
+      | 'JaVale McGee'          | 'Nuggets'       | 0       | 2012      |
+      | 'JaVale McGee'          | 'Mavericks'     | 0       | 2015      |
+      | 'JaVale McGee'          | 'Warriors'      | 0       | 2016      |
+      | 'JaVale McGee'          | 'Lakers'        | 0       | 2018      |
+      | 'Dwight Howard'         | 'Magic'         | 0       | 2004      |
+      | 'Dwight Howard'         | 'Lakers'        | 0       | 2012      |
+      | 'Dwight Howard'         | 'Rockets'       | 0       | 2013      |
+      | 'Dwight Howard'         | 'Hawks'         | 0       | 2016      |
+      | 'Dwight Howard'         | 'Hornets'       | 0       | 2017      |
+      | 'Dwight Howard'         | 'Wizards'       | 0       | 2018      |
diff --git a/tests/tck/features/lookup/Output.feature b/tests/tck/features/lookup/Output.feature
new file mode 100644
index 0000000000000000000000000000000000000000..838209ea80eaebe0404f342d25abb42207d5bf50
--- /dev/null
+++ b/tests/tck/features/lookup/Output.feature
@@ -0,0 +1,96 @@
+Feature: Lookup with output
+
+  Background: Prepare space
+    Given a graph with space named "nba"
+
+  Scenario: [1] tag output
+    When executing query:
+      """
+      LOOKUP ON player WHERE player.age == 40 |
+      FETCH PROP ON player $-.VertexID YIELD player.name
+      """
+    Then the result should be, in any order:
+      | VertexID        | player.name     |
+      | 'Kobe Bryant'   | 'Kobe Bryant'   |
+      | 'Dirk Nowitzki' | 'Dirk Nowitzki' |
+
+  Scenario: [1] tag ouput with yield rename
+    When executing query:
+      """
+      LOOKUP ON player WHERE player.age == 40 YIELD player.name AS name |
+      FETCH PROP ON player $-.name YIELD player.name AS name
+      """
+    Then the result should be, in any order:
+      | VertexID        | name            |
+      | 'Kobe Bryant'   | 'Kobe Bryant'   |
+      | 'Dirk Nowitzki' | 'Dirk Nowitzki' |
+
+  Scenario: [1] tag output by var
+    When executing query:
+      """
+      $a = LOOKUP ON player WHERE player.age == 40;
+      FETCH PROP ON player $a.VertexID YIELD player.name
+      """
+    Then the result should be, in any order:
+      | VertexID        | player.name     |
+      | 'Kobe Bryant'   | 'Kobe Bryant'   |
+      | 'Dirk Nowitzki' | 'Dirk Nowitzki' |
+
+  Scenario: [1] tag ouput with yield rename by var
+    When executing query:
+      """
+      $a = LOOKUP ON player WHERE player.age == 40 YIELD player.name AS name;
+      FETCH PROP ON player $a.name YIELD player.name AS name
+      """
+    Then the result should be, in any order:
+      | VertexID        | name            |
+      | 'Kobe Bryant'   | 'Kobe Bryant'   |
+      | 'Dirk Nowitzki' | 'Dirk Nowitzki' |
+
+  Scenario: [2] edge output
+    When executing query:
+      """
+      LOOKUP ON serve WHERE serve.start_year == 2008 and serve.end_year == 2019
+      YIELD serve.start_year |
+      FETCH PROP ON serve $-.SrcVID->$-.DstVID YIELD serve.start_year
+      """
+    Then the result should be, in any order:
+      | serve._src          | serve._dst  | serve._rank | serve.start_year |
+      | 'Russell Westbrook' | 'Thunders'  | 0           | 2008             |
+      | 'Marc Gasol'        | 'Grizzlies' | 0           | 2008             |
+
+  Scenario: [2] edge output with yield rename
+    When executing query:
+      """
+      LOOKUP ON serve WHERE serve.start_year == 2008 and serve.end_year == 2019
+      YIELD serve.start_year AS startYear |
+      FETCH PROP ON serve $-.SrcVID->$-.DstVID YIELD serve.start_year AS startYear
+      """
+    Then the result should be, in any order:
+      | serve._src          | serve._dst  | serve._rank | startYear |
+      | 'Russell Westbrook' | 'Thunders'  | 0           | 2008      |
+      | 'Marc Gasol'        | 'Grizzlies' | 0           | 2008      |
+
+  Scenario: [2] edge output by var
+    When executing query:
+      """
+      $a = LOOKUP ON serve WHERE serve.start_year == 2008 and serve.end_year == 2019
+      YIELD serve.start_year;
+      FETCH PROP ON serve $a.SrcVID->$a.DstVID YIELD serve.start_year
+      """
+    Then the result should be, in any order:
+      | serve._src          | serve._dst  | serve._rank | serve.start_year |
+      | 'Russell Westbrook' | 'Thunders'  | 0           | 2008             |
+      | 'Marc Gasol'        | 'Grizzlies' | 0           | 2008             |
+
+  Scenario: [2] edge output with yield rename by var
+    When executing query:
+      """
+      $a = LOOKUP ON serve WHERE serve.start_year == 2008 and serve.end_year == 2019
+      YIELD serve.start_year AS startYear;
+      FETCH PROP ON serve $a.SrcVID->$a.DstVID YIELD serve.start_year AS startYear
+      """
+    Then the result should be, in any order:
+      | serve._src          | serve._dst  | serve._rank | startYear |
+      | 'Russell Westbrook' | 'Thunders'  | 0           | 2008      |
+      | 'Marc Gasol'        | 'Grizzlies' | 0           | 2008      |
diff --git a/tests/tck/features/lookup/WithYield.feature b/tests/tck/features/lookup/WithYield.feature
new file mode 100644
index 0000000000000000000000000000000000000000..9aedd4ebe2750a9dc7cecb47c8ac8cb6d13c2635
--- /dev/null
+++ b/tests/tck/features/lookup/WithYield.feature
@@ -0,0 +1,46 @@
+Feature: Lookup with yield
+
+  Background: Prepare space
+    Given a graph with space named "nba"
+
+  Scenario: [1] tag with yield
+    When executing query:
+      """
+      LOOKUP ON player WHERE player.age == 40 YIELD player.name
+      """
+    Then the result should be, in any order:
+      | VertexID        | player.name     |
+      | 'Kobe Bryant'   | 'Kobe Bryant'   |
+      | 'Dirk Nowitzki' | 'Dirk Nowitzki' |
+
+  Scenario: [1] tag with yield rename
+    When executing query:
+      """
+      LOOKUP ON player WHERE player.age == 40 YIELD player.name AS name
+      """
+    Then the result should be, in any order:
+      | VertexID        | name            |
+      | 'Kobe Bryant'   | 'Kobe Bryant'   |
+      | 'Dirk Nowitzki' | 'Dirk Nowitzki' |
+
+  Scenario: [2] edge with yield
+    When executing query:
+      """
+      LOOKUP ON serve WHERE serve.start_year == 2008 and serve.end_year == 2019
+      YIELD serve.start_year
+      """
+    Then the result should be, in any order:
+      | SrcVID              | DstVID      | Ranking | serve.start_year |
+      | 'Russell Westbrook' | 'Thunders'  | 0       | 2008             |
+      | 'Marc Gasol'        | 'Grizzlies' | 0       | 2008             |
+
+  Scenario: [2] edge with yield rename
+    When executing query:
+      """
+      LOOKUP ON serve WHERE serve.start_year == 2008 and serve.end_year == 2019
+      YIELD serve.start_year AS startYear
+      """
+    Then the result should be, in any order:
+      | SrcVID              | DstVID      | Ranking | startYear |
+      | 'Russell Westbrook' | 'Thunders'  | 0       | 2008      |
+      | 'Marc Gasol'        | 'Grizzlies' | 0       | 2008      |