Compare commits

..

No commits in common. "d1ecd8f5a3a606c9bfe8603881e2dee041d4571c" and "1e88f075cb5f967885058f5a58b7562a2a58c697" have entirely different histories.

View File

@ -421,72 +421,52 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
conditions = append(conditions, servingString) conditions = append(conditions, servingString)
} }
// Define columns to select // Define columns to select. More fields can be added if the full text search is required
columns := []string{ columns := []string{
"r.id", "r.id",
} }
// Create search vector query with SAFE parameterization // TODO: Need to add these to the query
var orderBy string = ""
var searchQuery string = ""
// FROM ... JOIN favorites f ON f.recipeId = r.id
// WHERE ... AND f.userId = 3
// Create search vector query
var orderBy string = ""
if filters.Search != "" { if filters.Search != "" {
spl := strings.Split(filters.Search, " ") spl := strings.Split(filters.Search, " ")
var cleaned []string var cleaned []string
// Use a string replacer for safety // Use a string replacer, each word in the query will be passed through this
replacer := strings.NewReplacer( replacer := strings.NewReplacer(
"'", "", "'", "",
"-", "", "-", "",
"&", "", "&", "",
"|", "", "|", "",
"!", "", "!", "",
":", "", // Remove colons to prevent tsquery syntax injection
"(", "",
")", "",
) )
for i := range len(spl) { for i := range len(spl) {
q := strings.TrimSpace(replacer.Replace(spl[i])) q := strings.TrimSpace(replacer.Replace(spl[i]))
if q != "" { if q != "" {
// Add :* suffix for prefix matching cleaned = append(cleaned, q)
cleaned = append(cleaned, q+":*")
} }
} }
// Join with OR operator for full-text search
vector_query := strings.Join(cleaned, " | ") vector_query := strings.Join(cleaned, " | ")
searchQuery = vector_query
// Full-text search with prefix matching conditions = append(
searchCondition := fmt.Sprintf("r.search_vector @@ to_tsquery('english', '%s')", vector_query) conditions,
fmt.Sprintf("r.search_vector @@ to_tsquery('english', '%s')", vector_query),
)
// Add fallback ILIKE for true substring matching template := `
// This catches cases where "pan" is inside "pancake" but not at word boundaries
var ilikeConditions []string
for _, term := range spl {
cleanTerm := strings.TrimSpace(replacer.Replace(term))
if cleanTerm != "" {
ilikeConditions = append(ilikeConditions, fmt.Sprintf("(r.title ILIKE '%%%s%%' OR r.description ILIKE '%%%s%%')", cleanTerm, cleanTerm))
}
}
if len(ilikeConditions) > 0 {
searchCondition = fmt.Sprintf("(%s OR %s)", searchCondition, strings.Join(ilikeConditions, " OR "))
}
conditions = append(conditions, searchCondition)
// Ranking with preference for full-text matches
orderBy = fmt.Sprintf(`
ORDER BY ORDER BY
CASE
WHEN r.search_vector @@ to_tsquery('english', '%s') THEN 1
ELSE 2
END,
ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC, ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC,
ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC
`, searchQuery, searchQuery, searchQuery) `
orderBy = fmt.Sprintf(template, vector_query, vector_query)
} }
// Generate the query // Generate the query
@ -496,6 +476,8 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
"SELECT %s FROM recipes r JOIN favorites f ON f.recipeId = r.id", "SELECT %s FROM recipes r JOIN favorites f ON f.recipeId = r.id",
strings.Join(columns, ","), strings.Join(columns, ","),
) )
// Add new favorite condition to the conditions list
conditions = append(conditions, fmt.Sprintf("f.userid = %d", *userId)) conditions = append(conditions, fmt.Sprintf("f.userid = %d", *userId))
} else { } else {
query = fmt.Sprintf("SELECT %s FROM recipes r", strings.Join(columns, ",")) query = fmt.Sprintf("SELECT %s FROM recipes r", strings.Join(columns, ","))
@ -513,7 +495,7 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
query = fmt.Sprintf("%s %s", query, orderBy) query = fmt.Sprintf("%s %s", query, orderBy)
} }
// Finish it off with a semicolon! // Finish it off with a colon!
query += ";" query += ";"
// Execute the query // Execute the query
@ -529,6 +511,7 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
if err := rows.Scan(&id); err != nil { if err := rows.Scan(&id); err != nil {
return []int{}, fmt.Errorf("failed to extract ID: %s\n", err.Error()) return []int{}, fmt.Errorf("failed to extract ID: %s\n", err.Error())
} }
ids = append(ids, id) ids = append(ids, id)
} }