mirror of
https://github.com/owncloud/ocis.git
synced 2026-10-23 23:29:02 +03:00
Add a CLI-only optimize command that compacts the Bleve search index by merging segments. The command opens the index directly via a new engine factory (NewEngineFromConfig), without requiring the running gRPC service. Key changes per review feedback from jvillafanez: - CLI-only: no gRPC endpoint, no proto changes — admin triggers it directly when disruption is acceptable - Engine factory: extracted engine creation from the gRPC handler into NewEngineFromConfig, reusable by both the service and CLI commands - Optimize() merged into Engine interface: no separate Optimizer interface, non-supporting engines can return an error Signed-off-by: Paul Faure <paul@faure.ca> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Martin <github@diemattels.at>
140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
|
|
"github.com/blevesearch/bleve/v2/search"
|
|
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
|
|
|
searchMessage "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
|
|
searchService "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
|
bleveEngine "github.com/owncloud/ocis/v2/services/search/pkg/engine/bleve"
|
|
"github.com/owncloud/ocis/v2/services/search/pkg/config"
|
|
"github.com/owncloud/ocis/v2/services/search/pkg/content"
|
|
"github.com/owncloud/ocis/v2/services/search/pkg/query/bleve"
|
|
)
|
|
|
|
// ErrResourceNotFound is returned when a resource is not present in the index.
|
|
var ErrResourceNotFound = errors.New("entity not found")
|
|
|
|
var queryEscape = regexp.MustCompile(`([` + regexp.QuoteMeta(`+=&|><!(){}[]^\"~*?:\/`) + `\-\s])`)
|
|
|
|
// Engine is the interface to the search engine
|
|
type Engine interface {
|
|
Search(ctx context.Context, req *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error)
|
|
Upsert(id string, r Resource) error
|
|
Update(id string, mutateFn func(*Resource)) error
|
|
Lookup(id string) (*Resource, error)
|
|
Move(id string, parentid string, target string) error
|
|
Delete(id string) error
|
|
Restore(id string) error
|
|
Purge(id string) error
|
|
DocCount() (uint64, error)
|
|
Optimize(ctx context.Context) error
|
|
}
|
|
|
|
// NewEngineFromConfig creates an Engine from the search service configuration.
|
|
// The returned io.Closer must be called to release the underlying index
|
|
// resources. This factory is used by CLI commands that need direct engine
|
|
// access without starting the full gRPC service.
|
|
func NewEngineFromConfig(cfg *config.Config) (Engine, io.Closer, error) {
|
|
switch cfg.Engine.Type {
|
|
case "bleve":
|
|
bleveMapping, err := BuildBleveMapping()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var indexGetter bleveEngine.IndexGetter
|
|
indexGetter = bleveEngine.NewIndexGetterPersistent(cfg.Engine.Bleve.Datapath, bleveMapping)
|
|
if cfg.Engine.Bleve.Scale {
|
|
indexGetter = bleveEngine.NewIndexGetterPersistentScale(cfg.Engine.Bleve.Datapath, bleveMapping)
|
|
}
|
|
|
|
eng := NewBleveEngine(indexGetter, bleve.DefaultCreator)
|
|
return eng, eng, nil
|
|
|
|
default:
|
|
return nil, nil, fmt.Errorf("unknown search engine: %s", cfg.Engine.Type)
|
|
}
|
|
}
|
|
|
|
// Resource is the entity that is stored in the index.
|
|
type Resource struct {
|
|
content.Document
|
|
|
|
ID string
|
|
RootID string
|
|
Path string
|
|
ParentID string
|
|
Type uint64
|
|
Deleted bool
|
|
Hidden bool
|
|
Extracted bool
|
|
}
|
|
|
|
func resourceIDtoSearchID(id storageProvider.ResourceId) *searchMessage.ResourceID {
|
|
return &searchMessage.ResourceID{
|
|
StorageId: id.GetStorageId(),
|
|
SpaceId: id.GetSpaceId(),
|
|
OpaqueId: id.GetOpaqueId()}
|
|
}
|
|
|
|
func escapeQuery(s string) string {
|
|
return queryEscape.ReplaceAllString(s, "\\$1")
|
|
}
|
|
|
|
func getFragmentValue(m search.FieldFragmentMap, key string, idx int) string {
|
|
val, ok := m[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
if len(val) <= idx {
|
|
return ""
|
|
}
|
|
|
|
return val[idx]
|
|
}
|
|
|
|
func getFieldValue[T any](m map[string]interface{}, key string) (out T) {
|
|
val, ok := m[key]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
out, _ = val.(T)
|
|
|
|
return
|
|
}
|
|
|
|
func getFieldSliceValue[T any](m map[string]interface{}, key string) (out []T) {
|
|
iv := getFieldValue[interface{}](m, key)
|
|
add := func(v interface{}) {
|
|
cv, ok := v.(T)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
out = append(out, cv)
|
|
}
|
|
|
|
// bleve tend to convert []string{"foo"} to type string if slice contains only one value
|
|
// bleve: []string{"foo"} -> "foo"
|
|
// bleve: []string{"foo", "bar"} -> []string{"foo", "bar"}
|
|
switch v := iv.(type) {
|
|
case T:
|
|
add(v)
|
|
case []interface{}:
|
|
for _, rv := range v {
|
|
add(rv)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|