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

robocanic pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-admin.git


The following commit(s) were added to refs/heads/develop by this push:
     new b809b14c feat: extend indexer with prefix matching and db persistence 
(#1422)
b809b14c is described below

commit b809b14cea8c163c5ec83bb387b132f7b561e522
Author: ThunGuo <[email protected]>
AuthorDate: Sun Mar 29 19:27:19 2026 +0800

    feat: extend indexer with prefix matching and db persistence (#1422)
    
    * feat: extend indexer with prefix matching and db persistence
    
    * refactor: remove in-memory index from GormStore and add operator field
    
    * fix: copilot review suggestion and service panic
    
    * update
---
 go.mod                                            |   1 +
 go.sum                                            |   1 +
 pkg/console/service/application.go                |  51 +--
 pkg/console/service/condition_rule.go             |   4 +-
 pkg/console/service/configurator_rule.go          |   4 +-
 pkg/console/service/instance.go                   |  28 +-
 pkg/console/service/service.go                    |  32 +-
 pkg/console/service/service_provider_instances.go |   6 +-
 pkg/console/service/tag_rule.go                   |   4 +-
 pkg/core/bootstrap/init.go                        |   1 +
 pkg/core/discovery/subscriber/instance.go         |   6 +-
 pkg/core/discovery/subscriber/nacos_service.go    |  24 +-
 pkg/core/discovery/subscriber/rpc_instance.go     |   4 +-
 pkg/core/engine/subscriber/runtime_instance.go    |   8 +-
 pkg/core/manager/manager.go                       |  21 +-
 pkg/core/manager/manager_helper.go                |  35 +-
 pkg/core/store/index/condition.go                 |  38 ++
 pkg/core/store/store.go                           |   9 +-
 pkg/governor/mock/factory.go                      |  50 +++
 pkg/store/dbcommon/gorm_store.go                  | 383 +++++++++++++------
 pkg/store/dbcommon/gorm_store_test.go             | 377 +++++++++++++++----
 pkg/store/dbcommon/index.go                       | 254 -------------
 pkg/store/dbcommon/model.go                       |  19 +
 pkg/store/memory/store.go                         | 185 ++++++++-
 pkg/store/memory/store_test.go                    | 433 +++++++++++++++++++++-
 25 files changed, 1391 insertions(+), 587 deletions(-)

diff --git a/go.mod b/go.mod
index 7548c170..c8d33c25 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ toolchain go1.24.11
 require (
        dubbo.apache.org/dubbo-go/v3 v3.0.0-20260210015753-35ea886421f9
        github.com/apache/dubbo-go-hessian2 v1.12.5
+       github.com/armon/go-radix v1.0.0
        github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
        github.com/duke-git/lancet/v2 v2.3.6
        github.com/envoyproxy/go-control-plane/envoy v1.32.4
diff --git a/go.sum b/go.sum
index b288f3a0..414edca8 100644
--- a/go.sum
+++ b/go.sum
@@ -151,6 +151,7 @@ github.com/apolloconfig/agollo/v4 v4.4.0/go.mod 
h1:6WjI68IzqMk/Y6ghMtrj5AX6Uewo2
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod 
h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod 
h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod 
h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0 
h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
 github.com/armon/go-radix v1.0.0/go.mod 
h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod 
h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
 github.com/aws/aws-lambda-go v1.13.3/go.mod 
h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
diff --git a/pkg/console/service/application.go 
b/pkg/console/service/application.go
index 80e7ee68..cd295e2b 100644
--- a/pkg/console/service/application.go
+++ b/pkg/console/service/application.go
@@ -42,9 +42,9 @@ func GetApplicationDetail(ctx consolectx.Context, req 
*model.ApplicationDetailRe
        instanceResources, err := 
manager.ListByIndexes[*meshresource.InstanceResource](
                ctx.ResourceManager(),
                meshresource.InstanceKind,
-               map[string]string{
-                       index.ByMeshIndex:            req.Mesh,
-                       index.ByInstanceAppNameIndex: req.AppName,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByInstanceAppNameIndex, Value: 
req.AppName, Operator: index.Equals},
                },
        )
        if err != nil {
@@ -68,9 +68,9 @@ func GetAppInstanceInfo(ctx consolectx.Context, req 
*model.ApplicationTabInstanc
        pageData, err := 
manager.PageListByIndexes[*meshresource.InstanceResource](
                ctx.ResourceManager(),
                meshresource.InstanceKind,
-               map[string]string{
-                       index.ByMeshIndex:            req.Mesh,
-                       index.ByInstanceAppNameIndex: req.AppName,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByInstanceAppNameIndex, Value: 
req.AppName, Operator: index.Equals},
                },
                req.PageReq,
        )
@@ -120,23 +120,28 @@ func GetAppServiceInfo(ctx consolectx.Context, req 
*model.ApplicationServiceForm
 }
 
 func getAppProvideServiceInfo(ctx consolectx.Context, req 
*model.ApplicationServiceFormReq) (*model.SearchPaginationResult, error) {
-       var indexes map[string]string
+       var conditions []index.IndexCondition
+       conditions = append(conditions, index.IndexCondition{
+               IndexName: index.ByMeshIndex,
+               Value:     req.Mesh,
+               Operator:  index.Equals,
+       })
+       conditions = append(conditions, index.IndexCondition{
+               IndexName: index.ByServiceProviderAppName,
+               Value:     req.AppName,
+               Operator:  index.Equals,
+       })
        if strutil.IsNotBlank(req.ServiceName) {
-               indexes = map[string]string{
-                       index.ByMeshIndex:                  req.Mesh,
-                       index.ByServiceProviderAppName:     req.AppName,
-                       index.ByServiceProviderServiceName: req.ServiceName,
-               }
-       } else {
-               indexes = map[string]string{
-                       index.ByMeshIndex:              req.Mesh,
-                       index.ByServiceProviderAppName: req.AppName,
-               }
+               conditions = append(conditions, index.IndexCondition{
+                       IndexName: index.ByServiceProviderServiceName,
+                       Value:     req.ServiceName,
+                       Operator:  index.Equals,
+               })
        }
        pageData, err := 
manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource](
                ctx.ResourceManager(),
                meshresource.ServiceProviderMetadataKind,
-               indexes,
+               conditions,
                req.PageReq,
        )
        if err != nil {
@@ -167,9 +172,9 @@ func getAppConsumeServiceInfo(ctx consolectx.Context, req 
*model.ApplicationServ
        pageData, err := 
manager.PageListByIndexes[*meshresource.ServiceConsumerMetadataResource](
                ctx.ResourceManager(),
                meshresource.ServiceConsumerMetadataKind,
-               map[string]string{
-                       index.ByMeshIndex:              req.Mesh,
-                       index.ByServiceConsumerAppName: req.AppName,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByServiceConsumerAppName, Value: 
req.AppName, Operator: index.Equals},
                },
                req.PageReq,
        )
@@ -220,8 +225,8 @@ func SearchApplications(ctx consolectx.Context, req 
*model.ApplicationSearchReq)
        pageData, err := 
manager.PageListByIndexes[*meshresource.ApplicationResource](
                ctx.ResourceManager(),
                meshresource.ApplicationKind,
-               map[string]string{
-                       index.ByMeshIndex: req.Mesh,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
                },
                req.PageReq,
        )
diff --git a/pkg/console/service/condition_rule.go 
b/pkg/console/service/condition_rule.go
index de0ebfc8..9fae1594 100644
--- a/pkg/console/service/condition_rule.go
+++ b/pkg/console/service/condition_rule.go
@@ -40,8 +40,8 @@ func SearchConditionRules(ctx context.Context, req 
*model.SearchConditionRuleReq
        pageData, err := 
manager.PageListByIndexes[*meshresource.ConditionRouteResource](
                ctx.ResourceManager(),
                meshresource.ConditionRouteKind,
-               map[string]string{
-                       index.ByMeshIndex: req.Mesh,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
                },
                req.PageReq)
        if err != nil {
diff --git a/pkg/console/service/configurator_rule.go 
b/pkg/console/service/configurator_rule.go
index 16aa59e4..13dd2284 100644
--- a/pkg/console/service/configurator_rule.go
+++ b/pkg/console/service/configurator_rule.go
@@ -36,8 +36,8 @@ func PageListConfiguratorRule(ctx consolectx.Context, req 
*model.SearchReq) (*mo
        pageData, err := 
manager.PageListByIndexes[*meshresource.DynamicConfigResource](
                ctx.ResourceManager(),
                meshresource.DynamicConfigKind,
-               map[string]string{
-                       index.ByMeshIndex: req.Mesh,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
                },
                req.PageReq)
        if err != nil {
diff --git a/pkg/console/service/instance.go b/pkg/console/service/instance.go
index ed0f822d..85a04eb2 100644
--- a/pkg/console/service/instance.go
+++ b/pkg/console/service/instance.go
@@ -44,9 +44,9 @@ func SearchInstanceByIp(ctx consolectx.Context, req 
*model.SearchReq) (*model.Se
        pageData, err := 
manager.PageListByIndexes[*meshresource.InstanceResource](
                ctx.ResourceManager(),
                meshresource.InstanceKind,
-               map[string]string{
-                       index.ByMeshIndex:       req.Mesh,
-                       index.ByInstanceIpIndex: req.Keywords,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByInstanceIpIndex, Value: 
req.Keywords, Operator: index.HasPrefix},
                },
                req.PageReq)
        if err != nil {
@@ -54,7 +54,7 @@ func SearchInstanceByIp(ctx consolectx.Context, req 
*model.SearchReq) (*model.Se
        }
        if pageData.Data == nil || len(pageData.Data) == 0 {
                return &model.SearchPaginationResult{
-                       List: 
[]*meshresource.ServiceProviderMetadataResourceList{},
+                       List: []*model.SearchInstanceResp{},
                        PageInfo: coremodel.Pagination{
                                Total:      0,
                                PageSize:   req.PageReq.PageSize,
@@ -63,8 +63,8 @@ func SearchInstanceByIp(ctx consolectx.Context, req 
*model.SearchReq) (*model.Se
                }, nil
        }
        return &model.SearchPaginationResult{
-               List: slice.Map(pageData.Data, func(_ int, item 
*meshresource.InstanceResource) *model.AppInstanceInfoResp {
-                       return buildAppInstanceInfoResp(item, ctx.Config())
+               List: slice.Map(pageData.Data, func(_ int, item 
*meshresource.InstanceResource) *model.SearchInstanceResp {
+                       return 
model.NewSearchInstanceResp().FromInstanceResource(item, ctx.Config())
                }),
                PageInfo: pageData.Pagination,
        }, nil
@@ -75,9 +75,9 @@ func SearchInstanceByName(ctx consolectx.Context, req 
*model.SearchReq) (*model.
        pageData, err := 
manager.PageListByIndexes[*meshresource.InstanceResource](
                ctx.ResourceManager(),
                meshresource.InstanceKind,
-               map[string]string{
-                       index.ByMeshIndex:         req.Mesh,
-                       index.ByInstanceNameIndex: req.Keywords,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByInstanceNameIndex, Value: 
req.Keywords, Operator: index.HasPrefix},
                },
                req.PageReq)
        if err != nil {
@@ -85,7 +85,7 @@ func SearchInstanceByName(ctx consolectx.Context, req 
*model.SearchReq) (*model.
        }
        if pageData.Data == nil || len(pageData.Data) == 0 {
                return &model.SearchPaginationResult{
-                       List: 
[]*meshresource.ServiceProviderMetadataResourceList{},
+                       List: []*model.SearchInstanceResp{},
                        PageInfo: coremodel.Pagination{
                                Total:      0,
                                PageSize:   req.PageReq.PageSize,
@@ -94,8 +94,8 @@ func SearchInstanceByName(ctx consolectx.Context, req 
*model.SearchReq) (*model.
                }, nil
        }
        return &model.SearchPaginationResult{
-               List: slice.Map(pageData.Data, func(_ int, item 
*meshresource.InstanceResource) *model.AppInstanceInfoResp {
-                       return buildAppInstanceInfoResp(item, ctx.Config())
+               List: slice.Map(pageData.Data, func(_ int, item 
*meshresource.InstanceResource) *model.SearchInstanceResp {
+                       return 
model.NewSearchInstanceResp().FromInstanceResource(item, ctx.Config())
                }),
                PageInfo: pageData.Pagination,
        }, nil
@@ -113,8 +113,8 @@ func SearchInstances(ctx consolectx.Context, req 
*model.SearchInstanceReq) (*mod
        pageData, err := 
manager.PageListByIndexes[*meshresource.InstanceResource](
                ctx.ResourceManager(),
                meshresource.InstanceKind,
-               map[string]string{
-                       index.ByMeshIndex: req.Mesh,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
                },
                req.PageReq)
        if err != nil {
diff --git a/pkg/console/service/service.go b/pkg/console/service/service.go
index 9217a606..0178fe2c 100644
--- a/pkg/console/service/service.go
+++ b/pkg/console/service/service.go
@@ -41,17 +41,21 @@ import (
 
 // GetServiceTabDistribution get service distribution
 func GetServiceTabDistribution(ctx consolectx.Context, req 
*model.ServiceTabDistributionReq) (*model.SearchPaginationResult, error) {
-       indexes := map[string]string{
-               index.ByServiceConsumerServiceName: req.ServiceName,
+       conditions := []index.IndexCondition{
+               {IndexName: index.ByServiceConsumerServiceName, Value: 
req.ServiceName, Operator: index.Equals},
        }
        // for now, only support accurate name match
        if strutil.IsNotBlank(req.Keywords) {
-               indexes[index.ByServiceConsumerAppName] = req.Keywords
+               conditions = append(conditions, index.IndexCondition{
+                       IndexName: index.ByServiceConsumerAppName,
+                       Value:     req.Keywords,
+                       Operator:  index.Equals,
+               })
        }
        pageData, err := 
manager.PageListByIndexes[*meshresource.ServiceConsumerMetadataResource](
                ctx.ResourceManager(),
                meshresource.ServiceConsumerMetadataKind,
-               indexes,
+               conditions,
                req.PageReq)
        if err != nil {
                logger.Errorf("get service consumer %s failed, cause: %v", 
req.ServiceName, err)
@@ -98,8 +102,8 @@ func SearchServices(ctx consolectx.Context, req 
*model.ServiceSearchReq) (*model
        pageData, err := 
manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource](
                ctx.ResourceManager(),
                meshresource.ServiceProviderMetadataKind,
-               map[string]string{
-                       index.ByMeshIndex: req.Mesh,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
                },
                req.PageReq,
        )
@@ -120,14 +124,14 @@ func SearchServices(ctx consolectx.Context, req 
*model.ServiceSearchReq) (*model
        }, nil
 }
 
-// SearchServicesByKeywords search services by keywords, for now only support 
accurate search
+// SearchServicesByKeywords search services by keywords with prefix matching
 func SearchServicesByKeywords(ctx consolectx.Context, req 
*model.ServiceSearchReq) (*model.SearchPaginationResult, error) {
        pageData, err := 
manager.PageListByIndexes[*meshresource.ServiceProviderMetadataResource](
                ctx.ResourceManager(),
                meshresource.ServiceProviderMetadataKind,
-               map[string]string{
-                       index.ByMeshIndex:                  req.Mesh,
-                       index.ByServiceProviderServiceName: req.Keywords,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                       {IndexName: index.ByServiceProviderServiceName, Value: 
req.Keywords, Operator: index.HasPrefix},
                },
                req.PageReq,
        )
@@ -190,10 +194,10 @@ func GetServiceMethodDetail(ctx consolectx.Context, req 
model.ServiceMethodDetai
 }
 
 // providerIndexes defines the canonical indexes for provider metadata
-func providerIndexes(req model.BaseServiceReq) map[string]string {
-       return map[string]string{
-               index.ByMeshIndex:                 req.Mesh,
-               index.ByServiceProviderServiceKey: req.ServiceKey(),
+func providerIndexes(req model.BaseServiceReq) []index.IndexCondition {
+       return []index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: 
index.Equals},
+               {IndexName: index.ByServiceProviderServiceKey, Value: 
req.ServiceKey(), Operator: index.Equals},
        }
 }
 
diff --git a/pkg/console/service/service_provider_instances.go 
b/pkg/console/service/service_provider_instances.go
index 934c21fc..3ea90e81 100644
--- a/pkg/console/service/service_provider_instances.go
+++ b/pkg/console/service/service_provider_instances.go
@@ -50,9 +50,9 @@ func GetServiceProviderInstances(ctx consolectx.Context, req 
model.BaseServiceRe
                instanceList, err := 
manager.ListByIndexes[*meshresource.InstanceResource](
                        ctx.ResourceManager(),
                        meshresource.InstanceKind,
-                       map[string]string{
-                               index.ByMeshIndex:            req.Mesh,
-                               index.ByInstanceAppNameIndex: providerAppName,
+                       []index.IndexCondition{
+                               {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
+                               {IndexName: index.ByInstanceAppNameIndex, 
Value: providerAppName, Operator: index.Equals},
                        },
                )
                if err != nil {
diff --git a/pkg/console/service/tag_rule.go b/pkg/console/service/tag_rule.go
index 972cbf10..a051117c 100644
--- a/pkg/console/service/tag_rule.go
+++ b/pkg/console/service/tag_rule.go
@@ -36,8 +36,8 @@ func PageListTagRule(ctx consolectx.Context, req 
*model.SearchReq) (*model.Searc
        pageData, err := 
manager.PageListByIndexes[*meshresource.TagRouteResource](
                ctx.ResourceManager(),
                meshresource.TagRouteKind,
-               map[string]string{
-                       index.ByMeshIndex: req.Mesh,
+               []index.IndexCondition{
+                       {IndexName: index.ByMeshIndex, Value: req.Mesh, 
Operator: index.Equals},
                },
                req.PageReq)
        if err != nil {
diff --git a/pkg/core/bootstrap/init.go b/pkg/core/bootstrap/init.go
index 21bf1133..c590b84d 100644
--- a/pkg/core/bootstrap/init.go
+++ b/pkg/core/bootstrap/init.go
@@ -32,6 +32,7 @@ import (
        _ "github.com/apache/dubbo-admin/pkg/discovery/zk"
        _ "github.com/apache/dubbo-admin/pkg/engine/kubernetes"
        _ "github.com/apache/dubbo-admin/pkg/engine/mock"
+       _ "github.com/apache/dubbo-admin/pkg/governor/mock"
        _ "github.com/apache/dubbo-admin/pkg/governor/nacos2"
        _ "github.com/apache/dubbo-admin/pkg/governor/zk"
        _ "github.com/apache/dubbo-admin/pkg/store/memory"
diff --git a/pkg/core/discovery/subscriber/instance.go 
b/pkg/core/discovery/subscriber/instance.go
index ded8a67d..bb60fb8e 100644
--- a/pkg/core/discovery/subscriber/instance.go
+++ b/pkg/core/discovery/subscriber/instance.go
@@ -70,9 +70,9 @@ func (s *InstanceEventSubscriber) ProcessEvent(event 
events.Event) error {
        } else {
                instanceRes = oldObj
        }
-       instanceResList, err := s.instanceStore.ListByIndexes(map[string]string{
-               index.ByMeshIndex:            instanceRes.Mesh,
-               index.ByInstanceAppNameIndex: instanceRes.Spec.AppName,
+       instanceResList, err := 
s.instanceStore.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: instanceRes.Mesh, 
Operator: index.Equals},
+               {IndexName: index.ByInstanceAppNameIndex, Value: 
instanceRes.Spec.AppName, Operator: index.Equals},
        })
 
        appResKey := coremodel.BuildResourceKey(instanceRes.Mesh, 
instanceRes.Spec.AppName)
diff --git a/pkg/core/discovery/subscriber/nacos_service.go 
b/pkg/core/discovery/subscriber/nacos_service.go
index e5c9c631..07c23f10 100644
--- a/pkg/core/discovery/subscriber/nacos_service.go
+++ b/pkg/core/discovery/subscriber/nacos_service.go
@@ -127,9 +127,9 @@ func (n *NacosServiceEventSubscriber) 
processConsumerMetadataUpsert(serviceRes *
                logger.Errorf("process service consumer metadata upsert event, 
but cannot route to service consumer metadata resource, cause: %v", err)
                return err
        }
-       resources, err := st.ListByIndexes(map[string]string{
-               index.ByMeshIndex:                  serviceRes.Mesh,
-               index.ByServiceConsumerServiceName: serviceName,
+       resources, err := st.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, 
Operator: index.Equals},
+               {IndexName: index.ByServiceConsumerServiceName, Value: 
serviceName, Operator: index.Equals},
        })
        if err != nil {
                logger.Errorf("process service consumer metadata upsert event, 
but cannot list service consumer metadata resource of %s, cause: %v", 
serviceRes.Name, err)
@@ -212,9 +212,9 @@ func (n *NacosServiceEventSubscriber) 
processRPCInstanceUpsert(serviceRes *meshr
                logger.Errorf("process rpc instance upsert event, but cannot 
route to rpc instance resource, cause: %v", err)
                return err
        }
-       resources, err := st.ListByIndexes(map[string]string{
-               index.ByMeshIndex:          serviceRes.Mesh,
-               index.ByRPCInstanceAppName: serviceRes.Name,
+       resources, err := st.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, 
Operator: index.Equals},
+               {IndexName: index.ByRPCInstanceAppName, Value: serviceRes.Name, 
Operator: index.Equals},
        })
        if err != nil {
                logger.Errorf("process rpc instance upsert event, but cannot 
list rpc instance resource of %s, cause: %v", serviceRes.Name, err)
@@ -287,9 +287,9 @@ func (n *NacosServiceEventSubscriber) 
processServiceConsumerDelete(serviceRes *m
                logger.Errorf("process service consumer delete event, but 
cannot route to service consumer metadata resource, cause: %v", err)
                return err
        }
-       resources, err := st.ListByIndexes(map[string]string{
-               index.ByMeshIndex:                  serviceRes.Mesh,
-               index.ByServiceConsumerServiceName: serviceRes.Name,
+       resources, err := st.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, 
Operator: index.Equals},
+               {IndexName: index.ByServiceConsumerServiceName, Value: 
serviceRes.Name, Operator: index.Equals},
        })
        if err != nil {
                logger.Errorf("process service consumer delete event, but 
cannot list service consumer metadata resource of %s, cause: %v", 
serviceRes.Name, err)
@@ -311,9 +311,9 @@ func (n *NacosServiceEventSubscriber) 
processRPCInstanceDelete(serviceRes *meshr
                logger.Errorf("process rpc instance delete event, but cannot 
route to rpc instance resource, cause: %v", err)
                return err
        }
-       resources, err := st.ListByIndexes(map[string]string{
-               index.ByMeshIndex:          serviceRes.Mesh,
-               index.ByRPCInstanceAppName: serviceRes.Name,
+       resources, err := st.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByMeshIndex, Value: serviceRes.Mesh, 
Operator: index.Equals},
+               {IndexName: index.ByRPCInstanceAppName, Value: serviceRes.Name, 
Operator: index.Equals},
        })
        if err != nil {
                logger.Errorf("process rpc instance delete event, but cannot 
list rpc instance resource of %s, cause: %v", serviceRes.Name, err)
diff --git a/pkg/core/discovery/subscriber/rpc_instance.go 
b/pkg/core/discovery/subscriber/rpc_instance.go
index e970239d..0343e5cf 100644
--- a/pkg/core/discovery/subscriber/rpc_instance.go
+++ b/pkg/core/discovery/subscriber/rpc_instance.go
@@ -185,8 +185,8 @@ func (s *RPCInstanceEventSubscriber) 
findRelatedRuntimeInstanceAndMerge(instance
 }
 
 func (s *RPCInstanceEventSubscriber) getRuntimeInstanceByIp(ip string) 
*meshresource.RuntimeInstanceResource {
-       resources, err := s.rtInstanceStore.ListByIndexes(map[string]string{
-               index.ByRuntimeInstanceIPIndex: ip,
+       resources, err := 
s.rtInstanceStore.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByRuntimeInstanceIPIndex, Value: ip, 
Operator: index.Equals},
        })
        if err != nil {
                logger.Errorf("list runtime instance by ip index failed, ip: 
%s, err: %s", ip, err.Error())
diff --git a/pkg/core/engine/subscriber/runtime_instance.go 
b/pkg/core/engine/subscriber/runtime_instance.go
index 6038462c..f7d38709 100644
--- a/pkg/core/engine/subscriber/runtime_instance.go
+++ b/pkg/core/engine/subscriber/runtime_instance.go
@@ -161,8 +161,8 @@ func (s *RuntimeInstanceEventSubscriber) 
getRelatedInstanceByName(
                return nil, nil
        }
        instanceResName := 
meshresource.BuildInstanceResName(rtInstanceRes.Spec.AppName, 
rtInstanceRes.Spec.Ip, rtInstanceRes.Spec.RpcPort)
-       resources, err := s.instanceStore.ListByIndexes(map[string]string{
-               index.ByInstanceNameIndex: instanceResName,
+       resources, err := s.instanceStore.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByInstanceNameIndex, Value: instanceResName, 
Operator: index.Equals},
        })
        if err != nil {
                return nil, err
@@ -190,8 +190,8 @@ func (s *RuntimeInstanceEventSubscriber) 
getRelatedInstanceByName(
 
 func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByIP(
        rtInstanceRes *meshresource.RuntimeInstanceResource) 
(*meshresource.InstanceResource, error) {
-       resources, err := s.instanceStore.ListByIndexes(map[string]string{
-               index.ByInstanceIpIndex: rtInstanceRes.Spec.Ip,
+       resources, err := s.instanceStore.ListByIndexes([]index.IndexCondition{
+               {IndexName: index.ByInstanceIpIndex, Value: 
rtInstanceRes.Spec.Ip, Operator: index.Equals},
        })
        if err != nil {
                return nil, err
diff --git a/pkg/core/manager/manager.go b/pkg/core/manager/manager.go
index 44861ca7..3e4ce094 100644
--- a/pkg/core/manager/manager.go
+++ b/pkg/core/manager/manager.go
@@ -25,6 +25,7 @@ import (
        "github.com/apache/dubbo-admin/pkg/core/governor"
        "github.com/apache/dubbo-admin/pkg/core/resource/model"
        "github.com/apache/dubbo-admin/pkg/core/store"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
 )
 
 type ReadOnlyResourceManager interface {
@@ -32,13 +33,10 @@ type ReadOnlyResourceManager interface {
        GetByKey(rk model.ResourceKind, key string) (r model.Resource, exist 
bool, err error)
        // GetByKeys returns the resources with the given resource keys
        GetByKeys(rk model.ResourceKind, keys []string) ([]model.Resource, 
error)
-       // ListByIndexes returns the resources with the given indexes, indexes 
is a map of index name and index value
-       ListByIndexes(rk model.ResourceKind, indexes map[string]string) 
([]model.Resource, error)
-       // PageListByIndexes page list the resources with the given indexes, 
indexes is a map of index name and index value
-       PageListByIndexes(rk model.ResourceKind, indexes map[string]string, pr 
model.PageReq) (*model.PageData[model.Resource], error)
-       // PageSearchResourceByConditions page fuzzy search resource by 
conditions, conditions cannot be empty
-       // TODO support multiple conditions
-       PageSearchResourceByConditions(rk model.ResourceKind, conditions 
[]string, pr model.PageReq) (*model.PageData[model.Resource], error)
+       // ListByIndexes returns the resources with the given index conditions
+       ListByIndexes(rk model.ResourceKind, indexes []index.IndexCondition) 
([]model.Resource, error)
+       // PageListByIndexes page list the resources with the given index 
conditions
+       PageListByIndexes(rk model.ResourceKind, indexes 
[]index.IndexCondition, pr model.PageReq) (*model.PageData[model.Resource], 
error)
 }
 
 type WriteOnlyResourceManager interface {
@@ -100,7 +98,7 @@ func (rm *resourcesManager) GetByKeys(rk model.ResourceKind, 
keys []string) ([]m
        return resources, nil
 }
 
-func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes 
map[string]string) ([]model.Resource, error) {
+func (rm *resourcesManager) ListByIndexes(rk model.ResourceKind, indexes 
[]index.IndexCondition) ([]model.Resource, error) {
        rs, err := rm.storeRouter.ResourceKindRoute(rk)
        if err != nil {
                return nil, err
@@ -114,7 +112,7 @@ func (rm *resourcesManager) ListByIndexes(rk 
model.ResourceKind, indexes map[str
 
 func (rm *resourcesManager) PageListByIndexes(
        rk model.ResourceKind,
-       indexes map[string]string,
+       indexes []index.IndexCondition,
        pr model.PageReq) (*model.PageData[model.Resource], error) {
 
        rs, err := rm.storeRouter.ResourceKindRoute(rk)
@@ -128,11 +126,6 @@ func (rm *resourcesManager) PageListByIndexes(
        return pageData, nil
 }
 
-func (rm *resourcesManager) PageSearchResourceByConditions(rk 
model.ResourceKind, conditions []string, pr model.PageReq) 
(*model.PageData[model.Resource], error) {
-       //TODO implement me
-       panic("implement me")
-}
-
 func (rm *resourcesManager) Add(r model.Resource) error {
        if !governor.RuleResourceKinds.Contain(r.ResourceKind()) {
                return bizerror.New(bizerror.InvalidArgument, "invalid resource 
kind")
diff --git a/pkg/core/manager/manager_helper.go 
b/pkg/core/manager/manager_helper.go
index 80e40ba8..48a33363 100644
--- a/pkg/core/manager/manager_helper.go
+++ b/pkg/core/manager/manager_helper.go
@@ -22,6 +22,7 @@ import (
 
        "github.com/apache/dubbo-admin/pkg/common/bizerror"
        "github.com/apache/dubbo-admin/pkg/core/resource/model"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
 )
 
 // GetByKey is a helper function of ResourceManager.GeyByKey
@@ -59,7 +60,7 @@ func GetByKeys[T model.Resource](rm ReadOnlyResourceManager, 
rk model.ResourceKi
 }
 
 // ListByIndexes is a helper function of ResourceManager.ListByIndexes
-func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk 
model.ResourceKind, indexes map[string]string) ([]T, error) {
+func ListByIndexes[T model.Resource](rm ReadOnlyResourceManager, rk 
model.ResourceKind, indexes []index.IndexCondition) ([]T, error) {
        resources, err := rm.ListByIndexes(rk, indexes)
        if err != nil {
                return nil, err
@@ -81,7 +82,7 @@ func ListByIndexes[T model.Resource](rm 
ReadOnlyResourceManager, rk model.Resour
 func PageListByIndexes[T model.Resource](
        rm ReadOnlyResourceManager,
        rk model.ResourceKind,
-       indexes map[string]string,
+       indexes []index.IndexCondition,
        pr model.PageReq) (*model.PageData[T], error) {
 
        pageData, err := rm.PageListByIndexes(rk, indexes, pr)
@@ -107,33 +108,3 @@ func PageListByIndexes[T model.Resource](
        }
        return newPageData, nil
 }
-
-// PageSearchResourceByConditions is a helper function of 
ResourceManager.PageSearchResourceByConditions
-func PageSearchResourceByConditions[T model.Resource](
-       rm ReadOnlyResourceManager,
-       rk model.ResourceKind,
-       conditions []string,
-       pr model.PageReq) (*model.PageData[T], error) {
-       pageData, err := rm.PageSearchResourceByConditions(rk, conditions, pr)
-       if err != nil {
-               return nil, err
-       }
-
-       typedResources := make([]T, len(pageData.Data))
-       for i, resource := range pageData.Data {
-               typedResource, ok := resource.(T)
-               if !ok {
-                       return nil, bizerror.NewAssertionError(rk, 
reflect.TypeOf(typedResource).Name())
-               }
-               typedResources[i] = typedResource
-       }
-       newPageData := &model.PageData[T]{
-               Pagination: model.Pagination{
-                       Total:      pageData.Total,
-                       PageOffset: pageData.PageOffset,
-                       PageSize:   pageData.PageSize,
-               },
-               Data: typedResources,
-       }
-       return newPageData, nil
-}
diff --git a/pkg/core/store/index/condition.go 
b/pkg/core/store/index/condition.go
new file mode 100644
index 00000000..4bdcee2f
--- /dev/null
+++ b/pkg/core/store/index/condition.go
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package index
+
+// IndexOperator defines the comparison operator for index queries
+type IndexOperator string
+
+const (
+       // Equals performs exact match on the index value
+       Equals IndexOperator = "Equals"
+       // HasPrefix performs prefix match on the index value
+       HasPrefix IndexOperator = "HasPrefix"
+)
+
+// IndexCondition represents a single index query condition
+type IndexCondition struct {
+       // IndexName is the name of the index to query
+       IndexName string
+       // Value is the value to match against the index
+       Value string
+       // Operator is the comparison operator to use (Equals, HasPrefix, etc.)
+       Operator IndexOperator
+}
diff --git a/pkg/core/store/store.go b/pkg/core/store/store.go
index 15d2117e..8923f657 100644
--- a/pkg/core/store/store.go
+++ b/pkg/core/store/store.go
@@ -27,6 +27,7 @@ import (
 
        "github.com/apache/dubbo-admin/pkg/core/resource/model"
        "github.com/apache/dubbo-admin/pkg/core/runtime"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
 )
 
 // ResourceStore defines the interface for the persistance of a resource
@@ -36,10 +37,10 @@ type ResourceStore interface {
        // GetByKeys get resources by keys, return list of resource.
        // if a resource of specified key doesn't exist in the store, resource 
list will not include it
        GetByKeys(keys []string) ([]model.Resource, error)
-       // ListByIndexes list resources by indexes, indexes is map of index 
name and index value
-       ListByIndexes(indexes map[string]string) ([]model.Resource, error)
-       // PageListByIndexes list resources by indexes pageable, indexes is map 
of index name and index value
-       PageListByIndexes(indexes map[string]string, pq model.PageReq) 
(*model.PageData[model.Resource], error)
+       // ListByIndexes list resources by index conditions
+       ListByIndexes(indexes []index.IndexCondition) ([]model.Resource, error)
+       // PageListByIndexes list resources by index conditions pageable
+       PageListByIndexes(indexes []index.IndexCondition, pq model.PageReq) 
(*model.PageData[model.Resource], error)
 }
 
 // ManagedResourceStore includes both functional interfaces and lifecycle 
interfaces
diff --git a/pkg/governor/mock/factory.go b/pkg/governor/mock/factory.go
new file mode 100644
index 00000000..4c7b282d
--- /dev/null
+++ b/pkg/governor/mock/factory.go
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mock
+
+import (
+       discoverycfg "github.com/apache/dubbo-admin/pkg/config/discovery"
+       "github.com/apache/dubbo-admin/pkg/core/events"
+       "github.com/apache/dubbo-admin/pkg/core/governor"
+       coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
+       "github.com/apache/dubbo-admin/pkg/core/store"
+)
+
+func init() {
+       governor.RegisterFactory(&mockGovernorFactory{})
+}
+
+type mockGovernorFactory struct{}
+
+var _ governor.Factory = &mockGovernorFactory{}
+
+func (f *mockGovernorFactory) Support(t discoverycfg.Type) bool {
+       return t == discoverycfg.Mock
+}
+
+func (f *mockGovernorFactory) New(_ string, _ *discoverycfg.Config, _ 
store.Router, _ events.Emitter) (governor.RuleGovernor, error) {
+       return &mockGovernor{}, nil
+}
+
+type mockGovernor struct{}
+
+var _ governor.RuleGovernor = &mockGovernor{}
+
+func (g *mockGovernor) CreateRule(_ coremodel.Resource) error { return nil }
+func (g *mockGovernor) UpdateRule(_ coremodel.Resource) error { return nil }
+func (g *mockGovernor) DeleteRule(_ coremodel.Resource) error { return nil }
diff --git a/pkg/store/dbcommon/gorm_store.go b/pkg/store/dbcommon/gorm_store.go
index f72b4376..cedc2014 100644
--- a/pkg/store/dbcommon/gorm_store.go
+++ b/pkg/store/dbcommon/gorm_store.go
@@ -22,6 +22,7 @@ import (
        "fmt"
        "reflect"
        "sort"
+       "sync"
 
        "gorm.io/gorm"
        "k8s.io/client-go/tools/cache"
@@ -35,14 +36,15 @@ import (
 )
 
 // GormStore is a GORM-backed store implementation for Dubbo resources
-// It uses GORM for database operations and maintains in-memory indices for 
fast lookups
+// It uses GORM for database operations and persists all indices to the 
resource_indices table
 // This implementation is database-agnostic and works with any GORM-supported 
database
 type GormStore struct {
-       pool    *ConnectionPool // Shared connection pool with reference 
counting
-       kind    model.ResourceKind
-       address string
-       indices *Index // In-memory index with thread-safe operations
-       stopCh  chan struct{}
+       pool     *ConnectionPool // Shared connection pool with reference 
counting
+       kind     model.ResourceKind
+       address  string
+       indexers cache.Indexers // Index functions for creating indices
+       mu       sync.RWMutex   // Protects indexers
+       stopCh   chan struct{}
 }
 
 var _ store.ManagedResourceStore = &GormStore{}
@@ -50,15 +52,15 @@ var _ store.ManagedResourceStore = &GormStore{}
 // NewGormStore creates a new GORM store for the specified resource kind
 func NewGormStore(kind model.ResourceKind, address string, pool 
*ConnectionPool) *GormStore {
        return &GormStore{
-               kind:    kind,
-               address: address,
-               pool:    pool,
-               indices: NewIndex(),
-               stopCh:  make(chan struct{}),
+               kind:     kind,
+               address:  address,
+               pool:     pool,
+               indexers: make(cache.Indexers),
+               stopCh:   make(chan struct{}),
        }
 }
 
-// Init initializes the GORM store by migrating the schema and rebuilding 
indices
+// Init initializes the GORM store by migrating the schema and registering 
indexers
 func (gs *GormStore) Init(_ runtime.BuilderContext) error {
        // Perform table migration
        db := gs.pool.GetDB()
@@ -66,15 +68,22 @@ func (gs *GormStore) Init(_ runtime.BuilderContext) error {
        if err := 
db.Scopes(TableScope(gs.kind.ToString())).AutoMigrate(&ResourceModel{}); err != 
nil {
                return fmt.Errorf("failed to migrate schema for %s: %w", 
gs.kind.ToString(), err)
        }
+
+       // Migrate resource_indices table (shared across all resource kinds)
+       if err := db.AutoMigrate(&ResourceIndexModel{}); err != nil {
+               return fmt.Errorf("failed to migrate resource_indices: %w", err)
+       }
+
        // Register indexers for the resource kind
        indexers := index.IndexersRegistry().Indexers(gs.kind)
        if err := gs.AddIndexers(indexers); err != nil {
                return err
        }
 
-       // Rebuild indices from existing data in the database
-       if err := gs.rebuildIndices(); err != nil {
-               return fmt.Errorf("failed to rebuild indices for %s: %w", 
gs.kind.ToString(), err)
+       // Backfill resource_indices from existing ResourceModel rows so that
+       // ListByIndexes/HasPrefix work correctly after restarts or upgrades.
+       if err := gs.backfillIndices(); err != nil {
+               return fmt.Errorf("failed to backfill indices for %s: %w", 
gs.kind.ToString(), err)
        }
 
        logger.Infof("GORM store initialized for resource kind: %s", 
gs.kind.ToString())
@@ -138,14 +147,15 @@ func (gs *GormStore) Add(obj interface{}) error {
                return err
        }
 
-       if err := db.Scopes(TableScope(gs.kind.ToString())).Create(m).Error; 
err != nil {
-               return err
-       }
-
-       // Update indices after successful DB operation
-       gs.indices.UpdateResource(resource, nil)
-
-       return nil
+       return db.Transaction(func(tx *gorm.DB) error {
+               if err := 
tx.Scopes(TableScope(gs.kind.ToString())).Create(m).Error; err != nil {
+                       return err
+               }
+               if err := gs.persistIndexEntriesTx(tx, resource, nil); err != 
nil {
+                       return fmt.Errorf("failed to persist index entries for 
%s: %w", resource.ResourceKey(), err)
+               }
+               return nil
+       })
 }
 
 // Update modifies an existing resource in the database
@@ -178,30 +188,32 @@ func (gs *GormStore) Update(obj interface{}) error {
        }
 
        db := gs.pool.GetDB()
-       result := 
db.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).
-               Where("resource_key = ?", resource.ResourceKey()).
-               Updates(map[string]interface{}{
-                       "name": m.Name,
-                       "mesh": m.Mesh,
-                       "data": m.Data,
-               })
-
-       if result.Error != nil {
-               return result.Error
-       }
-
-       if result.RowsAffected == 0 {
-               return store.ErrorResourceNotFound(
-                       resource.ResourceKind().ToString(),
-                       resource.ResourceMeta().Name,
-                       resource.ResourceMesh(),
-               )
-       }
+       return db.Transaction(func(tx *gorm.DB) error {
+               result := 
tx.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).
+                       Where("resource_key = ?", resource.ResourceKey()).
+                       Updates(map[string]interface{}{
+                               "name": m.Name,
+                               "mesh": m.Mesh,
+                               "data": m.Data,
+                       })
+
+               if result.Error != nil {
+                       return result.Error
+               }
 
-       // Update indices: remove old and add new
-       gs.indices.UpdateResource(resource, oldResource.(model.Resource))
+               if result.RowsAffected == 0 {
+                       return store.ErrorResourceNotFound(
+                               resource.ResourceKind().ToString(),
+                               resource.ResourceMeta().Name,
+                               resource.ResourceMesh(),
+                       )
+               }
 
-       return nil
+               if err := gs.persistIndexEntriesTx(tx, resource, 
oldResource.(model.Resource)); err != nil {
+                       return fmt.Errorf("failed to persist index entries for 
%s: %w", resource.ResourceKey(), err)
+               }
+               return nil
+       })
 }
 
 // Delete removes a resource from the database
@@ -212,26 +224,26 @@ func (gs *GormStore) Delete(obj interface{}) error {
        }
 
        db := gs.pool.GetDB()
-       result := db.Scopes(TableScope(gs.kind.ToString())).
-               Where("resource_key = ?", resource.ResourceKey()).
-               Delete(&ResourceModel{})
-
-       if result.Error != nil {
-               return result.Error
-       }
+       return db.Transaction(func(tx *gorm.DB) error {
+               result := tx.Scopes(TableScope(gs.kind.ToString())).
+                       Where("resource_key = ?", resource.ResourceKey()).
+                       Delete(&ResourceModel{})
 
-       if result.RowsAffected == 0 {
-               return store.ErrorResourceNotFound(
-                       resource.ResourceKind().ToString(),
-                       resource.ResourceMeta().Name,
-                       resource.ResourceMesh(),
-               )
-       }
+               if result.Error != nil {
+                       return result.Error
+               }
 
-       // Remove from indices
-       gs.indices.RemoveResource(resource)
+               if result.RowsAffected == 0 {
+                       return store.ErrorResourceNotFound(
+                               resource.ResourceKind().ToString(),
+                               resource.ResourceMeta().Name,
+                               resource.ResourceMesh(),
+                       )
+               }
 
-       return nil
+               return tx.Where("resource_kind = ? AND resource_key = ?", 
gs.kind.ToString(), resource.ResourceKey()).
+                       Delete(&ResourceIndexModel{}).Error
+       })
 }
 
 // List returns all resources of the configured kind from the database
@@ -308,8 +320,11 @@ func (gs *GormStore) Replace(list []interface{}, _ string) 
error {
                        return err
                }
 
-               // Clear all indices
-               gs.clearIndices()
+               // Delete all index entries for this resource kind
+               if err := tx.Where("resource_kind = ?", gs.kind.ToString()).
+                       Delete(&ResourceIndexModel{}).Error; err != nil {
+                       return err
+               }
 
                // Return early if list is empty
                if len(list) == 0 {
@@ -338,9 +353,31 @@ func (gs *GormStore) Replace(list []interface{}, _ string) 
error {
                        return err
                }
 
-               // Rebuild indices for all resources
+               // Persist all index entries in bulk
+               var indexEntries []ResourceIndexModel
+               indexers := gs.GetIndexers()
                for _, resource := range resources {
-                       gs.indices.UpdateResource(resource, nil)
+                       for indexName, indexFunc := range indexers {
+                               values, err := indexFunc(resource)
+                               if err != nil {
+                                       continue
+                               }
+                               for _, v := range values {
+                                       indexEntries = append(indexEntries, 
ResourceIndexModel{
+                                               ResourceKind: 
gs.kind.ToString(),
+                                               IndexName:    indexName,
+                                               IndexValue:   v,
+                                               ResourceKey:  
resource.ResourceKey(),
+                                               Operator:     
string(index.Equals),
+                                       })
+                               }
+                       }
+               }
+
+               if len(indexEntries) > 0 {
+                       if err := tx.CreateInBatches(&indexEntries, 100).Error; 
err != nil {
+                               return fmt.Errorf("failed to persist index 
entries during replace: %w", err)
+                       }
                }
 
                return nil
@@ -352,11 +389,12 @@ func (gs *GormStore) Resync() error {
 }
 
 func (gs *GormStore) Index(indexName string, obj interface{}) ([]interface{}, 
error) {
-       if !gs.indices.IndexExists(indexName) {
+       if !gs.IndexExists(indexName) {
                return nil, fmt.Errorf("index %s does not exist", indexName)
        }
 
-       indexFunc := gs.indices.GetIndexers()[indexName]
+       indexers := gs.GetIndexers()
+       indexFunc := indexers[indexName]
        indexValues, err := indexFunc(obj)
        if err != nil {
                return nil, err
@@ -370,7 +408,7 @@ func (gs *GormStore) Index(indexName string, obj 
interface{}) ([]interface{}, er
 }
 
 func (gs *GormStore) IndexKeys(indexName, indexedValue string) ([]string, 
error) {
-       if !gs.indices.IndexExists(indexName) {
+       if !gs.IndexExists(indexName) {
                return nil, fmt.Errorf("index %s does not exist", indexName)
        }
 
@@ -390,15 +428,24 @@ func (gs *GormStore) IndexKeys(indexName, indexedValue 
string) ([]string, error)
 }
 
 func (gs *GormStore) ListIndexFuncValues(indexName string) []string {
-       if !gs.indices.IndexExists(indexName) {
+       if !gs.IndexExists(indexName) {
                return []string{}
        }
 
-       return gs.indices.ListIndexFuncValues(indexName)
+       var values []string
+       db := gs.pool.GetDB()
+       if err := db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND index_name = ?", 
gs.kind.ToString(), indexName).
+               Distinct("index_value").
+               Pluck("index_value", &values).Error; err != nil {
+               logger.Errorf("failed to list index func values for %s: %v", 
indexName, err)
+               return []string{}
+       }
+       return values
 }
 
 func (gs *GormStore) ByIndex(indexName, indexedValue string) ([]interface{}, 
error) {
-       if !gs.indices.IndexExists(indexName) {
+       if !gs.IndexExists(indexName) {
                return nil, fmt.Errorf("index %s does not exist", indexName)
        }
 
@@ -406,11 +453,28 @@ func (gs *GormStore) ByIndex(indexName, indexedValue 
string) ([]interface{}, err
 }
 
 func (gs *GormStore) GetIndexers() cache.Indexers {
-       return gs.indices.GetIndexers()
+       gs.mu.RLock()
+       defer gs.mu.RUnlock()
+
+       result := make(cache.Indexers, len(gs.indexers))
+       for k, v := range gs.indexers {
+               result[k] = v
+       }
+       return result
 }
 
 func (gs *GormStore) AddIndexers(newIndexers cache.Indexers) error {
-       return gs.indices.AddIndexers(newIndexers)
+       gs.mu.Lock()
+       defer gs.mu.Unlock()
+
+       for name, indexFunc := range newIndexers {
+               if _, exists := gs.indexers[name]; exists {
+                       return fmt.Errorf("indexer %s already exists", name)
+               }
+               gs.indexers[name] = indexFunc
+       }
+
+       return nil
 }
 
 func (gs *GormStore) GetByKeys(keys []string) ([]model.Resource, error) {
@@ -439,7 +503,7 @@ func (gs *GormStore) GetByKeys(keys []string) 
([]model.Resource, error) {
        return resources, nil
 }
 
-func (gs *GormStore) ListByIndexes(indexes map[string]string) 
([]model.Resource, error) {
+func (gs *GormStore) ListByIndexes(indexes []index.IndexCondition) 
([]model.Resource, error) {
        keys, err := gs.getKeysByIndexes(indexes)
        if err != nil {
                return nil, err
@@ -457,7 +521,7 @@ func (gs *GormStore) ListByIndexes(indexes 
map[string]string) ([]model.Resource,
        return resources, nil
 }
 
-func (gs *GormStore) PageListByIndexes(indexes map[string]string, pq 
model.PageReq) (*model.PageData[model.Resource], error) {
+func (gs *GormStore) PageListByIndexes(indexes []index.IndexCondition, pq 
model.PageReq) (*model.PageData[model.Resource], error) {
        keys, err := gs.getKeysByIndexes(indexes)
        if err != nil {
                return nil, err
@@ -485,17 +549,34 @@ func (gs *GormStore) PageListByIndexes(indexes 
map[string]string, pq model.PageR
 }
 
 func (gs *GormStore) findByIndex(indexName, indexedValue string) 
([]interface{}, error) {
-       if !gs.indices.IndexExists(indexName) {
+       if !gs.IndexExists(indexName) {
                return nil, fmt.Errorf("index %s does not exist", indexName)
        }
 
-       // Get resource keys from in-memory index
-       keys := gs.indices.GetKeys(indexName, indexedValue)
+       // Get resource keys from database index
+       db := gs.pool.GetDB()
+       var entries []ResourceIndexModel
+       err := db.Where("resource_kind = ? AND index_name = ? AND index_value = 
?",
+               gs.kind.ToString(), indexName, indexedValue).
+               Find(&entries).Error
+       if err != nil {
+               return nil, err
+       }
 
-       if len(keys) == 0 {
+       if len(entries) == 0 {
                return []interface{}{}, nil
        }
 
+       // Collect unique resource keys
+       keys := make([]string, 0, len(entries))
+       seen := make(map[string]struct{})
+       for _, e := range entries {
+               if _, ok := seen[e.ResourceKey]; !ok {
+                       keys = append(keys, e.ResourceKey)
+                       seen[e.ResourceKey] = struct{}{}
+               }
+       }
+
        // Fetch resources from DB by keys
        resources, err := gs.GetByKeys(keys)
        if err != nil {
@@ -511,7 +592,7 @@ func (gs *GormStore) findByIndex(indexName, indexedValue 
string) ([]interface{},
        return result, nil
 }
 
-func (gs *GormStore) getKeysByIndexes(indexes map[string]string) ([]string, 
error) {
+func (gs *GormStore) getKeysByIndexes(indexes []index.IndexCondition) 
([]string, error) {
        if len(indexes) == 0 {
                return gs.ListKeys(), nil
        }
@@ -519,8 +600,17 @@ func (gs *GormStore) getKeysByIndexes(indexes 
map[string]string) ([]string, erro
        var keySet map[string]struct{}
        first := true
 
-       for indexName, indexValue := range indexes {
-               keys, err := gs.IndexKeys(indexName, indexValue)
+       for _, condition := range indexes {
+               var keys []string
+               var err error
+               switch condition.Operator {
+               case index.Equals:
+                       keys, err = gs.IndexKeys(condition.IndexName, 
condition.Value)
+               case index.HasPrefix:
+                       keys, err = 
gs.getKeysByPrefixFromDB(condition.IndexName, condition.Value)
+               default:
+                       return nil, bizerror.New(bizerror.InvalidArgument, 
"operator not yet supported: "+string(condition.Operator))
+               }
                if err != nil {
                        return nil, err
                }
@@ -550,37 +640,122 @@ func (gs *GormStore) getKeysByIndexes(indexes 
map[string]string) ([]string, erro
        return result, nil
 }
 
-// clearIndices clears all in-memory indices
-func (gs *GormStore) clearIndices() {
-       gs.indices.Clear()
+// persistIndexEntriesTx writes index entries for a resource within an 
existing transaction.
+// If oldResource is not nil, first deletes its old entries scoped by 
resource_kind.
+func (gs *GormStore) persistIndexEntriesTx(tx *gorm.DB, resource 
model.Resource, oldResource model.Resource) error {
+       if oldResource != nil {
+               if err := tx.Where("resource_kind = ? AND resource_key = ?", 
gs.kind.ToString(), oldResource.ResourceKey()).
+                       Delete(&ResourceIndexModel{}).Error; err != nil {
+                       return err
+               }
+       }
+
+       indexers := gs.GetIndexers()
+       var entries []ResourceIndexModel
+       for indexName, indexFunc := range indexers {
+               values, err := indexFunc(resource)
+               if err != nil {
+                       continue
+               }
+               for _, v := range values {
+                       entries = append(entries, ResourceIndexModel{
+                               ResourceKind: gs.kind.ToString(),
+                               IndexName:    indexName,
+                               IndexValue:   v,
+                               ResourceKey:  resource.ResourceKey(),
+                               Operator:     string(index.Equals),
+                       })
+               }
+       }
+
+       if len(entries) == 0 {
+               return nil
+       }
+
+       return tx.Create(&entries).Error
 }
 
-// rebuildIndices rebuilds all in-memory indices from existing database records
-// This is called during initialization to ensure indices are populated with 
existing data
-func (gs *GormStore) rebuildIndices() error {
-       // Clear existing indices first
-       gs.clearIndices()
+// backfillIndices rebuilds resource_indices from existing ResourceModel rows.
+// Called at Init time so that ListByIndexes works correctly after restarts or 
upgrades.
+func (gs *GormStore) backfillIndices() error {
+       db := gs.pool.GetDB()
 
-       // Load all resources from the database
+       // Load all existing resources for this kind
        var models []ResourceModel
+       if err := 
db.Scopes(TableScope(gs.kind.ToString())).Find(&models).Error; err != nil {
+               return err
+       }
+       if len(models) == 0 {
+               return nil
+       }
+
+       return db.Transaction(func(tx *gorm.DB) error {
+               // Drop stale index rows for this kind and rebuild from scratch
+               if err := tx.Where("resource_kind = ?", gs.kind.ToString()).
+                       Delete(&ResourceIndexModel{}).Error; err != nil {
+                       return err
+               }
+
+               indexers := gs.GetIndexers()
+               var entries []ResourceIndexModel
+               for _, m := range models {
+                       resource, err := m.ToResource()
+                       if err != nil {
+                               logger.Warnf("backfillIndices: failed to 
deserialize resource %s: %v", m.ResourceKey, err)
+                               continue
+                       }
+                       for indexName, indexFunc := range indexers {
+                               values, err := indexFunc(resource)
+                               if err != nil {
+                                       continue
+                               }
+                               for _, v := range values {
+                                       entries = append(entries, 
ResourceIndexModel{
+                                               ResourceKind: 
gs.kind.ToString(),
+                                               IndexName:    indexName,
+                                               IndexValue:   v,
+                                               ResourceKey:  
resource.ResourceKey(),
+                                               Operator:     
string(index.Equals),
+                                       })
+                               }
+                       }
+               }
+
+               if len(entries) == 0 {
+                       return nil
+               }
+               return tx.CreateInBatches(&entries, 100).Error
+       })
+}
+
+// getKeysByPrefixFromDB retrieves resource keys matching a prefix from the 
database
+func (gs *GormStore) getKeysByPrefixFromDB(indexName, prefix string) 
([]string, error) {
        db := gs.pool.GetDB()
-       if err := 
db.Scopes(TableScope(gs.kind.ToString())).Model(&ResourceModel{}).Find(&models).Error;
 err != nil {
-               return fmt.Errorf("failed to load resources for index rebuild: 
%w", err)
+       var entries []ResourceIndexModel
+       err := db.Where("resource_kind = ? AND index_name = ? AND index_value 
LIKE ?",
+               gs.kind.ToString(), indexName, prefix+"%").
+               Find(&entries).Error
+       if err != nil {
+               return nil, err
        }
 
-       // Rebuild indices for all resources
-       for _, m := range models {
-               resource, err := m.ToResource()
-               if err != nil {
-                       logger.Errorf("failed to deserialize resource during 
index rebuild: %v", err)
-                       continue
+       keys := make([]string, 0, len(entries))
+       seen := make(map[string]struct{})
+       for _, e := range entries {
+               if _, ok := seen[e.ResourceKey]; !ok {
+                       keys = append(keys, e.ResourceKey)
+                       seen[e.ResourceKey] = struct{}{}
                }
-               // Add resource to indices (nil for oldResource since this is 
initial load)
-               gs.indices.UpdateResource(resource, nil)
        }
+       return keys, nil
+}
 
-       logger.Infof("Rebuilt indices for %s: loaded %d resources", 
gs.kind.ToString(), len(models))
-       return nil
+// IndexExists checks if an indexer with the given name exists
+func (gs *GormStore) IndexExists(indexName string) bool {
+       gs.mu.RLock()
+       defer gs.mu.RUnlock()
+       _, exists := gs.indexers[indexName]
+       return exists
 }
 
 // Pool returns the connection pool for this store
diff --git a/pkg/store/dbcommon/gorm_store_test.go 
b/pkg/store/dbcommon/gorm_store_test.go
index da9718ed..b1dd1b8c 100644
--- a/pkg/store/dbcommon/gorm_store_test.go
+++ b/pkg/store/dbcommon/gorm_store_test.go
@@ -33,6 +33,7 @@ import (
 
        storecfg "github.com/apache/dubbo-admin/pkg/config/store"
        "github.com/apache/dubbo-admin/pkg/core/resource/model"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
 )
 
 // mockResource is a mock implementation of model.Resource for testing
@@ -170,7 +171,7 @@ func TestNewGormStore(t *testing.T) {
        assert.Equal(t, kind, store.kind)
        assert.Equal(t, "test-address", store.address)
        assert.NotNil(t, store.pool)
-       assert.NotNil(t, store.indices)
+       assert.NotNil(t, store.indexers)
        assert.NotNil(t, store.stopCh)
 }
 
@@ -775,7 +776,7 @@ func TestGormStore_ListByIndexes(t *testing.T) {
        require.NoError(t, err)
 
        // List by indexes
-       indexes := map[string]string{"by-mesh": "mesh1"}
+       indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: 
"mesh1", Operator: index.Equals}}
        resources, err := store.ListByIndexes(indexes)
        assert.NoError(t, err)
        assert.Len(t, resources, 2)
@@ -802,7 +803,7 @@ func TestGormStore_ListByIndexesEmpty(t *testing.T) {
        require.NoError(t, err)
 
        // List with empty indexes should return all resources
-       resources, err := store.ListByIndexes(map[string]string{})
+       resources, err := store.ListByIndexes([]index.IndexCondition{})
        assert.NoError(t, err)
        assert.Len(t, resources, 1)
 }
@@ -855,7 +856,7 @@ func TestGormStore_PageListByIndexes(t *testing.T) {
        require.NoError(t, err)
 
        // Page list by indexes
-       indexes := map[string]string{"by-mesh": "mesh1"}
+       indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: 
"mesh1", Operator: index.Equals}}
        pageReq := model.PageReq{
                PageOffset: 0,
                PageSize:   2,
@@ -918,7 +919,7 @@ func TestGormStore_PageListByIndexesOffsetBeyondTotal(t 
*testing.T) {
        require.NoError(t, err)
 
        // Request page beyond total
-       indexes := map[string]string{"by-mesh": "default"}
+       indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: 
"default", Operator: index.Equals}}
        pageReq := model.PageReq{
                PageOffset: 10,
                PageSize:   2,
@@ -999,9 +1000,9 @@ func TestGormStore_MultipleIndexes(t *testing.T) {
        }
 
        // Test multiple indexes - get all resources in mesh1 and default 
namespace
-       indexes := map[string]string{
-               "by-mesh":      "mesh1",
-               "by-namespace": "default",
+       indexes := []index.IndexCondition{
+               {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals},
+               {IndexName: "by-namespace", Value: "default", Operator: 
index.Equals},
        }
        result, err := store.ListByIndexes(indexes)
        assert.NoError(t, err)
@@ -1238,79 +1239,6 @@ func TestGormStore_ReplaceIndices(t *testing.T) {
        assert.Contains(t, keys, "test-key-2")
 }
 
-func TestGormStore_InitRebuildIndices(t *testing.T) {
-       // This test verifies that indices are rebuilt from existing data 
during Init()
-       // Simulates the scenario where a GormStore starts with existing data 
in the database
-
-       // Create store and add data
-       store, cleanup := setupTestStore(t)
-       defer cleanup()
-
-       err := store.Init(nil)
-       require.NoError(t, err)
-
-       // Add indexer before adding data
-       indexers := map[string]cache.IndexFunc{
-               "by-mesh": func(obj interface{}) ([]string, error) {
-                       resource := obj.(model.Resource)
-                       return []string{resource.ResourceMesh()}, nil
-               },
-       }
-       err = store.AddIndexers(indexers)
-       require.NoError(t, err)
-
-       // Add some resources to the database
-       mockRes1 := &mockResource{
-               Kind: "TestResource",
-               Key:  "test-key-1",
-               Mesh: "mesh1",
-               Meta: metav1.ObjectMeta{Name: "test-resource-1"},
-       }
-       mockRes2 := &mockResource{
-               Kind: "TestResource",
-               Key:  "test-key-2",
-               Mesh: "mesh2",
-               Meta: metav1.ObjectMeta{Name: "test-resource-2"},
-       }
-       err = store.Add(mockRes1)
-       require.NoError(t, err)
-       err = store.Add(mockRes2)
-       require.NoError(t, err)
-
-       // Verify indices are populated
-       keys, err := store.IndexKeys("by-mesh", "mesh1")
-       assert.NoError(t, err)
-       assert.Contains(t, keys, "test-key-1")
-
-       // Now simulate a restart by creating a new store instance with the 
same pool
-       // This simulates the scenario where existing data exists in the 
database
-       pool := store.pool
-       pool.IncrementRef() // Increment ref count since we're creating another 
store using it
-
-       newStore := NewGormStore("TestResource", pool.Address(), pool)
-
-       // Add indexers BEFORE Init to ensure they're available during index 
rebuild
-       err = newStore.AddIndexers(indexers)
-       require.NoError(t, err)
-
-       // Init should rebuild indices from existing database data
-       err = newStore.Init(nil)
-       require.NoError(t, err)
-
-       // Verify indices were rebuilt with existing data
-       keys, err = newStore.IndexKeys("by-mesh", "mesh1")
-       assert.NoError(t, err)
-       assert.Contains(t, keys, "test-key-1", "Index should contain existing 
data after Init()")
-
-       keys, err = newStore.IndexKeys("by-mesh", "mesh2")
-       assert.NoError(t, err)
-       assert.Contains(t, keys, "test-key-2", "Index should contain existing 
data after Init()")
-
-       // Verify all keys are present
-       allKeys := newStore.ListKeys()
-       assert.Len(t, allKeys, 2)
-}
-
 func TestGormStore_Resync(t *testing.T) {
        store, cleanup := setupTestStore(t)
        defer cleanup()
@@ -1475,3 +1403,290 @@ func TestGormStore_InvalidResourceType(t *testing.T) {
        _, _, err = store.Get("not-a-resource")
        assert.Error(t, err)
 }
+
+func TestGormStore_IndexPersistence(t *testing.T) {
+       store, cleanup := setupTestStore(t)
+       defer cleanup()
+
+       err := store.Init(nil)
+       require.NoError(t, err)
+
+       // Add indexer for IP addresses
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       // Simulate an IP address from the resource key
+                       return []string{resource.ResourceKey()}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       require.NoError(t, err)
+
+       // Create and add resources with IP-like keys
+       mockRes1 := &mockResource{
+               Kind: "TestResource",
+               Key:  "192.168.1.1",
+               Mesh: "default",
+               Meta: metav1.ObjectMeta{Name: "resource-1"},
+       }
+       mockRes2 := &mockResource{
+               Kind: "TestResource",
+               Key:  "192.168.1.2",
+               Mesh: "default",
+               Meta: metav1.ObjectMeta{Name: "resource-2"},
+       }
+
+       err = store.Add(mockRes1)
+       require.NoError(t, err)
+       err = store.Add(mockRes2)
+       require.NoError(t, err)
+
+       // Verify indices are persisted to resource_indices table
+       db := store.pool.GetDB()
+       var count int64
+       err = db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND index_name = ?", "TestResource", 
"by-ip").
+               Count(&count).Error
+       assert.NoError(t, err)
+       assert.Equal(t, int64(2), count, "Both index entries should be 
persisted to resource_indices table")
+
+       // Verify operator field is set to "Equals"
+       var entries []ResourceIndexModel
+       err = db.Where("resource_kind = ? AND index_name = ?", "TestResource", 
"by-ip").
+               Find(&entries).Error
+       assert.NoError(t, err)
+       for _, entry := range entries {
+               assert.Equal(t, "Equals", entry.Operator, "Operator field 
should be set to Equals")
+       }
+}
+
+func TestGormStore_ListByIndexes_HasPrefix(t *testing.T) {
+       store, cleanup := setupTestStore(t)
+       defer cleanup()
+
+       err := store.Init(nil)
+       require.NoError(t, err)
+
+       // Add indexer for IP addresses
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       return []string{resource.ResourceKey()}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       require.NoError(t, err)
+
+       // Create resources with IP-like keys
+       mockRes1 := &mockResource{
+               Kind: "TestResource",
+               Key:  "192.168.1.1",
+               Mesh: "default",
+               Meta: metav1.ObjectMeta{Name: "resource-1"},
+       }
+       mockRes2 := &mockResource{
+               Kind: "TestResource",
+               Key:  "192.168.1.2",
+               Mesh: "default",
+               Meta: metav1.ObjectMeta{Name: "resource-2"},
+       }
+       mockRes3 := &mockResource{
+               Kind: "TestResource",
+               Key:  "10.0.0.1",
+               Mesh: "default",
+               Meta: metav1.ObjectMeta{Name: "resource-3"},
+       }
+
+       err = store.Add(mockRes1)
+       require.NoError(t, err)
+       err = store.Add(mockRes2)
+       require.NoError(t, err)
+       err = store.Add(mockRes3)
+       require.NoError(t, err)
+
+       // Query with HasPrefix operator
+       indexes := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168", Operator: 
index.HasPrefix},
+       }
+       resources, err := store.ListByIndexes(indexes)
+       assert.NoError(t, err)
+       assert.Len(t, resources, 2)
+
+       // Verify the correct resources were returned
+       keys := make([]string, len(resources))
+       for i, res := range resources {
+               keys[i] = res.ResourceKey()
+       }
+       assert.Contains(t, keys, "192.168.1.1")
+       assert.Contains(t, keys, "192.168.1.2")
+       assert.NotContains(t, keys, "10.0.0.1")
+}
+
+func TestGormStore_PageListByIndexes_HasPrefix(t *testing.T) {
+       store, cleanup := setupTestStore(t)
+       defer cleanup()
+
+       err := store.Init(nil)
+       require.NoError(t, err)
+
+       // Add indexer for IP addresses
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       return []string{resource.ResourceKey()}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       require.NoError(t, err)
+
+       // Create resources with IP-like keys
+       for i := 1; i <= 5; i++ {
+               mockRes := &mockResource{
+                       Kind: "TestResource",
+                       Key:  fmt.Sprintf("192.168.1.%d", i),
+                       Mesh: "default",
+                       Meta: metav1.ObjectMeta{Name: 
fmt.Sprintf("resource-%d", i)},
+               }
+               err = store.Add(mockRes)
+               require.NoError(t, err)
+       }
+
+       // Query with HasPrefix operator and pagination
+       indexes := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168", Operator: 
index.HasPrefix},
+       }
+       pageReq := model.PageReq{
+               PageOffset: 0,
+               PageSize:   2,
+       }
+       pageData, err := store.PageListByIndexes(indexes, pageReq)
+       assert.NoError(t, err)
+       assert.Equal(t, 5, pageData.Total)
+       assert.Len(t, pageData.Data, 2)
+
+       // Verify second page
+       pageReq.PageOffset = 2
+       pageData, err = store.PageListByIndexes(indexes, pageReq)
+       assert.NoError(t, err)
+       assert.Equal(t, 5, pageData.Total)
+       assert.Len(t, pageData.Data, 2)
+
+       // Verify last page
+       pageReq.PageOffset = 4
+       pageData, err = store.PageListByIndexes(indexes, pageReq)
+       assert.NoError(t, err)
+       assert.Equal(t, 5, pageData.Total)
+       assert.Len(t, pageData.Data, 1)
+}
+
+func TestGormStore_DeleteIndex_RemovesFromDB(t *testing.T) {
+       store, cleanup := setupTestStore(t)
+       defer cleanup()
+
+       err := store.Init(nil)
+       require.NoError(t, err)
+
+       // Add indexer
+       indexers := map[string]cache.IndexFunc{
+               "by-name": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       return []string{resource.ResourceMeta().Name}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       require.NoError(t, err)
+
+       // Create and add resource
+       mockRes := &mockResource{
+               Kind: "TestResource",
+               Key:  "test-key",
+               Mesh: "default",
+               Meta: metav1.ObjectMeta{Name: "test-resource"},
+       }
+       err = store.Add(mockRes)
+       require.NoError(t, err)
+
+       // Verify index entry exists in DB
+       db := store.pool.GetDB()
+       var count int64
+       err = db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND resource_key = ?", "TestResource", 
"test-key").
+               Count(&count).Error
+       assert.NoError(t, err)
+       assert.Greater(t, count, int64(0), "Index entry should exist after Add")
+
+       // Delete the resource
+       err = store.Delete(mockRes)
+       require.NoError(t, err)
+
+       // Verify index entry is removed from DB
+       count = 0
+       err = db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND resource_key = ?", "TestResource", 
"test-key").
+               Count(&count).Error
+       assert.NoError(t, err)
+       assert.Equal(t, int64(0), count, "Index entry should be removed after 
Delete")
+}
+
+func TestGormStore_UpdateIndex_UpdatesInDB(t *testing.T) {
+       store, cleanup := setupTestStore(t)
+       defer cleanup()
+
+       err := store.Init(nil)
+       require.NoError(t, err)
+
+       // Add indexer
+       indexers := map[string]cache.IndexFunc{
+               "by-mesh": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       return []string{resource.ResourceMesh()}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       require.NoError(t, err)
+
+       // Create and add resource
+       mockRes := &mockResource{
+               Kind: "TestResource",
+               Key:  "test-key",
+               Mesh: "mesh1",
+               Meta: metav1.ObjectMeta{Name: "test-resource"},
+       }
+       err = store.Add(mockRes)
+       require.NoError(t, err)
+
+       // Verify initial index entry in DB
+       db := store.pool.GetDB()
+       var count int64
+       err = db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND resource_key = ? AND index_value = 
?", "TestResource", "test-key", "mesh1").
+               Count(&count).Error
+       assert.NoError(t, err)
+       assert.Equal(t, int64(1), count, "Initial index entry should exist")
+
+       // Update resource with different mesh
+       updatedRes := &mockResource{
+               Kind: "TestResource",
+               Key:  "test-key",
+               Mesh: "mesh2",
+               Meta: metav1.ObjectMeta{Name: "test-resource"},
+       }
+       err = store.Update(updatedRes)
+       require.NoError(t, err)
+
+       // Verify old index entry is removed
+       count = 0
+       err = db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND resource_key = ? AND index_value = 
?", "TestResource", "test-key", "mesh1").
+               Count(&count).Error
+       assert.NoError(t, err)
+       assert.Equal(t, int64(0), count, "Old index entry should be removed")
+
+       // Verify new index entry exists
+       count = 0
+       err = db.Model(&ResourceIndexModel{}).
+               Where("resource_kind = ? AND resource_key = ? AND index_value = 
?", "TestResource", "test-key", "mesh2").
+               Count(&count).Error
+       assert.NoError(t, err)
+       assert.Equal(t, int64(1), count, "New index entry should exist")
+}
diff --git a/pkg/store/dbcommon/index.go b/pkg/store/dbcommon/index.go
deleted file mode 100644
index 5c535619..00000000
--- a/pkg/store/dbcommon/index.go
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package dbcommon
-
-import (
-       "fmt"
-       "sync"
-
-       set "github.com/duke-git/lancet/v2/datastructure/set"
-       "k8s.io/client-go/tools/cache"
-
-       "github.com/apache/dubbo-admin/pkg/core/resource/model"
-)
-
-// ValueIndex represents the mapping from indexed values to resource keys for 
a single index.
-// Structure: map[indexedValue]set[resourceKey]
-// Example: map["default"]{"resource1", "resource2", "resource3"}
-type ValueIndex struct {
-       values map[string]set.Set[string]
-}
-
-// NewValueIndex creates a new ValueIndex
-func NewValueIndex() *ValueIndex {
-       return &ValueIndex{
-               values: make(map[string]set.Set[string]),
-       }
-}
-
-// Add adds a resource key to the specified indexed value
-func (vi *ValueIndex) Add(indexedValue, resourceKey string) {
-       if vi.values[indexedValue] == nil {
-               vi.values[indexedValue] = set.New[string]()
-       }
-       vi.values[indexedValue].Add(resourceKey)
-}
-
-// Remove removes a resource key from the specified indexed value
-// Returns true if the value entry becomes empty after removal
-func (vi *ValueIndex) Remove(indexedValue, resourceKey string) bool {
-       if vi.values[indexedValue] == nil {
-               return false
-       }
-
-       vi.values[indexedValue].Delete(resourceKey)
-
-       // Check if the set is now empty
-       if vi.values[indexedValue].Size() == 0 {
-               delete(vi.values, indexedValue)
-               return true
-       }
-
-       return false
-}
-
-// GetKeys returns all resource keys for the specified indexed value
-func (vi *ValueIndex) GetKeys(indexedValue string) []string {
-       if vi.values[indexedValue] == nil {
-               return []string{}
-       }
-       return vi.values[indexedValue].ToSlice()
-}
-
-// GetAllValues returns all indexed values in this ValueIndex
-func (vi *ValueIndex) GetAllValues() []string {
-       if len(vi.values) == 0 {
-               return []string{}
-       }
-
-       result := make([]string, 0, len(vi.values))
-       for value := range vi.values {
-               result = append(result, value)
-       }
-       return result
-}
-
-// IsEmpty returns true if the ValueIndex has no entries
-func (vi *ValueIndex) IsEmpty() bool {
-       return len(vi.values) == 0
-}
-
-// Index is a thread-safe in-memory index structure that manages multiple 
named indices.
-// Each index maps values to sets of resource keys.
-//
-// Structure: map[indexName]*ValueIndex
-// Example: map["mesh"]*ValueIndex where ValueIndex contains {"default": 
{"res1", "res2"}}
-type Index struct {
-       mu       sync.RWMutex
-       indices  map[string]*ValueIndex // map[indexName]*ValueIndex
-       indexers cache.Indexers         // Index functions for creating indices
-}
-
-// NewIndex creates a new empty Index instance
-func NewIndex() *Index {
-       return &Index{
-               indices:  make(map[string]*ValueIndex),
-               indexers: cache.Indexers{},
-       }
-}
-
-// AddIndexers adds new indexer functions to the Index
-// Returns an error if an indexer with the same name already exists
-func (idx *Index) AddIndexers(newIndexers cache.Indexers) error {
-       idx.mu.Lock()
-       defer idx.mu.Unlock()
-
-       for name, indexFunc := range newIndexers {
-               if _, exists := idx.indexers[name]; exists {
-                       return fmt.Errorf("indexer %s already exists", name)
-               }
-               idx.indexers[name] = indexFunc
-       }
-
-       return nil
-}
-
-// GetIndexers returns a copy of all registered indexers
-func (idx *Index) GetIndexers() cache.Indexers {
-       idx.mu.RLock()
-       defer idx.mu.RUnlock()
-
-       result := make(cache.Indexers, len(idx.indexers))
-       for k, v := range idx.indexers {
-               result[k] = v
-       }
-       return result
-}
-
-// UpdateResource atomically updates all indices for a resource
-// If oldResource is nil, it's treated as an add operation
-// If oldResource is not nil, it's treated as an update operation (remove old, 
add new)
-// This is a high-level atomic operation that handles all indexers internally
-func (idx *Index) UpdateResource(newResource model.Resource, oldResource 
model.Resource) {
-       idx.mu.Lock()
-       defer idx.mu.Unlock()
-
-       // Remove old resource from indices if this is an update
-       if oldResource != nil {
-               idx.removeResourceUnsafe(oldResource)
-       }
-
-       // Add new resource to indices
-       idx.addResourceUnsafe(newResource)
-}
-
-// RemoveResource atomically removes a resource from all indices
-func (idx *Index) RemoveResource(resource model.Resource) {
-       idx.mu.Lock()
-       defer idx.mu.Unlock()
-
-       idx.removeResourceUnsafe(resource)
-}
-
-// GetKeys returns all resource keys for a given index name and value
-// Returns an empty slice if the index name or value doesn't exist
-func (idx *Index) GetKeys(indexName, indexValue string) []string {
-       idx.mu.RLock()
-       defer idx.mu.RUnlock()
-
-       valueIndex := idx.indices[indexName]
-       if valueIndex == nil {
-               return []string{}
-       }
-
-       return valueIndex.GetKeys(indexValue)
-}
-
-// ListIndexFuncValues returns all indexed values for a given index name
-// This directly retrieves values from the in-memory index without 
recalculating
-func (idx *Index) ListIndexFuncValues(indexName string) []string {
-       idx.mu.RLock()
-       defer idx.mu.RUnlock()
-
-       valueIndex := idx.indices[indexName]
-       if valueIndex == nil {
-               return []string{}
-       }
-
-       return valueIndex.GetAllValues()
-}
-
-// Clear removes all entries from the index
-func (idx *Index) Clear() {
-       idx.mu.Lock()
-       defer idx.mu.Unlock()
-       idx.indices = make(map[string]*ValueIndex)
-}
-
-// IndexExists checks if an indexer with the given name exists
-func (idx *Index) IndexExists(indexName string) bool {
-       idx.mu.RLock()
-       defer idx.mu.RUnlock()
-       _, exists := idx.indexers[indexName]
-       return exists
-}
-
-// addResourceUnsafe adds a resource to all indices (must be called with lock 
held)
-func (idx *Index) addResourceUnsafe(resource model.Resource) {
-       for indexName, indexFunc := range idx.indexers {
-               values, err := indexFunc(resource)
-               if err != nil {
-                       continue
-               }
-
-               // Ensure the ValueIndex exists for this index name
-               if idx.indices[indexName] == nil {
-                       idx.indices[indexName] = NewValueIndex()
-               }
-
-               // Add resource key to each indexed value
-               for _, value := range values {
-                       idx.indices[indexName].Add(value, 
resource.ResourceKey())
-               }
-       }
-}
-
-// removeResourceUnsafe removes a resource from all indices (must be called 
with lock held)
-func (idx *Index) removeResourceUnsafe(resource model.Resource) {
-       for indexName, indexFunc := range idx.indexers {
-               values, err := indexFunc(resource)
-               if err != nil {
-                       continue
-               }
-
-               valueIndex := idx.indices[indexName]
-               if valueIndex == nil {
-                       continue
-               }
-
-               // Remove resource key from each indexed value
-               for _, value := range values {
-                       valueIndex.Remove(value, resource.ResourceKey())
-               }
-
-               // Clean up empty ValueIndex
-               if valueIndex.IsEmpty() {
-                       delete(idx.indices, indexName)
-               }
-       }
-}
diff --git a/pkg/store/dbcommon/model.go b/pkg/store/dbcommon/model.go
index 5cf66c69..4bdecc13 100644
--- a/pkg/store/dbcommon/model.go
+++ b/pkg/store/dbcommon/model.go
@@ -123,3 +123,22 @@ func FromResource(resource model.Resource) 
(*ResourceModel, error) {
                Data:         data,
        }, nil
 }
+
+// ResourceIndexModel represents a persisted index entry in the database
+// This table stores index mappings to enable prefix queries and provide index 
persistence
+// across multiple replicas in distributed deployments
+// Table: resource_indices (shared across all resource kinds)
+type ResourceIndexModel struct {
+       ID           uint   `gorm:"primarykey"`                                 
          // Auto-incrementing primary key
+       ResourceKind string `gorm:"type:varchar(64);not 
null;index:idx_kind_name_value"`  // Resource kind (e.g., "Instance")
+       IndexName    string `gorm:"type:varchar(128);not 
null;index:idx_kind_name_value"` // Index name (e.g., "idx_instance_ip")
+       IndexValue   string `gorm:"type:varchar(255);not 
null;index:idx_kind_name_value"` // Indexed value (e.g., "192.168.1.1")
+       ResourceKey  string `gorm:"type:varchar(255);not 
null;index:idx_resource_key"`    // Resource unique key
+       Operator     string `gorm:"type:varchar(32);not null;default:Equals"`   
          // Index operator type, e.g., Equals, HasPrefix
+}
+
+// TableName specifies the table name for ResourceIndexModel
+// Unlike ResourceModel which uses dynamic per-kind tables, resource_indices 
is a shared global table
+func (ResourceIndexModel) TableName() string {
+       return "resource_indices"
+}
diff --git a/pkg/store/memory/store.go b/pkg/store/memory/store.go
index 28ce04d2..0b392bd5 100644
--- a/pkg/store/memory/store.go
+++ b/pkg/store/memory/store.go
@@ -18,9 +18,13 @@
 package memory
 
 import (
+       "fmt"
        "reflect"
        "sort"
+       "strings"
+       "sync"
 
+       "github.com/armon/go-radix"
        set "github.com/duke-git/lancet/v2/datastructure/set"
        "github.com/duke-git/lancet/v2/slice"
        "k8s.io/client-go/tools/cache"
@@ -33,8 +37,10 @@ import (
 )
 
 type resourceStore struct {
-       rk         coremodel.ResourceKind
-       storeProxy cache.Indexer
+       rk          coremodel.ResourceKind
+       storeProxy  cache.Indexer
+       prefixTrees map[string]*radix.Tree
+       treesMu     sync.RWMutex
 }
 
 var _ store.ManagedResourceStore = &resourceStore{}
@@ -55,6 +61,11 @@ func (rs *resourceStore) Init(_ runtime.BuilderContext) 
error {
                },
                indexers,
        )
+       // Initialize RadixTree for each index for prefix matching support
+       rs.prefixTrees = make(map[string]*radix.Tree)
+       for indexName := range indexers {
+               rs.prefixTrees[indexName] = radix.New()
+       }
        return nil
 }
 
@@ -63,15 +74,47 @@ func (rs *resourceStore) Start(_ runtime.Runtime, _ <-chan 
struct{}) error {
 }
 
 func (rs *resourceStore) Add(obj interface{}) error {
-       return rs.storeProxy.Add(obj)
+       if err := rs.storeProxy.Add(obj); err != nil {
+               return err
+       }
+       r, ok := obj.(coremodel.Resource)
+       if ok {
+               rs.addToTrees(r)
+       }
+       return nil
 }
 
 func (rs *resourceStore) Update(obj interface{}) error {
-       return rs.storeProxy.Update(obj)
+       r, ok := obj.(coremodel.Resource)
+       var oldRes coremodel.Resource
+       if ok {
+               // Fetch old resource before mutating the store
+               oldObj, exists, err := rs.storeProxy.Get(r)
+               if exists && err == nil {
+                       oldRes, _ = oldObj.(coremodel.Resource)
+               }
+       }
+       if err := rs.storeProxy.Update(obj); err != nil {
+               return err
+       }
+       // Only mutate trees after a successful store update
+       if ok {
+               if oldRes != nil {
+                       rs.removeFromTrees(oldRes)
+               }
+               rs.addToTrees(r)
+       }
+       return nil
 }
 
 func (rs *resourceStore) Delete(obj interface{}) error {
-       return rs.storeProxy.Delete(obj)
+       if err := rs.storeProxy.Delete(obj); err != nil {
+               return err
+       }
+       if r, ok := obj.(coremodel.Resource); ok {
+               rs.removeFromTrees(r)
+       }
+       return nil
 }
 
 func (rs *resourceStore) List() []interface{} {
@@ -91,7 +134,25 @@ func (rs *resourceStore) GetByKey(key string) (item 
interface{}, exists bool, er
 }
 
 func (rs *resourceStore) Replace(i []interface{}, s string) error {
-       return rs.storeProxy.Replace(i, s)
+       // Clear all trees before replace
+       rs.treesMu.Lock()
+       for indexName := range rs.prefixTrees {
+               rs.prefixTrees[indexName] = radix.New()
+       }
+       rs.treesMu.Unlock()
+
+       if err := rs.storeProxy.Replace(i, s); err != nil {
+               return err
+       }
+
+       // Add all new resources to trees
+       for _, obj := range i {
+               r, ok := obj.(coremodel.Resource)
+               if ok {
+                       rs.addToTrees(r)
+               }
+       }
+       return nil
 }
 
 func (rs *resourceStore) Resync() error {
@@ -119,7 +180,20 @@ func (rs *resourceStore) GetIndexers() cache.Indexers {
 }
 
 func (rs *resourceStore) AddIndexers(newIndexers cache.Indexers) error {
-       return rs.storeProxy.AddIndexers(newIndexers)
+       rs.treesMu.Lock()
+       defer rs.treesMu.Unlock()
+
+       if err := rs.storeProxy.AddIndexers(newIndexers); err != nil {
+               return err
+       }
+
+       // Add RadixTrees for new indexers
+       for indexName := range newIndexers {
+               if _, exists := rs.prefixTrees[indexName]; !exists {
+                       rs.prefixTrees[indexName] = radix.New()
+               }
+       }
+       return nil
 }
 
 func (rs *resourceStore) GetByKeys(keys []string) ([]coremodel.Resource, 
error) {
@@ -141,7 +215,7 @@ func (rs *resourceStore) GetByKeys(keys []string) 
([]coremodel.Resource, error)
        return resources, nil
 }
 
-func (rs *resourceStore) ListByIndexes(indexes map[string]string) 
([]coremodel.Resource, error) {
+func (rs *resourceStore) ListByIndexes(indexes []index.IndexCondition) 
([]coremodel.Resource, error) {
        keys, err := rs.getKeysByIndexes(indexes)
        if err != nil {
                return nil, err
@@ -156,7 +230,7 @@ func (rs *resourceStore) ListByIndexes(indexes 
map[string]string) ([]coremodel.R
        return resources, nil
 }
 
-func (rs *resourceStore) PageListByIndexes(indexes map[string]string, pq 
coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) {
+func (rs *resourceStore) PageListByIndexes(indexes []index.IndexCondition, pq 
coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) {
        keys, err := rs.getKeysByIndexes(indexes)
        if err != nil {
                return nil, err
@@ -183,17 +257,29 @@ func (rs *resourceStore) PageListByIndexes(indexes 
map[string]string, pq coremod
        return pageData, nil
 }
 
-func (rs *resourceStore) getKeysByIndexes(indexes map[string]string) 
([]string, error) {
+func (rs *resourceStore) getKeysByIndexes(indexes []index.IndexCondition) 
([]string, error) {
        if len(indexes) == 0 {
                return []string{}, nil
        }
        keySet := set.New[string]()
        first := true
-       for indexName, indexValue := range indexes {
-               keys, err := rs.storeProxy.IndexKeys(indexName, indexValue)
+       for _, condition := range indexes {
+               var keys []string
+               var err error
+
+               switch condition.Operator {
+               case index.Equals:
+                       keys, err = 
rs.storeProxy.IndexKeys(condition.IndexName, condition.Value)
+               case index.HasPrefix:
+                       keys, err = rs.getKeysByPrefix(condition.IndexName, 
condition.Value)
+               default:
+                       return nil, bizerror.New(bizerror.InvalidArgument, 
"operator not yet supported: "+string(condition.Operator))
+               }
+
                if err != nil {
                        return nil, err
                }
+
                if first {
                        keySet = set.FromSlice(keys)
                        first = false
@@ -204,3 +290,78 @@ func (rs *resourceStore) getKeysByIndexes(indexes 
map[string]string) ([]string,
        }
        return keySet.ToSlice(), nil
 }
+
+// addToTrees adds a resource to all relevant RadixTrees for prefix matching
+func (rs *resourceStore) addToTrees(resource coremodel.Resource) {
+       rs.treesMu.Lock()
+       defer rs.treesMu.Unlock()
+
+       // Get indexers from storeProxy, not from global registry
+       // This ensures we include both init-time and dynamically-added indexers
+       indexers := rs.storeProxy.GetIndexers()
+       for indexName, indexFunc := range indexers {
+               values, err := indexFunc(resource)
+               if err != nil {
+                       continue
+               }
+               tree, ok := rs.prefixTrees[indexName]
+               if !ok || tree == nil {
+                       continue
+               }
+               for _, v := range values {
+                       // Key format: "indexValue/resourceKey"
+                       key := v + "/" + resource.ResourceKey()
+                       tree.Insert(key, struct{}{})
+               }
+       }
+}
+
+// removeFromTrees removes a resource from all relevant RadixTrees
+func (rs *resourceStore) removeFromTrees(resource coremodel.Resource) {
+       rs.treesMu.Lock()
+       defer rs.treesMu.Unlock()
+
+       // Get indexers from storeProxy, not from global registry
+       // This ensures we include both init-time and dynamically-added indexers
+       indexers := rs.storeProxy.GetIndexers()
+       for indexName, indexFunc := range indexers {
+               values, err := indexFunc(resource)
+               if err != nil {
+                       continue
+               }
+               tree, ok := rs.prefixTrees[indexName]
+               if !ok || tree == nil {
+                       continue
+               }
+               for _, v := range values {
+                       // Key format: "indexValue/resourceKey"
+                       key := v + "/" + resource.ResourceKey()
+                       tree.Delete(key)
+               }
+       }
+}
+
+// getKeysByPrefix retrieves resource keys by prefix match using RadixTree
+func (rs *resourceStore) getKeysByPrefix(indexName, prefix string) ([]string, 
error) {
+       rs.treesMu.RLock()
+       defer rs.treesMu.RUnlock()
+
+       tree, ok := rs.prefixTrees[indexName]
+       if !ok {
+               return nil, fmt.Errorf("index %s does not exist", indexName)
+       }
+
+       var keys []string
+       tree.WalkPrefix(prefix, func(k string, v interface{}) bool {
+               // Key format: "indexValue/resourceKey"
+               // Key format: "indexValue/resourceKey"
+               // Use Index (first "/") because resourceKey itself contains 
"/" (mesh/name)
+               idx := strings.Index(k, "/")
+               if idx >= 0 && idx < len(k)-1 {
+                       keys = append(keys, k[idx+1:])
+               }
+               return false // Continue walking
+       })
+
+       return keys, nil
+}
diff --git a/pkg/store/memory/store_test.go b/pkg/store/memory/store_test.go
index 8d1f1104..8ad40199 100644
--- a/pkg/store/memory/store_test.go
+++ b/pkg/store/memory/store_test.go
@@ -28,6 +28,7 @@ import (
        "k8s.io/client-go/tools/cache"
 
        "github.com/apache/dubbo-admin/pkg/core/resource/model"
+       "github.com/apache/dubbo-admin/pkg/core/store/index"
 )
 
 // mockResource is a mock implementation of model.Resource for testing
@@ -377,7 +378,7 @@ func TestResourceStore_ListByIndexes(t *testing.T) {
        assert.NoError(t, err)
 
        // List by indexes
-       indexes := map[string]string{"by-mesh": "mesh1"}
+       indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: 
"mesh1", Operator: index.Equals}}
        resources, err := store.ListByIndexes(indexes)
        assert.NoError(t, err)
        assert.Len(t, resources, 2)
@@ -432,7 +433,7 @@ func TestResourceStore_PageListByIndexes(t *testing.T) {
        assert.NoError(t, err)
 
        // Page list by indexes
-       indexes := map[string]string{"by-mesh": "mesh1"}
+       indexes := []index.IndexCondition{{IndexName: "by-mesh", Value: 
"mesh1", Operator: index.Equals}}
        pageReq := model.PageReq{
                PageOffset: 0,
                PageSize:   2,
@@ -543,9 +544,9 @@ func TestResourceStore_MultipleIndexes(t *testing.T) {
        }
 
        // Test multiple indexes - get all prod env resources in mesh1
-       indexes := map[string]string{
-               "by-mesh":    "mesh1",
-               "by-version": "v1",
+       indexes := []index.IndexCondition{
+               {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals},
+               {IndexName: "by-version", Value: "v1", Operator: index.Equals},
        }
        result, err := store.ListByIndexes(indexes)
        assert.NoError(t, err)
@@ -784,3 +785,425 @@ func TestResourceStore_ListIndexFuncValues(t *testing.T) {
        assert.Contains(t, values, "active")
        assert.Contains(t, values, "inactive")
 }
+
+func TestResourceStore_HasPrefixMatch(t *testing.T) {
+       store := NewMemoryResourceStore("TestInstance")
+       err := store.Init(nil)
+       assert.NoError(t, err)
+
+       // Create mock resources with IP-like values for prefix matching
+       mockRes1 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-1",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-1",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.10",
+                       },
+               },
+       }
+
+       mockRes2 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-2",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-2",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.20",
+                       },
+               },
+       }
+
+       mockRes3 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-3",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-3",
+                       Labels: map[string]string{
+                               "ip": "10.0.0.5",
+                       },
+               },
+       }
+
+       mockRes4 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-4",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-4",
+                       Labels: map[string]string{
+                               "ip": "192.168.2.1",
+                       },
+               },
+       }
+
+       // Add indexer
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       ip := resource.ResourceMeta().Labels["ip"]
+                       return []string{ip}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       assert.NoError(t, err)
+
+       // Add resources
+       err = store.Add(mockRes1)
+       assert.NoError(t, err)
+       err = store.Add(mockRes2)
+       assert.NoError(t, err)
+       err = store.Add(mockRes3)
+       assert.NoError(t, err)
+       err = store.Add(mockRes4)
+       assert.NoError(t, err)
+
+       // Test HasPrefix match: find all IPs starting with "192.168.1."
+       conditions := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168.1.", Operator: 
index.HasPrefix},
+       }
+       result, err := store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 2)
+
+       // Verify the correct instances are returned
+       keys := make([]string, len(result))
+       for i, res := range result {
+               keys[i] = res.ResourceKey()
+       }
+       assert.Contains(t, keys, "instance-1")
+       assert.Contains(t, keys, "instance-2")
+
+       // Test HasPrefix match: find all IPs starting with "192.168."
+       conditions = []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168.", Operator: 
index.HasPrefix},
+       }
+       result, err = store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 3)
+
+       keys = make([]string, len(result))
+       for i, res := range result {
+               keys[i] = res.ResourceKey()
+       }
+       assert.Contains(t, keys, "instance-1")
+       assert.Contains(t, keys, "instance-2")
+       assert.Contains(t, keys, "instance-4")
+
+       // Test HasPrefix match: find all IPs starting with "10."
+       conditions = []index.IndexCondition{
+               {IndexName: "by-ip", Value: "10.", Operator: index.HasPrefix},
+       }
+       result, err = store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 1)
+       assert.Equal(t, "instance-3", result[0].ResourceKey())
+}
+
+func TestResourceStore_HasPrefixPageList(t *testing.T) {
+       store := NewMemoryResourceStore("TestInstance")
+       err := store.Init(nil)
+       assert.NoError(t, err)
+
+       // Create mock resources
+       mockRes1 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-1",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-1",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.10",
+                       },
+               },
+       }
+
+       mockRes2 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-2",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-2",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.20",
+                       },
+               },
+       }
+
+       mockRes3 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-3",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-3",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.30",
+                       },
+               },
+       }
+
+       // Add indexer
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       ip := resource.ResourceMeta().Labels["ip"]
+                       return []string{ip}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       assert.NoError(t, err)
+
+       // Add resources
+       err = store.Add(mockRes1)
+       assert.NoError(t, err)
+       err = store.Add(mockRes2)
+       assert.NoError(t, err)
+       err = store.Add(mockRes3)
+       assert.NoError(t, err)
+
+       // Test HasPrefix with pagination
+       conditions := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168.1.", Operator: 
index.HasPrefix},
+       }
+       pageReq := model.PageReq{
+               PageOffset: 0,
+               PageSize:   2,
+       }
+
+       pageData, err := store.PageListByIndexes(conditions, pageReq)
+       assert.NoError(t, err)
+       assert.Equal(t, 3, pageData.Total)
+       assert.Equal(t, 0, pageData.PageOffset)
+       assert.Equal(t, 2, pageData.PageSize)
+       assert.Len(t, pageData.Data, 2)
+
+       // Test second page
+       pageReq.PageOffset = 2
+       pageData, err = store.PageListByIndexes(conditions, pageReq)
+       assert.NoError(t, err)
+       assert.Equal(t, 3, pageData.Total)
+       assert.Equal(t, 2, pageData.PageOffset)
+       assert.Len(t, pageData.Data, 1)
+}
+
+func TestResourceStore_HasPrefixWithMultipleConditions(t *testing.T) {
+       store := NewMemoryResourceStore("TestInstance")
+       err := store.Init(nil)
+       assert.NoError(t, err)
+
+       // Create mock resources
+       mockRes1 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-1",
+               mesh: "mesh1",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-1",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.10",
+                       },
+               },
+       }
+
+       mockRes2 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-2",
+               mesh: "mesh1",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-2",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.20",
+                       },
+               },
+       }
+
+       mockRes3 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-3",
+               mesh: "mesh2",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-3",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.30",
+                       },
+               },
+       }
+
+       // Add indexers
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       ip := resource.ResourceMeta().Labels["ip"]
+                       return []string{ip}, nil
+               },
+               "by-mesh": func(obj interface{}) ([]string, error) {
+                       return []string{obj.(model.Resource).ResourceMesh()}, 
nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       assert.NoError(t, err)
+
+       // Add resources
+       err = store.Add(mockRes1)
+       assert.NoError(t, err)
+       err = store.Add(mockRes2)
+       assert.NoError(t, err)
+       err = store.Add(mockRes3)
+       assert.NoError(t, err)
+
+       // Test combined: HasPrefix on IP AND Equals on mesh
+       conditions := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168.1.", Operator: 
index.HasPrefix},
+               {IndexName: "by-mesh", Value: "mesh1", Operator: index.Equals},
+       }
+       result, err := store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 2)
+
+       keys := make([]string, len(result))
+       for i, res := range result {
+               keys[i] = res.ResourceKey()
+       }
+       assert.Contains(t, keys, "instance-1")
+       assert.Contains(t, keys, "instance-2")
+       assert.NotContains(t, keys, "instance-3")
+}
+
+func TestResourceStore_HasPrefixAfterUpdate(t *testing.T) {
+       store := NewMemoryResourceStore("TestInstance")
+       err := store.Init(nil)
+       assert.NoError(t, err)
+
+       // Create initial mock resource
+       mockRes1 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-1",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-1",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.10",
+                       },
+               },
+       }
+
+       // Add indexer
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       ip := resource.ResourceMeta().Labels["ip"]
+                       return []string{ip}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       assert.NoError(t, err)
+
+       // Add resource
+       err = store.Add(mockRes1)
+       assert.NoError(t, err)
+
+       // Verify it matches 192.168.1.
+       conditions := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168.1.", Operator: 
index.HasPrefix},
+       }
+       result, err := store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 1)
+
+       // Update resource with different IP
+       updatedRes := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-1",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-1",
+                       Labels: map[string]string{
+                               "ip": "10.0.0.5",
+                       },
+               },
+       }
+       err = store.Update(updatedRes)
+       assert.NoError(t, err)
+
+       // Verify it no longer matches 192.168.1.
+       result, err = store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 0)
+
+       // Verify it matches 10.0.
+       conditions = []index.IndexCondition{
+               {IndexName: "by-ip", Value: "10.0.", Operator: index.HasPrefix},
+       }
+       result, err = store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 1)
+       assert.Equal(t, "instance-1", result[0].ResourceKey())
+}
+
+func TestResourceStore_HasPrefixAfterDelete(t *testing.T) {
+       store := NewMemoryResourceStore("TestInstance")
+       err := store.Init(nil)
+       assert.NoError(t, err)
+
+       // Create mock resources
+       mockRes1 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-1",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-1",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.10",
+                       },
+               },
+       }
+
+       mockRes2 := &mockResource{
+               kind: "TestInstance",
+               key:  "instance-2",
+               mesh: "default",
+               meta: metav1.ObjectMeta{
+                       Name: "instance-2",
+                       Labels: map[string]string{
+                               "ip": "192.168.1.20",
+                       },
+               },
+       }
+
+       // Add indexer
+       indexers := map[string]cache.IndexFunc{
+               "by-ip": func(obj interface{}) ([]string, error) {
+                       resource := obj.(model.Resource)
+                       ip := resource.ResourceMeta().Labels["ip"]
+                       return []string{ip}, nil
+               },
+       }
+       err = store.AddIndexers(indexers)
+       assert.NoError(t, err)
+
+       // Add resources
+       err = store.Add(mockRes1)
+       assert.NoError(t, err)
+       err = store.Add(mockRes2)
+       assert.NoError(t, err)
+
+       // Verify both match 192.168.1.
+       conditions := []index.IndexCondition{
+               {IndexName: "by-ip", Value: "192.168.1.", Operator: 
index.HasPrefix},
+       }
+       result, err := store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 2)
+
+       // Delete one resource
+       err = store.Delete(mockRes1)
+       assert.NoError(t, err)
+
+       // Verify only one still matches
+       result, err = store.ListByIndexes(conditions)
+       assert.NoError(t, err)
+       assert.Len(t, result, 1)
+       assert.Equal(t, "instance-2", result[0].ResourceKey())
+}


Reply via email to