(DB/FEAT): Implemented full text search vector for database searching!

This is a HUGE upgrade and can mark the searching nearly complete! Other
than the scrolling and some other smaller UI things. The search appears
to be working.
This commit is contained in:
Hayden Hargreaves 2025-07-10 19:54:21 -07:00
parent 70536147b7
commit 3c5109c7d0
2 changed files with 76 additions and 15 deletions

View File

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

View File

@ -99,7 +99,13 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
return nil, err 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 durationBytes []byte
var ingredientBytes []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 // 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 // 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 // description can be found in the recipe service implementation. Any errors will be bubbled to the
// caller. // caller.
// //
// TODO: Pagination is required, to provide infinite scroll. // TODO: Pagination is required, to provide infinite scroll.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) { func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain.Recipe, error) {
tx, err := r.db.Begin() tx, err := r.db.Begin()
@ -172,9 +178,6 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
return nil, err return nil, err
} }
// Generate the query
query := "SELECT * FROM recipes"
// Compute meals type filters (there are 7 bits) // Compute meals type filters (there are 7 bits)
var mealConditions []string var mealConditions []string
for i := range 7 { for i := range 7 {
@ -257,15 +260,56 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
conditions = append(conditions, servingString) conditions = append(conditions, servingString)
} }
// Convert and append conditions // Define columns to select. More fields can be added if the full text search is required
if len(conditions) > 0 { columns := []string{
conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND ")) "id",
query = fmt.Sprintf("%s %s;", query, conditionsString) "title",
} else { "description",
query += ";" "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 // Execute the query
rows, err := tx.Query(query) rows, err := tx.Query(query)