typesense/pkg/api/utils.go
2025-03-12 12:05:22 +01:00

176 lines
5.3 KiB
Go

package typesenseapi
import (
"context"
"fmt"
"sort"
"strings"
"time"
pkgx "github.com/foomo/typesense/pkg"
"github.com/typesense/typesense-go/v3/typesense/api"
"github.com/typesense/typesense-go/v3/typesense/api/pointer"
"go.uber.org/zap"
)
// buildSearchParams will return the search collection parameters
// this is meant as a utility function to create the search collection parameters
// for the typesense search API without any knowledge of the typesense API
func buildSearchParams(
q string,
filterBy map[string][]string,
page, perPage int,
sortBy string,
) *api.SearchCollectionParams {
parameters := &api.SearchCollectionParams{}
parameters.Q = pointer.String(q)
if filterByString := formatFilterQuery(filterBy); filterByString != "" {
parameters.FilterBy = pointer.String(filterByString)
}
parameters.Page = pointer.Int(page)
parameters.PerPage = pointer.Int(perPage)
if sortBy != "" {
parameters.SortBy = pointer.String(sortBy)
}
return parameters
}
func formatFilterQuery(filterBy map[string][]string) string {
if filterBy == nil {
return ""
}
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(filterClauses, " && ")
}
func (b *BaseAPI[indexDocument, returnType]) generateRevisionID() pkgx.RevisionID {
return pkgx.RevisionID(time.Now().Format("2006-01-02-15-04")) // "YYYY-MM-DD-HH-MM"
}
func formatCollectionName(indexID pkgx.IndexID, revisionID pkgx.RevisionID) string {
return fmt.Sprintf("%s-%s", indexID, revisionID)
}
func extractRevisionID(collectionName, name string) pkgx.RevisionID {
if !strings.HasPrefix(collectionName, name+"-") {
return ""
}
revisionID := strings.TrimPrefix(collectionName, name+"-")
// Validate that the extracted revision ID follows YYYY-MM-DD-HH-MM format (16 chars)
if len(revisionID) != 16 {
return ""
}
return pkgx.RevisionID(revisionID)
}
// ensureAliasMapping ensures an alias correctly points to the specified collection.
func (b *BaseAPI[indexDocument, returnType]) ensureAliasMapping(ctx context.Context, indexID pkgx.IndexID, collectionName string) error {
_, err := b.client.Aliases().Upsert(ctx, string(indexID), &api.CollectionAliasSchema{
CollectionName: collectionName,
})
if err != nil {
b.l.Error("failed to upsert alias",
zap.String("alias", string(indexID)),
zap.String("collection", collectionName),
zap.Error(err),
)
}
return err
}
func (b *BaseAPI[indexDocument, returnType]) pruneOldCollections(ctx context.Context, alias, currentCollection string) error {
// Step 1: Retrieve all collections
collections, err := b.client.Collections().Retrieve(ctx)
if err != nil {
b.l.Error("failed to retrieve collections", zap.Error(err))
return err
}
var oldCollections []string
for _, col := range collections {
if strings.HasPrefix(col.Name, alias+"-") && col.Name != currentCollection {
oldCollections = append(oldCollections, col.Name)
}
}
// Step 2: Sort collections by timestamp (latest first)
sort.Slice(oldCollections, func(i, j int) bool {
return oldCollections[i] > oldCollections[j] // Reverse order
})
// Step 3: Delete all but the latest two collections
if len(oldCollections) > 1 {
toDelete := oldCollections[1:] // Keep only the latest two
for _, col := range toDelete {
_, err := b.client.Collection(col).Delete(ctx)
if err != nil {
b.l.Error("failed to delete collection", zap.String("collection", col), zap.Error(err))
} else {
b.l.Info("deleted old collection", zap.String("collection", col))
}
}
}
return nil
}
// fetchExistingCollections retrieves all existing collections and stores them in a map for quick lookup.
func (b *BaseAPI[indexDocument, returnType]) fetchExistingCollections(ctx context.Context) (map[string]bool, error) {
collections, err := b.client.Collections().Retrieve(ctx)
if err != nil {
b.l.Error("failed to retrieve collections", zap.Error(err))
return nil, err
}
existingCollections := make(map[string]bool)
for _, col := range collections {
existingCollections[col.Name] = true
}
return existingCollections, nil
}
// createCollectionIfNotExists ensures that a collection exists before trying to use it.
func (b *BaseAPI[indexDocument, returnType]) createCollectionIfNotExists(ctx context.Context, schema *api.CollectionSchema, collectionName string) error {
// Check if collection already exists
existingCollections, err := b.fetchExistingCollections(ctx)
if err != nil {
return err
}
if existingCollections[collectionName] {
b.l.Info("collection already exists, skipping creation", zap.String("collection", collectionName))
return nil
}
// Set the collection name and create it
schema.Name = collectionName
_, err = b.client.Collections().Create(ctx, schema)
if err != nil {
b.l.Error("failed to create collection", zap.String("collection", collectionName), zap.Error(err))
return err
}
b.l.Info("created new collection", zap.String("collection", collectionName))
return nil
}