Skip to content
Merged
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
248 changes: 179 additions & 69 deletions api/search/v1/search_service.pb.go

Large diffs are not rendered by default.

45 changes: 44 additions & 1 deletion api/search/v1/search_service_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions proto/agntcy/dir/search/v1/search_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ service SearchService {
// Returns complete record data including all metadata, skills, domains, etc.
// This operation does not interact with the network.
rpc SearchRecords(SearchRecordsRequest) returns (stream SearchRecordsResponse);

// Count records that match the given parameters.
// This operation does not interact with the network.
rpc CountRecords(CountRecordsRequest) returns (CountRecordsResponse);
}

message SearchCIDsRequest {
Expand Down Expand Up @@ -69,6 +73,11 @@ message SearchRecordsRequest {
SortMode sort_mode = 4;
}

message CountRecordsRequest {
// List of queries to match against the records.
repeated RecordQuery queries = 1;
}

message SearchCIDsResponse {
// The CID of the record that matches the search criteria.
string record_cid = 1;
Expand All @@ -78,3 +87,8 @@ message SearchRecordsResponse {
// The full record that matches the search criteria.
agntcy.dir.core.v1.Record record = 1;
}

message CountRecordsResponse {
// Total number of distinct records that match the search criteria.
uint32 total_count = 1;
}
4 changes: 4 additions & 0 deletions reconciler/tasks/signature/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ func (f *fakeSignatureDB) GetRecordCIDs(opts ...types.FilterOption) ([]string, e
return nil, nil
}

func (f *fakeSignatureDB) CountRecords(opts ...types.FilterOption) (uint32, error) {
return 0, nil
}

func (f *fakeSignatureDB) GetRecords(opts ...types.FilterOption) ([]coretypes.Record, error) {
return nil, nil
}
Expand Down
17 changes: 17 additions & 0 deletions server/controller/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package controller

import (
"context"
"fmt"

corev1 "github.com/agntcy/dir/api/core/v1"
Expand All @@ -29,6 +30,22 @@ func NewSearchController(db types.DatabaseAPI, store types.StoreAPI) searchv1.Se
}
}

func (c *searchCtlr) CountRecords(_ context.Context, req *searchv1.CountRecordsRequest) (*searchv1.CountRecordsResponse, error) {
searchLogger.Debug("Called search controller's CountRecords method", "req", req)

filterOptions, err := databaseutils.QueryToFilters(req.GetQueries())
if err != nil {
return nil, fmt.Errorf("failed to create filter options: %w", err)
}

totalCount, err := c.db.CountRecords(filterOptions...)
if err != nil {
return nil, fmt.Errorf("failed to count records: %w", err)
}

return &searchv1.CountRecordsResponse{TotalCount: totalCount}, nil
}

func (c *searchCtlr) SearchCIDs(req *searchv1.SearchCIDsRequest, srv searchv1.SearchService_SearchCIDsServer) error {
searchLogger.Debug("Called search controller's SearchCIDs method", "req", req)

Expand Down
74 changes: 74 additions & 0 deletions server/controller/search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright AGNTCY Contributors (https://github.com/agntcy)
// SPDX-License-Identifier: Apache-2.0

package controller

import (
"context"
"testing"

searchv1 "github.com/agntcy/dir/api/search/v1"
"github.com/agntcy/dir/server/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type fakeSearchDB struct {
types.DatabaseAPI
totalCount uint32
err error
gotFilters types.RecordFilters
}

func (f *fakeSearchDB) CountRecords(opts ...types.FilterOption) (uint32, error) {
for _, opt := range opts {
if opt != nil {
opt(&f.gotFilters)
}
}

return f.totalCount, f.err
}

func TestCountRecords(t *testing.T) {
db := &fakeSearchDB{totalCount: 7}
ctrl := NewSearchController(db, nil)

resp, err := ctrl.CountRecords(context.Background(), &searchv1.CountRecordsRequest{
Queries: []*searchv1.RecordQuery{
{
Type: searchv1.RecordQueryType_RECORD_QUERY_TYPE_NAME,
Value: "*assistant*",
},
},
})
require.NoError(t, err)
assert.Equal(t, uint32(7), resp.GetTotalCount())
assert.Equal(t, []string{"*assistant*"}, db.gotFilters.Names)
assert.Zero(t, db.gotFilters.Limit)
assert.Zero(t, db.gotFilters.Offset)
assert.Empty(t, db.gotFilters.OrderBy)
}

func TestCountRecords_InvalidQuery(t *testing.T) {
ctrl := NewSearchController(&fakeSearchDB{}, nil)

_, err := ctrl.CountRecords(context.Background(), &searchv1.CountRecordsRequest{
Queries: []*searchv1.RecordQuery{
{
Type: searchv1.RecordQueryType_RECORD_QUERY_TYPE_SKILL_ID,
Value: "not-a-number",
},
},
})
require.Error(t, err)
assert.ErrorContains(t, err, "failed to create filter options")
}

func TestCountRecords_DatabaseError(t *testing.T) {
ctrl := NewSearchController(&fakeSearchDB{err: assert.AnError}, nil)

_, err := ctrl.CountRecords(context.Background(), &searchv1.CountRecordsRequest{})
require.Error(t, err)
assert.ErrorContains(t, err, "failed to count records")
}
49 changes: 49 additions & 0 deletions server/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,55 @@ func TestGetRecordCIDs_Pagination(t *testing.T) {
assert.Len(t, cids, 1)
}

func TestCountRecords(t *testing.T) {
db := setupTestDB(t)
seedDB(t, db)

tests := []struct {
name string
opts []types.FilterOption
expected uint32
}{
{name: "all records", expected: 3},
{
name: "filters records",
opts: []types.FilterOption{types.WithNames("*assistant*")},
expected: 2,
},
{
name: "ignores pagination and sorting",
opts: []types.FilterOption{
types.WithLimit(1),
types.WithOffset(2),
types.WithOrderBy(types.RecordOrderClause{Column: "name"}),
},
expected: 3,
},
{
name: "counts distinct records across joined rows",
opts: []types.FilterOption{types.WithSkillNames("natural_language_processing/*")},
expected: 2,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
count, err := db.CountRecords(tc.opts...)
require.NoError(t, err)
assert.Equal(t, tc.expected, count)
})
}
}

func TestCountRecords_NilOption(t *testing.T) {
db := setupTestDB(t)

var nilOpt types.FilterOption

_, err := db.CountRecords(nilOpt)
assert.Error(t, err)
}

func TestGetRecordCIDs_Wildcards(t *testing.T) {
db := setupTestDB(t)
seedDB(t, db)
Expand Down
34 changes: 34 additions & 0 deletions server/database/gorm/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package gorm
import (
"errors"
"fmt"
"math"
"strings"
"time"

Expand Down Expand Up @@ -256,6 +257,39 @@ type cidRecord struct {
RecordCID string `gorm:"column:record_cid"`
}

// CountRecords returns the number of distinct records matching the provided options.
// Pagination and sorting options are ignored.
func (d *DB) CountRecords(opts ...types.FilterOption) (uint32, error) {
cfg := &types.RecordFilters{}

for _, opt := range opts {
if opt == nil {
return 0, errors.New("nil option provided")
}

opt(cfg)
}

cfg.Limit = 0
cfg.Offset = 0
cfg.OrderBy = nil

query := d.gormDB.Model(&Record{})
query = d.handleFilterOptions(query, cfg)
query = query.Distinct("records.record_cid")

var count int64
if err := query.Count(&count).Error; err != nil {
return 0, fmt.Errorf("count records: %w", err)
}

if count < 0 || math.MaxUint32 < count {
return 0, fmt.Errorf("can't convert %d to uint32", count)
}

return uint32(count), nil
}

// GetRecordCIDs retrieves only record CIDs based on the provided options.
// This is optimized for cases where only CIDs are needed, avoiding expensive joins and preloads.
func (d *DB) GetRecordCIDs(opts ...types.FilterOption) ([]string, error) {
Expand Down
3 changes: 3 additions & 0 deletions server/types/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type SearchDatabaseAPI interface {
// GetRecordCIDs retrieves record CIDs based on the provided filters.
GetRecordCIDs(opts ...FilterOption) ([]string, error)

// CountRecords returns the number of distinct records matching the provided filters.
CountRecords(opts ...FilterOption) (uint32, error)

// GetRecords retrieves full records based on the provided filters.
GetRecords(opts ...FilterOption) ([]coretypes.Record, error)

Expand Down
Loading