diff --git a/go.mod b/go.mod index 6b1d7f6..c33b95b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/foomo/typesense -go 1.23.4 +go 1.24.0 require ( github.com/foomo/contentserver v1.11.2 diff --git a/pkg/api/api.go b/pkg/api/api.go index 6617fc2..fef178c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -270,10 +270,10 @@ func (b *BaseAPI[indexDocument, returnType]) SimpleSearch( ctx context.Context, index pkgtypesense.IndexID, q string, - filterBy map[string]string, + filterBy map[string][]string, page, perPage int, sortBy string, -) ([]returnType, pkgtypesense.Scores, error) { +) ([]returnType, pkgtypesense.Scores, int, error) { // Call buildSearchParams but also set QueryBy explicitly parameters := buildSearchParams(q, filterBy, page, perPage, sortBy) parameters.QueryBy = pointer.String("title") @@ -282,29 +282,41 @@ func (b *BaseAPI[indexDocument, returnType]) SimpleSearch( } // ExpertSearch will perform a search operation on the given index -// it will return the documents and the scores +// it will return the documents, scores, and totalResults func (b *BaseAPI[indexDocument, returnType]) ExpertSearch( ctx context.Context, indexID pkgtypesense.IndexID, parameters *api.SearchCollectionParams, -) ([]returnType, pkgtypesense.Scores, error) { +) ([]returnType, pkgtypesense.Scores, int, error) { if parameters == nil { b.l.Error("Search parameters are nil") - return nil, nil, errors.New("search parameters cannot be nil") + return nil, nil, 0, errors.New("search parameters cannot be nil") } collectionName := string(indexID) // digital-bks-at-de searchResponse, err := b.client.Collection(collectionName).Documents().Search(ctx, parameters) if err != nil { b.l.Error("Failed to perform search", zap.String("index", collectionName), zap.Error(err)) - return nil, nil, err + return nil, nil, 0, err + } + // Extract totalResults from the search response + totalResults := *searchResponse.Found + + // Ensure Hits is not empty before proceeding + if searchResponse.Hits == nil || len(*searchResponse.Hits) == 0 { + b.l.Warn("Search response contains no hits", zap.String("index", collectionName)) + return nil, nil, totalResults, nil } - // Parse search results - var results = make([]returnType, 0, len(*searchResponse.Hits)) + results := make([]returnType, len(*searchResponse.Hits)) scores := make(pkgtypesense.Scores) - for _, hit := range *searchResponse.Hits { + for i, hit := range *searchResponse.Hits { + if hit.Document == nil { + b.l.Warn("Hit document is nil", zap.String("index", collectionName)) + continue + } + docMap := *hit.Document // Extract document ID safely @@ -322,7 +334,7 @@ func (b *BaseAPI[indexDocument, returnType]) ExpertSearch( continue } - results = append(results, doc) + results[i] = doc index := 0 if hit.TextMatchInfo != nil && hit.TextMatchInfo.Score != nil { if score, err := strconv.Atoi(*hit.TextMatchInfo.Score); err == nil { @@ -341,7 +353,8 @@ func (b *BaseAPI[indexDocument, returnType]) ExpertSearch( b.l.Info("Search completed", zap.String("index", collectionName), zap.Int("results_count", len(results)), + zap.Int("total_results", totalResults), ) - return results, scores, nil + return results, scores, totalResults, nil } diff --git a/pkg/api/utils.go b/pkg/api/utils.go index 7a3a504..818f849 100644 --- a/pkg/api/utils.go +++ b/pkg/api/utils.go @@ -18,7 +18,7 @@ import ( // for the typesense search API without any knowledge of the typesense API func buildSearchParams( q string, - filterBy map[string]string, + filterBy map[string][]string, page, perPage int, sortBy string, ) *api.SearchCollectionParams { @@ -36,31 +36,33 @@ func buildSearchParams( return parameters } -func formatFilterQuery(filterBy map[string]string) string { +func formatFilterQuery(filterBy map[string][]string) string { if filterBy == nil { return "" } - filterByString := []string{} - for key, value := range filterBy { - filterByString = append(filterByString, key+":="+value) + + var filterClauses []string + for key, values := range filterBy { + if len(values) == 1 { + // Single value → Use `:=` operator + filterClauses = append(filterClauses, fmt.Sprintf("%s:=\"%s\"", key, values[0])) + } else { + // Multiple values → Use `["val1","val2"]` array syntax + formattedValues := []string{} + for _, v := range values { + formattedValues = append(formattedValues, fmt.Sprintf("\"%s\"", v)) + } + filterClauses = append(filterClauses, fmt.Sprintf("%s:[%s]", key, strings.Join(formattedValues, ","))) + } } - return strings.Join(filterByString, "&&") + + return strings.Join(filterClauses, " && ") } func (b *BaseAPI[indexDocument, returnType]) generateRevisionID() pkgtypesense.RevisionID { return pkgtypesense.RevisionID(time.Now().Format("2006-01-02-15-04")) // "YYYY-MM-DD-HH-MM" } -func getLatestRevisionID(revisions map[pkgtypesense.IndexID]pkgtypesense.RevisionID) pkgtypesense.RevisionID { - var latest pkgtypesense.RevisionID - for _, rev := range revisions { - if rev > latest { - latest = rev - } - } - return latest -} - func formatCollectionName(indexID pkgtypesense.IndexID, revisionID pkgtypesense.RevisionID) string { return fmt.Sprintf("%s-%s", indexID, revisionID) } diff --git a/pkg/indexing/contentserver.go b/pkg/indexing/contentserver.go index fe99311..58eeb28 100644 --- a/pkg/indexing/contentserver.go +++ b/pkg/indexing/contentserver.go @@ -40,12 +40,18 @@ func (c ContentServer[indexDocument]) Provide( if err != nil { return nil, err } + + urlsByIDs, err := c.fetchURLsByDocumentIDs(ctx, indexID, documentInfos) + if err != nil { + return nil, err + } + documents := make([]*indexDocument, len(documentInfos)) for index, documentInfo := range documentInfos { if documentProvider, ok := c.documentProviderFuncs[documentInfo.DocumentType]; !ok { c.l.Warn("no document provider available for document type", zap.String("documentType", string(documentInfo.DocumentType))) } else { - document, err := documentProvider(ctx, indexID, documentInfo.DocumentID) + document, err := documentProvider(ctx, indexID, documentInfo.DocumentID, urlsByIDs) if err != nil { c.l.Error( "index document not created", @@ -113,3 +119,31 @@ func createFlatRepoNodeMap(node *content.RepoNode, nodeMap map[string]*content.R } return nodeMap } + +func (c ContentServer[indexDocument]) fetchURLsByDocumentIDs( + ctx context.Context, + indexID typesense.IndexID, + documentInfos []typesense.DocumentInfo, +) (map[typesense.DocumentID]string, error) { + ids := make([]string, len(documentInfos)) + + for i, documentInfo := range documentInfos { + ids[i] = string(documentInfo.DocumentID) + } + + uriMap, err := c.contentserverClient.GetURIs(ctx, string(indexID), ids) + if err != nil { + c.l.Error("failed to get URIs", zap.Error(err)) + return nil, err + } + + return convertMapStringToDocumentID(uriMap), nil +} + +func convertMapStringToDocumentID(input map[string]string) map[typesense.DocumentID]string { + output := make(map[typesense.DocumentID]string, len(input)) + for key, value := range input { + output[typesense.DocumentID(key)] = value + } + return output +} diff --git a/pkg/interface.go b/pkg/interface.go index 3865237..b838883 100644 --- a/pkg/interface.go +++ b/pkg/interface.go @@ -21,11 +21,11 @@ type API[indexDocument any, returnType any] interface { ctx context.Context, index IndexID, q string, - filterBy map[string]string, + filterBy map[string][]string, page, perPage int, sortBy string, - ) ([]returnType, Scores, error) - ExpertSearch(ctx context.Context, index IndexID, parameters *api.SearchCollectionParams) ([]returnType, Scores, error) + ) ([]returnType, Scores, int, error) + ExpertSearch(ctx context.Context, index IndexID, parameters *api.SearchCollectionParams) ([]returnType, Scores, int, error) Healthz(ctx context.Context) error Indices() ([]IndexID, error) } diff --git a/pkg/vo.go b/pkg/vo.go index 1783932..b7acbb2 100644 --- a/pkg/vo.go +++ b/pkg/vo.go @@ -19,6 +19,7 @@ type DocumentProviderFunc[indexDocument any] func( ctx context.Context, indexID IndexID, documentID DocumentID, + urlsByIDs map[DocumentID]string, ) (*indexDocument, error) type DocumentInfo struct {