1
0
mirror of https://github.com/owncloud/ocis.git synced 2026-10-23 23:29:02 +03:00
Files
ocis/services/search/pkg/engine/engine.go
Paul Faure a17760898c feat(search): add ocis search optimize CLI command (#12136)
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>
2026-04-17 09:16:03 +00:00

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
}