Skip to content

Commit

Permalink
Sorting filtering (#78)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris <[email protected]>
  • Loading branch information
SuliR123 and wyattchris authored Dec 3, 2024
1 parent 744a783 commit 6cb8a76
Show file tree
Hide file tree
Showing 8 changed files with 11,305 additions and 469 deletions.
7 changes: 4 additions & 3 deletions backend/internal/handlers/venues/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ func Routes(app *fiber.App, params types.Params) {
service := newService(params.Store)

// Create Protected Grouping
protected := app.Group("/venues")
protected := app.Group("/venues")

// Register Middleware
protected.Use(auth.Protected(&params.Supabase))
protected.Use(auth.Protected(&params.Supabase))

//Endpoints
protected.Get("/", service.GetAllVenues)
protected.Get("/", service.GetAllVenuesWithFilter)
protected.Get("/batch", service.GetVenuesByIDs)
protected.Get("/persona/:venueId", service.GetVenuePersona)

protected.Get("/search", service.GetVenueFromName)
protected.Get("/:venueId", service.GetVenueFromID)
Expand Down
132 changes: 100 additions & 32 deletions backend/internal/handlers/venues/venues.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package venues
import (
"fmt"
"log"
"math"
"net/http"
"strconv"
"strings"

"github.com/GenerateNU/nightlife/internal/errs"
"github.com/GenerateNU/nightlife/internal/models"
"github.com/GenerateNU/nightlife/internal/types"

"github.com/gofiber/fiber/v2"
Expand Down Expand Up @@ -142,41 +144,107 @@ func (s *Service) GetAllVenues(c *fiber.Ctx) error {
venues, err := s.store.GetAllVenues(c.Context())
if err != nil {
fmt.Println(err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Could not get venue")
return fiber.NewError(fiber.StatusInternalServerError, "Could not get venues")
}
return c.Status(fiber.StatusOK).JSON(venues)
}

// SORT FORMAT: /venues/getAll?sort= {sort}
// where sort can equal one of the following strings:
// ByPrice (no extra parameter needed)
// ByRating (no extra parameter needed)
// ByDistance {longitude} {latitude}
// ByRecommendation {persona_name} // must be one of the seven listed personas
func (s *Service) GetAllVenuesWithFilter(c *fiber.Ctx) error {
// parse all filters from the context
sort := c.Query("sort")
f := c.Query("filters")
filters := []string{} // default to empty array if no filters applied
if f != `` {
filters = strings.Split(f, ",")
}
// pass filters into SortAndFilter instance and retrieve query string
sortAndFilter := models.SortAndFilter{}
sortAndFilter = sortAndFilter.Make()
whereQuery := sortAndFilter.ConstructFilterQuery(filters)
sortQuery := sortAndFilter.SortVenues(sort)
// retrieve venues with given filters from db
venues, err := s.store.GetAllVenuesWithFilter(c.Context(), whereQuery, sortQuery)
if err != nil {
fmt.Println(err.Error())
return s.GetAllVenues(c) // attempt to get all venues without the filter (default choice if a sort isn't possible)
}
// Use SortAndFilter instance to sort the filtered list of venues and return final list
return c.Status(fiber.StatusOK).JSON(venues)
}

func (s *Service) GetVenuePersona(c *fiber.Ctx) error {
venueID := c.Params("venueId")
if venueID == "" {
return fiber.NewError(fiber.StatusBadRequest, "Venue ID is required")
}
formattedID, err := uuid.Parse(venueID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Couldn't parse venue id to uuid")
}

v, err := s.store.GetVenueFromID(c.Context(), formattedID)
if err != nil {
fmt.Println("error: " + err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Could not get venue")
}
total := v.AvgEnergy + v.AvgExclusive + v.AvgMainstream + v.AvgPrice
priceWeight := v.AvgPrice / total
mainstreamWeight := v.AvgMainstream / total
energyWeight := v.AvgEnergy / total
exclusiveWeight := v.AvgExclusive / total
temp := models.ByRecommendation{}
persona := ``
minDistance := math.Inf(1)
for key, value := range temp.CharacterMap() {
// energy, exclusive, mainstream, price
distance := math.Abs(float64(energyWeight)-float64(value[0])) + math.Abs(float64(exclusiveWeight)-float64(value[1])) + math.Abs(float64(mainstreamWeight)-float64(value[2])) + math.Abs(float64(priceWeight)-float64(value[3]))
if distance < minDistance {
persona = key
minDistance = distance
}
}
if persona == `` {
return c.Status(fiber.StatusOK).JSON("Not enough reviews to determine venue persona")
}
return c.Status(fiber.StatusOK).JSON(persona)
}

func (s *Service) GetVenuesByIDs(c *fiber.Ctx) error {
// Get the "ids" query parameter
ids := c.Query("ids")
if ids == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Missing venue IDs",
})
}

// Split the IDs into a slice
idStrings := strings.Split(ids, ",")
var venueIDs []uuid.UUID
for _, idStr := range idStrings {
parsedID, err := uuid.Parse(strings.TrimSpace(idStr))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": fmt.Sprintf("Invalid venue ID format: %s", idStr),
})
}
venueIDs = append(venueIDs, parsedID)
}

