diff --git a/internal/infrastructure/database/migrations/004_create_fts_index.sql b/internal/infrastructure/database/migrations/004_create_fts_index.sql new file mode 100644 index 0000000..e58b359 --- /dev/null +++ b/internal/infrastructure/database/migrations/004_create_fts_index.sql @@ -0,0 +1,17 @@ +-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com) +-- Desc: Create a full text index on the recipes table. +-- Date: 07/10/2025 + +BEGIN; + +-- Update recipes table with the search vector column +ALTER TABLE Recipes +ADD search_vector tsvector GENERATED ALWAYS AS ( + setweight(to_tsvector('english', coalesce(title, '')), 'A') || + setweight(to_tsvector('english', coalesce(description, '')), 'B') +) STORED; + +-- Create the search index +CREATE INDEX idx_recipe_search_vector ON recipes USING GIN (search_vector); + +COMMIT; diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index 3372d2c..bdbf297 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -99,7 +99,13 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) { return nil, err } - query := "SELECT * FROM recipes WHERE id = $1" + query := ` + SELECT + id, title, description, instructions, serves, difficulty, duration, category, ingredients, + userid, modified, created + FROM recipes + WHERE id = $1 + ` var durationBytes []byte var ingredientBytes []byte @@ -160,10 +166,10 @@ func isBitActive(bits, pos int) bool { } // SearchRecipes will search the recipe table using the provided filters and return an unbound list -// of recipes. The filters are fairly complex, they are stored as bit masks. A more details -// description can be found in the recipe service implementation. Any errors will be bubbled to the -// caller. -// +// of recipes. The filters are fairly complex, they are stored as bit masks. A more details +// description can be found in the recipe service implementation. Any errors will be bubbled to the +// caller. +// // TODO: Pagination is required, to provide infinite scroll. func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) { tx, err := r.db.Begin() @@ -172,9 +178,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain return nil, err } - // Generate the query - query := "SELECT * FROM recipes" - // Compute meals type filters (there are 7 bits) var mealConditions []string for i := range 7 { @@ -257,15 +260,56 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain conditions = append(conditions, servingString) } - // Convert and append conditions - if len(conditions) > 0 { - conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND ")) - query = fmt.Sprintf("%s %s;", query, conditionsString) - } else { - query += ";" + // Define columns to select. More fields can be added if the full text search is required + columns := []string{ + "id", + "title", + "description", + "instructions", + "serves", + "difficulty", + "duration", + "category", + "ingredients", + "userid", + "modified", + "created", } - fmt.Println(query) + // Create search vector query + var orderBy string = "" + if filters.Search != "" { + vector_query := strings.ReplaceAll(filters.Search, " ", " | ") + + conditions = append( + conditions, + fmt.Sprintf("search_vector @@ to_tsquery('english', '%s')", vector_query), + ) + + template := ` + ORDER BY + ts_rank(search_vector, to_tsquery('english', '%s')) DESC, + ts_rank_cd(search_vector, to_tsquery('english', '%s')) DESC + ` + orderBy = fmt.Sprintf(template, vector_query, vector_query) + } + + // Generate the query + query := fmt.Sprintf("SELECT %s FROM recipes", strings.Join(columns, ",")) + + // Convert and append conditions if provided + if len(conditions) > 0 { + conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND ")) + query = fmt.Sprintf("%s %s", query, conditionsString) + } + + // Append sorting order if exists + if len(orderBy) > 0 { + query = fmt.Sprintf("%s %s", query, orderBy) + } + + // Finish it off with a colon! + query += ";" // Execute the query rows, err := tx.Query(query)