enhancement(search): implement engine search skeleton

This commit is contained in:
fschade
2025-08-28 09:30:59 +02:00
parent 492340f6f7
commit 37d8b1d608
4 changed files with 124 additions and 45 deletions
+44 -1
View File
@@ -8,6 +8,8 @@ import (
opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
"github.com/opencloud-eu/opencloud/pkg/kql"
searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/search/pkg/engine"
)
@@ -22,7 +24,48 @@ func NewEngine(index string, client *opensearchgoAPI.Client) (*Engine, error) {
}
func (e *Engine) Search(ctx context.Context, sir *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error) {
return &searchService.SearchIndexResponse{}, nil
ast, err := kql.Builder{}.Build(sir.Query)
if err != nil {
return nil, fmt.Errorf("failed to build query: %w", err)
}
query, err := KQL{}.Compile(ast)
if err != nil {
return nil, fmt.Errorf("failed to compile query: %w", err)
}
body, err := query.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal query: %w", err)
}
resp, err := e.client.Search(context.Background(), &opensearchgoAPI.SearchReq{
Indices: []string{e.index},
Body: bytes.NewReader(body),
})
if err != nil {
return nil, fmt.Errorf("failed to count documents: %w", err)
}
matches := make([]*searchMessage.Match, len(resp.Hits.Hits))
for i, hit := range resp.Hits.Hits {
resource, err := convert[engine.Resource](hit.Source)
if err != nil {
return nil, fmt.Errorf("failed to convert hit %d: %w", i, err)
}
matches[i] = &searchMessage.Match{
Score: hit.Score,
Entity: &searchMessage.Entity{
Name: resource.Name,
},
}
}
return &searchService.SearchIndexResponse{
Matches: matches,
TotalMatches: int32(resp.Hits.Total.Value),
}, nil
}
func (e *Engine) Upsert(id string, r engine.Resource) error {
+27 -2
View File
@@ -5,10 +5,35 @@ import (
"github.com/stretchr/testify/assert"
searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/search/pkg/opensearch"
"github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
)
func TestEngine_Search(t *testing.T) {
index := "test-engine-search"
tc := ostest.NewDefaultTestClient(t)
tc.Require.IndicesReset([]string{index})
tc.Require.IndicesCount([]string{index}, "", 0)
defer tc.Require.IndicesDelete([]string{index})
document := ostest.Testdata.Resources.Full
tc.Require.DocumentCreate(index, document.ID, toJSON(t, document))
tc.Require.IndicesCount([]string{index}, "", 1)
engine, err := opensearch.NewEngine(index, tc.Client())
assert.NoError(t, err)
t.Run("most simple search", func(t *testing.T) {
resp, err := engine.Search(t.Context(), &searchService.SearchIndexRequest{Query: "\"" + document.Name + "\""})
assert.NoError(t, err)
assert.Len(t, resp.Matches, 1)
assert.Equal(t, int32(1), resp.TotalMatches)
assert.Equal(t, document.Name, resp.Matches[0].Entity.Name)
})
}
func TestEngine_Upsert(t *testing.T) {
index := "test-engine-upsert"
tc := ostest.NewDefaultTestClient(t)
@@ -20,7 +45,7 @@ func TestEngine_Upsert(t *testing.T) {
engine, err := opensearch.NewEngine(index, tc.Client())
assert.NoError(t, err)
t.Run("Upsert with full document", func(t *testing.T) {
t.Run("upsert with full document", func(t *testing.T) {
document := ostest.Testdata.Resources.Full
assert.NoError(t, engine.Upsert(document.ID, document))
@@ -96,7 +121,7 @@ func TestEngine_Purge(t *testing.T) {
engine, err := opensearch.NewEngine(index, tc.Client())
assert.NoError(t, err)
t.Run("Purge with full document", func(t *testing.T) {
t.Run("purge with full document", func(t *testing.T) {
document := ostest.Testdata.Resources.Full
tc.Require.DocumentCreate(index, document.ID, toJSON(t, document))
tc.Require.IndicesCount([]string{index}, "", 1)
+33 -41
View File
@@ -1,6 +1,7 @@
package opensearch
import (
"errors"
"strings"
"github.com/opencloud-eu/opencloud/pkg/ast"
@@ -8,52 +9,43 @@ import (
type KQL struct{}
func (k KQL) Compile(givenAst *ast.Ast) (Builder, error) {
q, err := k.compile(givenAst)
if err != nil {
return nil, err
}
return q, nil
}
func (k KQL) compile(a *ast.Ast) (Builder, error) {
q, _, err := k.walk(0, a.Nodes)
if err != nil {
return nil, err
}
return q, nil
}
func (k KQL) walk(offset int, nodes []ast.Node) (Builder, int, error) {
var boolQuery = NewBoolQuery()
for i := offset; i < len(nodes); i++ {
switch n := nodes[i].(type) {
case *ast.StringNode:
field := k.getField(n.Key)
switch spaces := strings.Split(n.Value, " "); {
case len(spaces) == 1:
boolQuery.Must(NewTermQuery[string](field).Value(n.Value))
case len(spaces) > 1:
boolQuery.Must(NewMatchPhraseQuery(field).Query(n.Value))
default:
continue
}
case *ast.OperatorNode:
func (k KQL) Compile(a *ast.Ast) (*RootQuery, error) {
switch {
case len(a.Nodes) == 0:
return nil, errors.New("no nodes in AST")
case len(a.Nodes) == 1:
builder, err := k.getBuilder(a.Nodes[0])
if err != nil {
return nil, err
}
return NewRootQuery(builder), nil
}
return boolQuery, 0, nil
return nil, nil
}
func (k KQL) getField(name string) string {
if name == "" {
func (k KQL) getBuilder(someNode ast.Node) (Builder, error) {
var query Builder
switch node := someNode.(type) {
case *ast.StringNode:
field := k.mapField(node.Key)
switch spaces := strings.Split(node.Value, " "); {
case len(spaces) == 1:
query = NewTermQuery[string](field).Value(node.Value)
case len(spaces) > 1:
query = NewMatchPhraseQuery(field).Query(node.Value)
}
}
return query, nil
}
func (k KQL) mapField(field string) string {
if field == "" {
return "Name"
}
fields := map[string]string{
mappings := map[string]string{
"rootid": "RootID",
"path": "Path",
"id": "ID",
@@ -68,9 +60,9 @@ func (k KQL) getField(name string) string {
"hidden": "Hidden",
}
if _, ok := fields[strings.ToLower(name)]; ok {
return fields[strings.ToLower(name)]
if mapped, ok := mappings[strings.ToLower(field)]; ok {
return mapped
}
return name
return field
}
+20 -1
View File
@@ -10,7 +10,26 @@ import (
)
func TestKQL_Compile(t *testing.T) {
tests := []tableTest[*ast.Ast, opensearch.Builder]{}
tests := []tableTest[*ast.Ast, opensearch.Builder]{
{
name: "federated",
got: &ast.Ast{
Nodes: []ast.Node{
&ast.StringNode{Value: "federated"},
},
},
want: opensearch.NewRootQuery(opensearch.NewTermQuery[string]("Name").Value("federated")),
},
{
name: "John Smith",
got: &ast.Ast{
Nodes: []ast.Node{
&ast.StringNode{Value: "John Smith"},
},
},
want: opensearch.NewRootQuery(opensearch.NewMatchPhraseQuery("Name").Query("John Smith")),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {