HadCoffee Blog – Development blog for HadCoffee.com

Laravel Meilisearch

Ok I’ve cracked it! The Scout driver is more limited in the query construction it can support.

I’m breaking out to use the Meili client directly and can build up my queries with a lot more flexibility.

Migrating Search to Meilisearch or Similar

I began a partial migration to Algolia for cafe search a while ago. That was a spike that only covered some of the simpler uses.

As part of the work to make location search easier with suburb autocompletion I’d also like to move the suburb database into a search index. The free tier of Algolia is limited and it’d blow away my quota, so I’m looking at Meilisearch (MS). That’d mean the cafe search would move into it as well.

Initial testing was ok, but it seems that MS does not support multiple geo location positions per document in the same way that ElasticSearch does. This would have been extremely useful, as my data model has a one to many relationship between cafes and their individual locations. The search would be easier to build if MS could internally handle that.

If I’m right, I’ll either need to do a hybrid, multi-step search to find cafes within a location, and then another with those ids to handle the main search which might sort by score and other ranking factors.

Going full ElasticSearch is also not that appealing because it’s not supported by Scout, so I’d have to do more custom data mapping, querying and syncing.

Much to consider.

Building the Cafe Photo Gallery Feature

I’ve held off on this for a while, but starting to build it out.

Did phase one today, which was:

  • New DB table for photos, using ulid for non-sequential ids
  • TDD: Writing tests that endpoint returns a list of photos for a cafe, and excludes ones that haven’t been approved yet
  • Creating the model and factory, and adding a global scope to the model so that by default only visible photos are returned.
    Admin interfaces that will need to get all photos for approval can bypass that scope (which is verified by a test).

Next steps could be endpoints and UI to actually upload photos. It’ll be a while before anything is visible on site, because uploading itself has a number of components. I’ll need a form to upload, possibly with Ajax for better UX, a queued job to resize images and maybe create a WebP conversion too, copying to S3 and updating the DB.

Ideally there’d also be a cropping widget in the browser for the thumbnail view.

To begin with only I will be able to add images, but I’d like to open it up to trusted users too.

Will also need tools for deleting (including S3 objects, not just the DB records).

Version 2.0 – Better Search and Discovery UX

Ok, it’s been a while and I think to make it more useful I need:

  • Better search with location autocomplete (suburb list), and Algolia or Meilisearch backend to do better natural language and sorting
  • Map result display
  • More photos for cafes so you can get a better sense of them

The lists (ToTry, Fav) should probably also be sortable by arbitrary location too.

Steps

  1. Get uniquified Australian suburb DB list
  2. Build an autocomplete using it so it’s way easier to select a location (Algolia?)
  3. Use that on main search and adding a cafe
  4. Once main search pulls location that way you can rewrite the querying to use location + Algolia to actually return results.
  5. Then UX improvements: Show them on a map etc.

HadCoffee Launched

Woot. I spent the last few weeks tidying up rough edges and doing the little tasks like email notifications, password resets, social images etc.

Opened up public registrations yesterday and posted on Slack today. Haven’t yet hit up the mailing list or Instagram account.

I wouldn’t mind doing a little video explainer of the features. A reel or two if you will.

All up, to get to this point was 633 hours over 4½ years

  • 2022 – 123h 52m (to April)
  • 2021 – 126h 04m
  • 2020 – 146h 20m
  • 2019 – 93h 48m
  • 2018 – 129h 18m
  • 2017 – 13h 36m

Sign up here

Deleting a Coffee Rating

Seldom used, but important to have the option in the case the user has written something they don’t want public anymore.

I’ve written the first pass of the feature: Ajax request from the coffee history on the cafe page, server authorization with the Laravel policy and the simple CRUD delete action is working.

The other part of this feature though will be recalculating cafe data now that the input ratings have changed. I’ll see to reconfigure some of these jobs to recompute the cafe’s global ratings; the user’s own ratings at the cafe and the position on their most frequent list.

I do have an existing event listener that recalculates new ratings, but unfortunately it’s specific to adding new ratings. When a user deletes a coffee the input data will be different (one less rating); so I need to change this code to recalc from the remaining data set.

Reworking the Fav and “To Try” Coffee List Feature

One of the main features of HadCoffee is keeping a list of your favourite cafes and the ones you want to try. Favs are useful when people ask for recommendations and you’re at risk of forgetting those dearest to you.

To Try is a good way to bookmark cafes to visit when you’re near them later.

Cafes can be added and removed from these lists with their UI controls. Initially these were only present on the cafe detail page, but over time I have included them on the search results listing and on the Cafes page.

To correctly show the status (i.e. whether a cafe is already on either of these lists) I needed to rebuild the local state to use Vuex. This means the icons can be rendered in multiple locations of the UI and show synced state. It also makes it far easier to deploy them in new UI sections later.

To support that data structure though I needed to do a refactor of the backend API which provides that data from the server. The API can be used to both list just the cafe ids for each list, or return more fleshed out object data for the features that render more complete cafe information.

UI screenshot diagram. Multiple Vue component with linked state

I’m happy with this final design. I can now place these controls wherever they need to go on the UI and they’ll initiate with state from the server, then manage it locally through Vuex. As a bonus you get a nice little page load animation with the star filling up and the list clicking on if the cafe is active on the list.

Allowing Users to Add Cafes

This was always part of the plan, however there are a few steps required to get this going. As an admin I have already coded most of the functionality, however for non-admin users I need to consider:

  • Suggesting existing cafes they may have missed
  • Being able to merge cafes for when duplicates are created
  • Verifying the user entry to fix mistakes, add additional optional info such as social media profiles for the cafes, or even cleaning up or monitoring spam. Creating a cafe essentially gives some entity a web page on my app, so it could be a target for abuse.
  • Adding more locations to an existing cafe
  • Suggesting that cafes or specific locations have closed down (happens quite a lot, especially with covid).

Right now my Controllers are fragmented with a frontend and admin equivalent. I need to move the ::store method to the public one, use a Form Request to validate and handle the slight differences in behaviour depending on who is adding.

Also need to ensure the geocoding job is queued in either case, and possibly monitor usage as there could be an API cost if it’s abused.

MVP

To get something going soon I think I’ll let users add cafes until there is abuse. I might throttle them to adding N in some number of hours/days.

I’ll review additions manually, and later when I can I’ll build the merge tool.

Still so much work for what seems a simple CRUD feature.

Managing List State Globally

I’ve progressed the search feature and would like the cafes listed in the results to use the components for the Favourites and To Try lists. The Vue components themselves can be dropped in, but I realised that they don’t have a default link to their underlying state.

Where they are used on other pages in the app I have loaded the data as part of the initial server response, which flows through to the components.

So now I have to either replicate that pattern on other pages, or refactor the components to use Vuex state and add an API endpoint to load in the data. The downside is it creates a bit more frontend work for me now, and also requires another HTTP request to load the full page data. The upside is more portable, self contained components.

In hindsight I can see why an SPA approach using Intertia would be helpful as it would remove some of this extra work, but I didn’t have the scope to build that way when I started this project.

I’m wondering if this refactor will also allow for fresher data on the My Cafes page. If user toggles a Fav/ToTry status on one route, it currently doesn’t automatically update if they switch Vue routes. If I move to central Vuex state that problem might be solved by default. So it’s a more robust solution.