// Fetch venues from the store
venues, err := s.store.GetVenuesByIDs(c.Context(), venueIDs)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to fetch venue details",
})
}

// Return the list of venues
return c.Status(fiber.StatusOK).JSON(venues)
// Get the "ids" query parameter
ids := c.Query("ids")
if ids == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Missing venue IDs",
})
}

// Split the IDs into a slice
idStrings := strings.Split(ids, ",")
var venueIDs []uuid.UUID
for _, idStr := range idStrings {
parsedID, err := uuid.Parse(strings.TrimSpace(idStr))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": fmt.Sprintf("Invalid venue ID format: %s", idStr),
})
}
venueIDs = append(venueIDs, parsedID)
}

// Fetch venues from the store
venues, err := s.store.GetVenuesByIDs(c.Context(), venueIDs)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to fetch venue details",
})
}

// Return the list of venues
return c.Status(fiber.StatusOK).JSON(venues)
}
188 changes: 188 additions & 0 deletions backend/internal/models/SortAndFilter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package models

import (
"fmt"
"strconv"
"strings"
)

type SortAndFilter struct {
filterMap map[string]FilterBy
}

func (s *SortAndFilter) Make() SortAndFilter {
filterMap := map[string]FilterBy {
"PriceLessThan" : &PriceLessThan{},
"RatingGreaterThan" : &RatingGreaterThan{},
}
return SortAndFilter{filterMap: filterMap}
}

// assumes a non-empty list of filters, list of strings is case sensitive
func (s *SortAndFilter) ConstructFilterQuery(filters []string) string {
// based on list of strings for filters construct SQL WHERE queries to filter database based on certain parameters
if len(filters) == 0 {
return ``
}

query := `WHERE `
for i := range filters {
// map each given filter to its associated object
filter := filters[i] // get filter
index := strings.Index(filter, " ") // check if the filter has parameters/get the index for the command
command := filter
if index != -1 { // if the filter command has a parameter
command = filter[0:index]
parameter := filter[index + 1:] // extract parameter for the filter
s.filterMap[command].Set(parameter) // set the parameter for the query
}
// get the string for that object and concate WHERE command to query string
query += s.filterMap[command].WhereQuery()
if i + 1 < len(filters) {
query += ` AND `
}
}
return query
}

// assumes input is a non-empty string and one of the possible sort types, case sensitive
func (s *SortAndFilter) SortVenues(input string) string {
if input == `` {
return ``
}
// parse given input string to get sort string value and possible parameters for the sort
index := strings.Index(input, " ")
command := input
if index != -1 { // if the sort has a parameter
command = input[0:index]
}
// create sort object and sort array of venues based on sort
createdSort := s.createSort(command, input, index)
return createdSort.SortQuery()
}

// takes in an array of venues to sort, command string determing what sort we want to create, input string storing that commands parameters, and index of the first space is applicable
func (s *SortAndFilter) createSort(command string, input string, index int) SortBy {
if command == "ByDistance" { // format: ByDistance long lat
// parse location to check distance from
lastIndex := strings.LastIndex(input, " ")
long, err := strconv.ParseFloat(input[index + 1: lastIndex], 64)
if err != nil {
fmt.Println("Formatting error Long " + err.Error())
}
lat, err := strconv.ParseFloat(input[lastIndex + 1:], 64)
if err != nil {
fmt.Println("Formatting error Lat " + err.Error())
}
s := ByDistance{long, lat}
return &s
} else if command == `ByRecommendation`{
arr := strings.Split(input, " ")
return &ByRecommendation{arr[1]}
} else if command == `ByPrice` {
return &ByPrice{}
} else if command == `ByRating` {
return &ByRating{}
}
return &ByDistance{} // throw an error/return blank sort or something
}

