This is an automated email from the ASF dual-hosted git repository.

twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 9598a26dd feat(search): support to skip expired keys in query (#3086)
9598a26dd is described below

commit 9598a26ddc439911927b482431840487eaead853
Author: Twice <[email protected]>
AuthorDate: Fri Aug 1 20:07:52 2025 +0800

    feat(search): support to skip expired keys in query (#3086)
---
 src/search/executors/filter_executor.h  |  3 +++
 src/search/executors/topn_executor.h    |  3 +++
 src/search/indexer.cc                   |  4 ++--
 src/search/plan_executor.cc             | 15 +++++++++++++--
 tests/gocase/unit/search/search_test.go | 27 +++++++++++++++++++++++++++
 5 files changed, 48 insertions(+), 4 deletions(-)

diff --git a/src/search/executors/filter_executor.h 
b/src/search/executors/filter_executor.h
index 39b6c56cd..dfce82f2e 100644
--- a/src/search/executors/filter_executor.h
+++ b/src/search/executors/filter_executor.h
@@ -78,6 +78,7 @@ struct QueryExprEvaluator {
 
   StatusOr<bool> Visit(TagContainExpr *v) const {
     auto val = GET_OR_RET(ctx->Retrieve(ctx->db_ctx, row, v->field->info));
+    if (val.IsNull()) return false;
 
     CHECK(val.Is<kqir::StringArray>());
     auto tags = val.Get<kqir::StringArray>();
@@ -93,6 +94,7 @@ struct QueryExprEvaluator {
 
   StatusOr<bool> Visit(NumericCompareExpr *v) const {
     auto l_val = GET_OR_RET(ctx->Retrieve(ctx->db_ctx, row, v->field->info));
+    if (l_val.IsNull()) return false;
 
     CHECK(l_val.Is<kqir::Numeric>());
     auto l = l_val.Get<kqir::Numeric>();
@@ -118,6 +120,7 @@ struct QueryExprEvaluator {
 
   StatusOr<bool> Visit(VectorRangeExpr *v) const {
     auto val = GET_OR_RET(ctx->Retrieve(ctx->db_ctx, row, v->field->info));
+    if (val.IsNull()) return false;
 
     CHECK(val.Is<kqir::NumericArray>());
     auto l_values = val.Get<kqir::NumericArray>();
diff --git a/src/search/executors/topn_executor.h 
b/src/search/executors/topn_executor.h
index 7eade082e..226045eca 100644
--- a/src/search/executors/topn_executor.h
+++ b/src/search/executors/topn_executor.h
@@ -58,6 +58,9 @@ struct TopNExecutor : ExecutorNode {
 
         auto get_order = [this](RowType &row) -> StatusOr<double> {
           auto order_val = GET_OR_RET(ctx->Retrieve(ctx->db_ctx, row, 
topn->order->field->info));
+          // TODO(twice): here we return NaN if this field is not found,
+          // but we should consider to just skip this row instead.
+          if (order_val.IsNull()) return std::nan("");
           CHECK(order_val.Is<kqir::Numeric>());
           return order_val.Get<kqir::Numeric>();
         };
diff --git a/src/search/indexer.cc b/src/search/indexer.cc
index 84b5a6547..e7089e54c 100644
--- a/src/search/indexer.cc
+++ b/src/search/indexer.cc
@@ -44,7 +44,7 @@ StatusOr<FieldValueRetriever> 
FieldValueRetriever::Create(IndexOnDataType type,
     HashMetadata metadata(false);
 
     auto s = db.GetMetadata(ctx, ns_key, &metadata);
-    if (!s.ok()) return {Status::NotOK, s.ToString()};
+    if (!s.ok()) return {s.IsNotFound() ? Status::NotFound : Status::NotOK, 
s.ToString()};
     return FieldValueRetriever(db, metadata, key);
   } else if (type == IndexOnDataType::JSON) {
     Json db(storage, ns);
@@ -52,7 +52,7 @@ StatusOr<FieldValueRetriever> 
FieldValueRetriever::Create(IndexOnDataType type,
     JsonMetadata metadata(false);
     JsonValue value;
     auto s = db.read(ctx, ns_key, &metadata, &value);
-    if (!s.ok()) return {Status::NotOK, s.ToString()};
+    if (!s.ok()) return {s.IsNotFound() ? Status::NotFound : Status::NotOK, 
s.ToString()};
     return FieldValueRetriever(value);
   } else {
     unreachable();
diff --git a/src/search/plan_executor.cc b/src/search/plan_executor.cc
index 6fdd375eb..afb2de03f 100644
--- a/src/search/plan_executor.cc
+++ b/src/search/plan_executor.cc
@@ -37,6 +37,7 @@
 #include "search/executors/topn_executor.h"
 #include "search/indexer.h"
 #include "search/ir_plan.h"
+#include "status.h"
 
 namespace kqir {
 
@@ -167,10 +168,20 @@ auto ExecutorContext::Retrieve(engine::Context &ctx, 
RowType &row, const FieldIn
     return iter->second;
   }
 
-  auto retriever = GET_OR_RET(
-      redis::FieldValueRetriever::Create(field->index->metadata.on_data_type, 
row.key, storage, field->index->ns));
+  auto s_retriever =
+      redis::FieldValueRetriever::Create(field->index->metadata.on_data_type, 
row.key, storage, field->index->ns);
+
+  if (s_retriever.Is<Status::NotFound>()) {
+    row.fields.emplace(field, kqir::Null{});
+    return kqir::Null{};
+  }
+  auto retriever = GET_OR_RET(std::move(s_retriever));
 
   auto s = retriever.Retrieve(ctx, field->name, field->metadata.get());
+  if (s.Is<Status::NotFound>()) {
+    row.fields.emplace(field, kqir::Null{});
+    return kqir::Null{};
+  }
   if (!s) return s;
 
   row.fields.emplace(field, *s);
diff --git a/tests/gocase/unit/search/search_test.go 
b/tests/gocase/unit/search/search_test.go
index 615aba513..241cd966f 100644
--- a/tests/gocase/unit/search/search_test.go
+++ b/tests/gocase/unit/search/search_test.go
@@ -24,6 +24,7 @@ import (
        "context"
        "encoding/binary"
        "testing"
+       "time"
 
        "github.com/apache/kvrocks/tests/gocase/util"
        "github.com/redis/go-redis/v9"
@@ -205,4 +206,30 @@ func TestSearch(t *testing.T) {
                srv.Restart()
                verify(t)
        })
+
+       t.Run("FT.SEARCH with expired keys", func(t *testing.T) {
+               require.NoError(t, rdb.Do(ctx, "FT.CREATE", "testidx_expired", 
"ON", "HASH", "PREFIX", "1", "test_expired:", "SCHEMA", "a", "TAG", "b", 
"NUMERIC").Err())
+               require.NoError(t, rdb.Do(ctx, "HSET", "test_expired:k1", "a", 
"x,y", "b", "11").Err())
+               require.NoError(t, rdb.Do(ctx, "HSET", "test_expired:k2", "a", 
"x,z", "b", "22").Err())
+               require.NoError(t, rdb.Do(ctx, "HSET", "test_expired:k3", "a", 
"y,z", "b", "33").Err())
+               require.NoError(t, rdb.Do(ctx, "HSET", "test_expired:k4", "a", 
"x,y,z", "b", "44").Err())
+
+               res := rdb.Do(ctx, "FT.SEARCHSQL", "select * from 
testidx_expired where a hastag \"z\" and b < 40")
+               require.NoError(t, res.Err())
+               // result should be [2 test_expired:k2 [a x,z b 22] 
test_expired:k3 [a y,z b 33]]
+               require.Equal(t, 5, len(res.Val().([]interface{})))
+               require.Equal(t, int64(2), res.Val().([]interface{})[0])
+               require.Equal(t, "test_expired:k2", 
res.Val().([]interface{})[1])
+               require.Equal(t, "test_expired:k3", 
res.Val().([]interface{})[3])
+
+               require.NoError(t, rdb.Do(ctx, "EXPIRE", "test_expired:k2", 
1).Err())
+               time.Sleep(time.Millisecond * 1500) // wait for the key to 
expire
+
+               res = rdb.Do(ctx, "FT.SEARCHSQL", "select * from 
testidx_expired where a hastag \"z\" and b < 40")
+               require.NoError(t, res.Err())
+               // result should be [1 test_expired:k3 [a y,z b 33]]
+               require.Equal(t, 3, len(res.Val().([]interface{})))
+               require.Equal(t, int64(1), res.Val().([]interface{})[0])
+               require.Equal(t, "test_expired:k3", 
res.Val().([]interface{})[1])
+       })
 }

Reply via email to