fix(search): take a deleted space out of the index when it is deleted

This commit is contained in:
Florian Schade committed 2026-08-19 13:47:44 +02:00
1 parent 5e55b80e2c
commit 45403e0909
10 files changed
+277 -3

No files matched your search

+25
View File
@@ -225,6 +225,31 @@ func (b *Backend) Purge(id string, onlyDeleted bool) error {
return batch.Push()
}
func (b *Backend) PurgeSpace(rootID string) error {
for {
req := bleve.NewSearchRequest(&query.TermQuery{FieldVal: "RootID", Term: rootID})
req.Size = defaultBatchSize
res, err := b.index.Search(req)
if err != nil {
return err
}
if res.Hits.Len() == 0 {
return nil
}
batch := b.index.NewBatch()
for _, hit := range res.Hits {
batch.Delete(hit.ID)
}
if err := b.index.Batch(batch); err != nil {
return err
}
}
}
func (b *Backend) NewBatch(size int) (search.BatchOperator, error) {
return NewBatch(b.index, size)
}
+46
View File
@@ -103,6 +103,52 @@ var _ = Describe("Bleve", func() {
}
})
Describe("PurgeSpace", func() {
It("takes every record of that space out of the index", func() {
otherSpace := search.Resource{
ID: "1$9!9",
RootID: "1$9!9",
Path: ".",
Document: content.Document{Name: "other"},
}
for _, resource := range []search.Resource{rootResource, parentResource, childResource, otherSpace} {
Expect(eng.Upsert(resource.ID, resource)).To(Succeed())
}
Expect(eng.PurgeSpace(rootResource.RootID)).To(Succeed())
count, err := idx.DocCount()
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(uint64(1)), "only the records of that space are gone")
})
It("takes a space out that holds more records than one round", func() {
otherSpace := search.Resource{
ID: "1$9!9",
RootID: "1$9!9",
Path: ".",
Document: content.Document{Name: "other"},
}
Expect(eng.Upsert(otherSpace.ID, otherSpace)).To(Succeed())
for i := range 120 {
resource := search.Resource{
ID: fmt.Sprintf("%s!file-%d", rootResource.RootID, i),
RootID: rootResource.RootID,
Path: fmt.Sprintf("./file-%d", i),
Document: content.Document{Name: fmt.Sprintf("file-%d", i)},
}
Expect(eng.Upsert(resource.ID, resource)).To(Succeed())
}
Expect(eng.PurgeSpace(rootResource.RootID)).To(Succeed())
count, err := idx.DocCount()
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(uint64(1)), "only the record of the other space is left")
})
})
Describe("New", func() {
It("returns a new index instance", func() {
b := bleve.NewBackend(idx, bleveQuery.DefaultCreator, log.Logger{})
+26
View File
@@ -246,6 +246,32 @@ func (b *Backend) Purge(id string, onlyDeleted bool) error {
return batch.Push()
}
func (b *Backend) PurgeSpace(rootID string) error {
req, err := osu.BuildDocumentDeleteByQueryReq(
opensearchgoAPI.DocumentDeleteByQueryReq{
Indices: []string{b.index},
Params: opensearchgoAPI.DocumentDeleteByQueryParams{
WaitForCompletion: conversions.ToPointer(true),
Refresh: conversions.ToPointer(true),
},
},
osu.NewBoolQuery().Must(osu.NewTermQuery[string]("RootID").Value(rootID)),
)
if err != nil {
return fmt.Errorf("failed to build the space purge request %s: %w", rootID, err)
}
resp, err := b.client.Document.DeleteByQuery(context.TODO(), req)
switch {
case err != nil:
return fmt.Errorf("failed to purge space %s: %w", rootID, err)
case len(resp.Failures) != 0:
return fmt.Errorf("failed to purge space %s: %v", rootID, resp.Failures)
}
return nil
}
func (b *Backend) NewBatch(size int) (search.BatchOperator, error) {
return NewBatch(b.client, b.index, size)
}
@@ -355,6 +355,48 @@ var _ = Describe("Backend", func() {
})
})
Describe("PurgeSpace", func() {
const indexName = "opencloud-test-engine-purge-space"
var (
tc *opensearchtest.TestClient
backend *opensearch.Backend
)
BeforeEach(func() {
tc = opensearchtest.NewDefaultTestClient(GinkgoTB(), defaultConfig.Engine.OpenSearch.Client)
tc.Require.IndicesReset([]string{indexName})
tc.Require.IndicesCount([]string{indexName}, nil, 0)
deleteIndexOnCleanup(tc, indexName)
var err error
backend, err = opensearch.NewBackend(indexName, tc.Client())
Expect(err).ToNot(HaveOccurred())
})
It("takes every record of that space out of the index", func() {
gone := opensearchtest.Testdata.Resources.File
tc.Require.DocumentCreate(indexName, gone.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), gone)))
stays := opensearchtest.Testdata.Resources.File
stays.ID = "1$2!3"
stays.RootID = "1$2!2"
tc.Require.DocumentCreate(indexName, stays.ID, strings.NewReader(opensearchtest.JSONMustMarshal(GinkgoTB(), stays)))
tc.Require.IndicesCount([]string{indexName}, nil, 2)
Expect(backend.PurgeSpace(gone.RootID)).To(Succeed())
tc.Require.IndicesRefresh([]string{indexName}, nil)
left := opensearchtest.SearchHitsMustBeConverted[search.Resource](
GinkgoTB(),
tc.Require.Search(indexName, strings.NewReader(`{"query":{"match_all":{}}}`)).Hits,
)
Expect(left).To(HaveLen(1), "only the records of that space are gone")
Expect(left[0].ID).To(Equal(stays.ID))
})
})
Describe("DocCount", func() {
const indexName = "opencloud-test-engine-doc-count"
@@ -325,6 +325,57 @@ func (_c *Engine_Purge_Call) RunAndReturn(run func(id string, onlyDeleted bool)
return _c
}
// PurgeSpace provides a mock function for the type Engine
func (_mock *Engine) PurgeSpace(rootID string) error {
ret := _mock.Called(rootID)
if len(ret) == 0 {
panic("no return value specified for PurgeSpace")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(rootID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Engine_PurgeSpace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PurgeSpace'
type Engine_PurgeSpace_Call struct {
*mock.Call
}
// PurgeSpace is a helper method to define mock.On call
// - rootID string
func (_e *Engine_Expecter) PurgeSpace(rootID interface{}) *Engine_PurgeSpace_Call {
return &Engine_PurgeSpace_Call{Call: _e.mock.On("PurgeSpace", rootID)}
}
func (_c *Engine_PurgeSpace_Call) Run(run func(rootID string)) *Engine_PurgeSpace_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
})
return _c
}
func (_c *Engine_PurgeSpace_Call) Return(err error) *Engine_PurgeSpace_Call {
_c.Call.Return(err)
return _c
}
func (_c *Engine_PurgeSpace_Call) RunAndReturn(run func(rootID string) error) *Engine_PurgeSpace_Call {
_c.Call.Return(run)
return _c
}
// Restore provides a mock function for the type Engine
func (_mock *Engine) Restore(id string) error {
ret := _mock.Called(id)
@@ -227,6 +227,57 @@ func (_c *Searcher_PurgeItem_Call) RunAndReturn(run func(ref *providerv1beta1.Re
return _c
}
// PurgeSpace provides a mock function for the type Searcher
func (_mock *Searcher) PurgeSpace(spaceID *providerv1beta1.StorageSpaceId) error {
ret := _mock.Called(spaceID)
if len(ret) == 0 {
panic("no return value specified for PurgeSpace")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(*providerv1beta1.StorageSpaceId) error); ok {
r0 = returnFunc(spaceID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Searcher_PurgeSpace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PurgeSpace'
type Searcher_PurgeSpace_Call struct {
*mock.Call
}
// PurgeSpace is a helper method to define mock.On call
// - spaceID *providerv1beta1.StorageSpaceId
func (_e *Searcher_Expecter) PurgeSpace(spaceID interface{}) *Searcher_PurgeSpace_Call {
return &Searcher_PurgeSpace_Call{Call: _e.mock.On("PurgeSpace", spaceID)}
}
func (_c *Searcher_PurgeSpace_Call) Run(run func(spaceID *providerv1beta1.StorageSpaceId)) *Searcher_PurgeSpace_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.StorageSpaceId
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.StorageSpaceId)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_PurgeSpace_Call) Return(err error) *Searcher_PurgeSpace_Call {
_c.Call.Return(err)
return _c
}
func (_c *Searcher_PurgeSpace_Call) RunAndReturn(run func(spaceID *providerv1beta1.StorageSpaceId) error) *Searcher_PurgeSpace_Call {
_c.Call.Return(run)
return _c
}
// RestoreItem provides a mock function for the type Searcher
func (_mock *Searcher) RestoreItem(ref *providerv1beta1.Reference) {
_mock.Called(ref)
+1
View File
@@ -33,6 +33,7 @@ type Engine interface {
Delete(id string) error
Restore(id string) error
Purge(id string, onlyDeleted bool) error
PurgeSpace(rootID string) error
NewBatch(batchSize int) (BatchOperator, error)
}
+26
View File
@@ -48,6 +48,7 @@ type Searcher interface {
Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error)
IndexSpace(spaceID *provider.StorageSpaceId, forceRescan bool) error
PurgeSpace(spaceID *provider.StorageSpaceId) error
PurgeDeleted(spaceID *provider.StorageSpaceId) error
TrashItem(resourceID *provider.ResourceId)
@@ -561,6 +562,31 @@ func (s *Service) PurgeItem(ref *provider.Reference) {
logDocCount(s.engine, s.logger)
}
func (s *Service) PurgeSpace(spaceID *provider.StorageSpaceId) error {
if spaceID == nil {
return fmt.Errorf("spaceID must not be nil")
}
rootID, err := storagespace.ParseID(spaceID.GetOpaqueId())
if err != nil {
s.logger.Error().Err(err).Str("space_id", spaceID.GetOpaqueId()).Msg("invalid space id")
return err
}
if rootID.StorageId == "" || rootID.SpaceId == "" {
return fmt.Errorf("invalid space id %s", spaceID.GetOpaqueId())
}
rootID.OpaqueId = rootID.SpaceId
if err := s.engine.PurgeSpace(storagespace.FormatResourceID(&rootID)); err != nil {
s.logger.Error().Err(err).Str("space_id", spaceID.GetOpaqueId()).Msg("failed to purge the space from the index")
return err
}
logDocCount(s.engine, s.logger)
return nil
}
func (s *Service) PurgeDeleted(spaceID *provider.StorageSpaceId) error {
if spaceID == nil {
return fmt.Errorf("spaceID must not be nil")
+8 -3
View File
@@ -7,14 +7,15 @@ import (
"time"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/search/pkg/metrics"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/raw"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/search/pkg/metrics"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
var tracer trace.Tracer
@@ -61,6 +62,7 @@ func New(ctx context.Context, stream raw.Stream, logger log.Logger, tp trace.Tra
events.TagsAdded{},
events.TagsRemoved{},
events.SpaceRenamed{},
events.SpaceDeleted{},
events.LabelAdded{},
events.LabelRemoved{},
},
@@ -200,6 +202,9 @@ func (s Service) processEvent(e raw.Event) error {
s.indexSpaceDebouncer.Debounce(getSpaceID(ev.FileRef), e.Ack)
case events.SpaceRenamed:
s.indexSpaceDebouncer.Debounce(ev.ID, e.Ack)
case events.SpaceDeleted:
s.index.PurgeSpace(ev.ID)
e.Ack()
case events.LabelAdded:
s.index.UpsertItem(ev.Ref)
case events.LabelRemoved:
@@ -50,6 +50,7 @@ var _ = DescribeTable("event",
return int(calls.Load())
}, "2s").Should(Equal(len(mcks)))
},
Entry("SpaceDeleted", []string{"PurgeSpace"}, events.SpaceDeleted{}, false),
Entry("ItemTrashed", []string{"TrashItem", "IndexSpace"}, events.ItemTrashed{}, false),
Entry("ItemMoved", []string{"MoveItem", "IndexSpace"}, events.ItemMoved{}, false),
Entry("ItemRestored", []string{"RestoreItem", "IndexSpace"}, events.ItemRestored{}, false),