(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:
parent
70536147b7
commit
3c5109c7d0
@ -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;
|
||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user