cursor pagination
This commit is contained in:
@@ -26,7 +26,7 @@ type TenderRepository interface {
|
||||
GetTenderCountByCountry(ctx context.Context) (map[string]int64, error)
|
||||
GetTenderCountByType(ctx context.Context) (map[string]int64, error)
|
||||
GetTenderCountByClassification(ctx context.Context) (map[string]int64, error)
|
||||
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error)
|
||||
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchPageResult, error)
|
||||
GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error)
|
||||
GetUnScrapedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
|
||||
GetUnSummarizedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
|
||||
@@ -40,11 +40,27 @@ func collectionName() string {
|
||||
return "tenders"
|
||||
}
|
||||
|
||||
// deepOffsetSkipCountThreshold skips CountDocuments when offset pagination goes this deep.
|
||||
const deepOffsetSkipCountThreshold = 1000
|
||||
|
||||
// SearchPageResult is the paginated outcome of a tender search/list query.
|
||||
type SearchPageResult struct {
|
||||
Items []Tender
|
||||
TotalCount int64 // -1 when count was skipped
|
||||
NextCursor string
|
||||
HasMore bool
|
||||
}
|
||||
|
||||
// tenderSearchListProjection excludes large fields not needed for list/search API payloads.
|
||||
func tenderSearchListProjection() bson.M {
|
||||
return bson.M{
|
||||
"content_xml": 0,
|
||||
"document_summaries": 0,
|
||||
"content_xml": 0,
|
||||
"document_summaries": 0,
|
||||
"scraped_documents": 0,
|
||||
"selection_criteria": 0,
|
||||
"modifications": 0,
|
||||
"source_file_url": 0,
|
||||
"source_file_name": 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +101,26 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Te
|
||||
*orm.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}),
|
||||
*orm.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}),
|
||||
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
// Stable keyset pagination for default admin list (created_at desc).
|
||||
*orm.NewIndex("created_at_id_idx", bson.D{
|
||||
{Key: "created_at", Value: -1},
|
||||
{Key: "_id", Value: -1},
|
||||
}),
|
||||
*orm.NewIndex("status_created_at_id_idx", bson.D{
|
||||
{Key: "status", Value: 1},
|
||||
{Key: "created_at", Value: -1},
|
||||
{Key: "_id", Value: -1},
|
||||
}),
|
||||
*orm.NewIndex("country_code_created_at_id_idx", bson.D{
|
||||
{Key: "country_code", Value: 1},
|
||||
{Key: "created_at", Value: -1},
|
||||
{Key: "_id", Value: -1},
|
||||
}),
|
||||
*orm.NewIndex("notice_type_code_created_at_id_idx", bson.D{
|
||||
{Key: "notice_type_code", Value: 1},
|
||||
{Key: "created_at", Value: -1},
|
||||
{Key: "_id", Value: -1},
|
||||
}),
|
||||
*orm.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
|
||||
// Compound indexes for common queries
|
||||
*orm.NewIndex("status_country_idx", bson.D{
|
||||
@@ -321,10 +357,9 @@ func (r *tenderRepository) Delete(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// Search searches tenders based on criteria
|
||||
func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) {
|
||||
func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchPageResult, error) {
|
||||
filter := r.buildSearchFilter(form)
|
||||
|
||||
// Build sort
|
||||
sort := "created_at"
|
||||
if pagination.SortBy != "" {
|
||||
sort = pagination.SortBy
|
||||
@@ -335,23 +370,34 @@ func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, paginat
|
||||
sortOrder = pagination.SortOrder
|
||||
}
|
||||
|
||||
// Build pagination
|
||||
skipCount := pagination.Cursor != "" || pagination.Offset >= deepOffsetSkipCountThreshold
|
||||
|
||||
paginationBuilder := orm.NewPaginationBuilder().
|
||||
Limit(pagination.Limit).
|
||||
Skip(pagination.Offset).
|
||||
SortBy(sort, sortOrder).
|
||||
Projection(tenderSearchListProjection()).
|
||||
Build()
|
||||
SkipCount(skipCount)
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
|
||||
if pagination.Cursor != "" {
|
||||
paginationBuilder.Cursor(pagination.Cursor)
|
||||
} else {
|
||||
paginationBuilder.Skip(pagination.Offset)
|
||||
}
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Items, result.TotalCount, nil
|
||||
return &SearchPageResult{
|
||||
Items: result.Items,
|
||||
TotalCount: result.TotalCount,
|
||||
NextCursor: result.NextCursor,
|
||||
HasMore: result.HasMore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetExpiredTenders retrieves tenders that are expired
|
||||
|
||||
Reference in New Issue
Block a user