// SORT TYPES

// NOTE: ALSO REORGANIZE FHEIWQHIO

type SortBy interface {
// returns a string for calculating a parameter to sort by (empty if sorting by defined venue parameter)
// order by string, determines what order to sort the array in
SortQuery() string
}

type ByDistance struct {
long float64
lat float64
}

func (s *ByDistance) SortQuery() string {
return fmt.Sprintf(`ORDER BY 6371 * 2 * ASIN(SQRT(
POWER(SIN((%f - ST_Y(location::geometry)) * PI() / 180 / 2), 2) +
COS(%f * PI() / 180) * COS(ST_Y(location::geometry) * PI() / 180) *
POWER(SIN((%f - ST_X(location::geometry)) * PI() / 180 / 2), 2)))`, s.lat, s.lat, s.long)
}

type ByPrice struct {}

func (s *ByPrice) SortQuery() string {
return `ORDER BY price`
}

type ByRating struct {}

func (s *ByRating) SortQuery() string {
return `ORDER BY total_rating`
}

type ByRecommendation struct {
personality string
}

func (s *ByRecommendation) SortQuery() string {
// user personalities will be measured as the 4 preferences split among a weight of 1
// for each venue calculate the average of each preference out of 10
// from the average calculation get a total overall rating and get the weight of each preference from that overall
// calculate the difference from the weights of the user higher score and sum them up, further away from what the user wants
query := `
ORDER BY
( ABS( ((avg_energy / (avg_energy + avg_exclusive + avg_mainstream + avg_price)) - %f) ) +
ABS( ((avg_exclusive / (avg_energy + avg_exclusive + avg_mainstream + avg_price)) - %f) )) +
ABS( ((avg_mainstream / (avg_energy + avg_exclusive + avg_mainstream + avg_price)) - %f) ) +
ABS( ((avg_price / (avg_energy + avg_exclusive + avg_mainstream + avg_price)) - %f) )
`
vals := s.CharacterMap()[s.personality]
return fmt.Sprintf(query, vals[0], vals[1], vals[2], vals[3])
}

func (s *ByRecommendation) CharacterMap() map[string][]float32 {
return map[string][]float32 {
// energy, exclusive, mainstream, price
`plumehart` : {.05, .3, .05, .6},
`serafina` : {.1, .2, .1, .5},
`buckley`: {.3, .1, .4, .1},
`roux`: {.2, .1, .4, .3},
`sprig`: {.4, .1, .4, .2},
`blitz`: {.7, 0, .2, .1},
`lumi`: {.6, .2, .2, 0},
}
}

// NOTE TO SELF - REORGANIZE PLZ

// FILTER TYPES:

type FilterBy interface {
WhereQuery() string // return the WHERE command to be passed into the SQL query
Set(v string) // assigns parameter within struct for filter command
}

type PriceLessThan struct {
price string
}

func (f *PriceLessThan) WhereQuery() string {
return `price < ` + f.price
}

func (f *PriceLessThan) Set(price string) {
f.price = price
}

type RatingGreaterThan struct {
rating string
}

func (f *RatingGreaterThan) WhereQuery() string {
return `total_rating > ` + f.rating
}

func (f *RatingGreaterThan) Set(rating string) {
f.rating = rating
}
18 changes: 16 additions & 2 deletions backend/internal/models/venues.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package models

import (
"database/sql"
"time"

"github.com/google/uuid"
Expand All @@ -23,9 +24,22 @@ type Venue struct {

Longitude float64 `json:"longitude"`

// VenueType string `json:"venue_type"`
VenueType string `json:"venue_type"`

CreatedAt time.Time `json:"created_at"`

//UpdatedAt time.Time `json:"updated_at"`
UpdatedAt sql.NullTime `json:"updated_at"`

TotalRating float32 `json:"total_rating"`

Price float32 `json:"price"`

AvgEnergy float32 `json:"avg_energy"`

AvgMainstream float32 `json:"avg_mainstream"`

AvgPrice float32 `json:"avg_price"`

AvgExclusive float32 `json:"avg_exclusive"`
}

Loading

0 comments on commit 6cb8a76

Please sign in to comment.