enhancement(search): implement engine search skeleton
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user