Merge pull request #2 from foomo/feature/support-filter-by-multiple-values

feature: support filter by multiple values, pass uri map to documentProviders
This commit is contained in:
Miroslav Cvetic 2025-03-07 08:34:16 +01:00 committed by GitHub
commit 21da1fdc84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 32 deletions

2
go.mod
View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {