Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion services/search/pkg/bleve/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
Highlights: buildHighlights(hit.Fragments),
Audio: getAudioValue[searchMessage.Audio](hit.Fields),
Image: getImageValue[searchMessage.Image](hit.Fields),
Location: getLocationValue[searchMessage.GeoCoordinates](hit.Fields),
Expand Down
35 changes: 35 additions & 0 deletions services/search/pkg/bleve/bleve.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ func getFieldSliceValue[T any](m map[string]any, key string) (out []T) {
return
}

// buildHighlights combines Content and Metadata highlights into a single string.
// Content highlights are shown first, followed by metadata field matches.
func buildHighlights(fragments bleveSearch.FieldFragmentMap) string {
var parts []string

// Content highlight (traditional full-text match)
if contentFrags, ok := fragments["Content"]; ok && len(contentFrags) > 0 {
parts = append(parts, contentFrags[0])
}

// Metadata highlights (e.g. Metadata.oy.fileReference, Metadata.oy.subject, ...)
for field, frags := range fragments {
if strings.HasPrefix(field, "Metadata.") && len(frags) > 0 {
key := strings.TrimPrefix(field, "Metadata.")
parts = append(parts, key+": "+frags[0])
}
}

return strings.Join(parts, " · ")
}

func getFragmentValue(m bleveSearch.FieldFragmentMap, key string, idx int) string {
val, ok := m[key]
if !ok {
Expand Down Expand Up @@ -178,6 +199,19 @@ func getFieldName(structField reflect.StructField) string {
}

func matchToResource(match *bleveSearch.DocumentMatch) *search.Resource {
// Extract Metadata.* fields from the flat bleve field map
metadata := make(map[string]string)
for k, v := range match.Fields {
if strings.HasPrefix(k, "Metadata.") {
if s, ok := v.(string); ok {
metadata[strings.TrimPrefix(k, "Metadata.")] = s
}
}
}
if len(metadata) == 0 {
metadata = nil
}

return &search.Resource{
ID: getFieldValue[string](match.Fields, "ID"),
RootID: getFieldValue[string](match.Fields, "RootID"),
Expand All @@ -194,6 +228,7 @@ func matchToResource(match *bleveSearch.DocumentMatch) *search.Resource {
Content: getFieldValue[string](match.Fields, "Content"),
Tags: getFieldSliceValue[string](match.Fields, "Tags"),
Favorites: getFieldSliceValue[string](match.Fields, "Favorites"),
Metadata: metadata,
Audio: getAudioValue[libregraph.Audio](match.Fields),
Image: getImageValue[libregraph.Image](match.Fields),
Location: getLocationValue[libregraph.GeoCoordinates](match.Fields),
Expand Down
6 changes: 6 additions & 0 deletions services/search/pkg/bleve/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,17 @@ func NewMapping() (mapping.IndexMapping, error) {
fulltextFieldMapping.Analyzer = "fulltext"
fulltextFieldMapping.IncludeInAll = false

// Metadata sub-document: each key is a searchable text field
metadataMapping := bleve.NewDocumentMapping()
metadataMapping.Dynamic = true
metadataMapping.DefaultAnalyzer = "lowercaseKeyword"

docMapping := bleve.NewDocumentMapping()
docMapping.AddFieldMappingsAt("Name", nameMapping)
docMapping.AddFieldMappingsAt("Tags", lowercaseMapping)
docMapping.AddFieldMappingsAt("Favorites", lowercaseMapping)
docMapping.AddFieldMappingsAt("Content", fulltextFieldMapping)
docMapping.AddSubDocumentMapping("Metadata", metadataMapping)

indexMapping := bleve.NewIndexMapping()
indexMapping.DefaultAnalyzer = keyword.Name
Expand Down
10 changes: 10 additions & 0 deletions services/search/pkg/content/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ func (b Basic) Extract(_ context.Context, ri *storageProvider.ResourceInfo) (Doc
if t, ok := m["tags"]; ok {
doc.Tags = tags.New(t).AsSlice()
}
// Index all arbitrary metadata (user.oc.md.* xattrs)
md := make(map[string]string)
for k, v := range m {
if k != "tags" && v != "" {
md[k] = v
}
}
if len(md) > 0 {
doc.Metadata = md
}
}

if m := ri.Opaque.GetMap(); m != nil && m["favorites"] != nil {
Expand Down
1 change: 1 addition & 0 deletions services/search/pkg/content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Document struct {
MimeType string
Tags []string
Favorites []string
Metadata map[string]string `json:"metadata,omitempty"`
Audio *libregraph.Audio `json:"audio,omitempty"`
Image *libregraph.Image `json:"image,omitempty"`
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
Expand Down
15 changes: 10 additions & 5 deletions services/search/pkg/opensearch/internal/convert/opensearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ func OpenSearchHitToMatch(hit opensearchgoAPI.SearchHit) (*searchMessage.Match,
Deleted: resource.Deleted,
Tags: resource.Tags,
Highlights: func() string {
contentHighlights, ok := hit.Highlight["Content"]
if !ok {
return ""
var parts []string
if contentHighlights, ok := hit.Highlight["Content"]; ok {
parts = append(parts, strings.Join(contentHighlights, "; "))
}

return strings.Join(contentHighlights[:], "; ")
for field, frags := range hit.Highlight {
if strings.HasPrefix(field, "Metadata.") && len(frags) > 0 {
key := strings.TrimPrefix(field, "Metadata.")
parts = append(parts, key+": "+strings.Join(frags, "; "))
}
}
return strings.Join(parts, " · ")
}(),
Audio: func() *searchMessage.Audio {
if !strings.HasPrefix(resource.MimeType, "audio/") {
Expand Down