mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-14 06:39:07 -04:00
258 lines
7.0 KiB
Go
258 lines
7.0 KiB
Go
package bleve
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/blevesearch/bleve/v2"
|
|
"github.com/blevesearch/bleve/v2/search/query"
|
|
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
|
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
|
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
|
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/opencloud-eu/opencloud/pkg/kql"
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
|
|
|
|
searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
|
|
searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
|
|
searchQuery "github.com/opencloud-eu/opencloud/services/search/pkg/query"
|
|
)
|
|
|
|
const defaultBatchSize = 50
|
|
|
|
var _ search.Engine = (*Backend)(nil) // ensure Backend implements Engine
|
|
|
|
type Backend struct {
|
|
index bleve.Index
|
|
queryCreator searchQuery.Creator[query.Query]
|
|
log log.Logger
|
|
}
|
|
|
|
func NewBackend(index bleve.Index, queryCreator searchQuery.Creator[query.Query], log log.Logger) *Backend {
|
|
return &Backend{
|
|
index: index,
|
|
queryCreator: queryCreator,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// Search executes a search request operation within the index.
|
|
// Returns a SearchIndexResponse object or an error.
|
|
func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error) {
|
|
createdQuery, err := b.queryCreator.Create(sir.Query)
|
|
if err != nil {
|
|
if kql.IsValidationError(err) {
|
|
return nil, errtypes.BadRequest(err.Error())
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
q := bleve.NewConjunctionQuery(
|
|
// Skip documents that have been marked as deleted
|
|
&query.BoolFieldQuery{
|
|
Bool: false,
|
|
FieldVal: "Deleted",
|
|
},
|
|
createdQuery,
|
|
)
|
|
|
|
if sir.Ref != nil {
|
|
q.Conjuncts = append(
|
|
q.Conjuncts,
|
|
&query.TermQuery{
|
|
FieldVal: "RootID",
|
|
Term: storagespace.FormatResourceID(
|
|
&storageProvider.ResourceId{
|
|
StorageId: sir.Ref.GetResourceId().GetStorageId(),
|
|
SpaceId: sir.Ref.GetResourceId().GetSpaceId(),
|
|
OpaqueId: sir.Ref.GetResourceId().GetOpaqueId(),
|
|
},
|
|
),
|
|
},
|
|
)
|
|
// Scope below the space root: restrict at query level so totals and
|
|
// paging respect the path too. Path is a case-preserving keyword
|
|
// (paths act as references, /Foo and /foo are distinct), so the exact
|
|
// folder or the folder prefix matches all of, and only, the scope.
|
|
if requestedPath := utils.MakeRelativePath(sir.Ref.Path); requestedPath != "." {
|
|
q.Conjuncts = append(q.Conjuncts, query.NewDisjunctionQuery([]query.Query{
|
|
&query.TermQuery{FieldVal: "Path", Term: requestedPath},
|
|
&query.PrefixQuery{FieldVal: "Path", Prefix: requestedPath + "/"},
|
|
}))
|
|
}
|
|
}
|
|
|
|
bleveReq := bleve.NewSearchRequest(q)
|
|
bleveReq.Highlight = bleve.NewHighlight()
|
|
|
|
switch {
|
|
case sir.PageSize == -1:
|
|
bleveReq.Size = math.MaxInt
|
|
case sir.PageSize == 0:
|
|
bleveReq.Size = 200
|
|
default:
|
|
bleveReq.Size = int(sir.PageSize)
|
|
}
|
|
|
|
bleveReq.Fields = []string{"*"}
|
|
res, err := b.index.Search(bleveReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
matches := make([]*searchMessage.Match, 0, len(res.Hits))
|
|
totalMatches := res.Total
|
|
for _, hit := range res.Hits {
|
|
rootID, err := storagespace.ParseID(getFieldValue[string](hit.Fields, "RootID"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rID, err := storagespace.ParseID(getFieldValue[string](hit.Fields, "ID"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pID, _ := storagespace.ParseID(getFieldValue[string](hit.Fields, "ParentID"))
|
|
match := &searchMessage.Match{
|
|
Score: float32(hit.Score),
|
|
Entity: &searchMessage.Entity{
|
|
Ref: &searchMessage.Reference{
|
|
ResourceId: resourceIDtoSearchID(rootID),
|
|
Path: getFieldValue[string](hit.Fields, "Path"),
|
|
},
|
|
Id: resourceIDtoSearchID(rID),
|
|
Name: getFieldValue[string](hit.Fields, "Name"),
|
|
ParentId: resourceIDtoSearchID(pID),
|
|
Size: uint64(getFieldValue[float64](hit.Fields, "Size")),
|
|
Type: uint64(getFieldValue[float64](hit.Fields, "Type")),
|
|
MimeType: getFieldValue[string](hit.Fields, "MimeType"),
|
|
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
|
|
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
|
|
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
|
|
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
|
|
Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"),
|
|
Image: hitToFacet[searchMessage.Image](hit.Fields, "image"),
|
|
Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"),
|
|
Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"),
|
|
Video: hitToFacet[searchMessage.Video](hit.Fields, "video"),
|
|
MotionPhoto: hitToFacet[searchMessage.MotionPhoto](hit.Fields, "motionPhoto"),
|
|
LivePhoto: hitToFacet[searchMessage.LivePhoto](hit.Fields, "livePhoto"),
|
|
},
|
|
}
|
|
|
|
if mtime, err := time.Parse(time.RFC3339, getFieldValue[string](hit.Fields, "Mtime")); err == nil {
|
|
match.Entity.LastModifiedTime = ×tamppb.Timestamp{Seconds: mtime.Unix(), Nanos: int32(mtime.Nanosecond())}
|
|
}
|
|
|
|
matches = append(matches, match)
|
|
}
|
|
|
|
return &searchService.SearchIndexResponse{
|
|
Matches: matches,
|
|
TotalMatches: int32(totalMatches),
|
|
}, nil
|
|
}
|
|
|
|
func (b *Backend) DocCount() (uint64, error) {
|
|
return b.index.DocCount()
|
|
}
|
|
|
|
func (b *Backend) Upsert(id string, r search.Resource) error {
|
|
batch, err := b.NewBatch(defaultBatchSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := batch.Upsert(id, r); err != nil {
|
|
return err
|
|
}
|
|
|
|
return batch.Push()
|
|
}
|
|
|
|
func (b *Backend) Move(id, parentID, targetPath string) error {
|
|
batch, err := b.NewBatch(defaultBatchSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := batch.Move(id, parentID, targetPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
return batch.Push()
|
|
}
|
|
|
|
func (b *Backend) Delete(id string) error {
|
|
batch, err := b.NewBatch(defaultBatchSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := batch.Delete(id); err != nil {
|
|
return err
|
|
}
|
|
|
|
return batch.Push()
|
|
}
|
|
|
|
func (b *Backend) Restore(id string) error {
|
|
batch, err := b.NewBatch(defaultBatchSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := batch.Restore(id); err != nil {
|
|
return err
|
|
}
|
|
|
|
return batch.Push()
|
|
}
|
|
|
|
func (b *Backend) Purge(id string, onlyDeleted bool) error {
|
|
batch, err := b.NewBatch(defaultBatchSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := batch.Purge(id, onlyDeleted); err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|