Mat Ryer · 17 Nov 2020
Mat Ryer · 17 Nov 2020
Pace customers search through their cards and conversations using the built-in search box in the app.
This powerful search is powered by Firesearch.dev, which is now available for you to use in your own apps.
This post will explain how Pace uses Firesearch to achieve its search experience.
It’s important for customer data to remain siloed, so Pace uses an index per organisation.
When a new organisation is created, Pace uses the IndexService.CreateIndex
method to programmatically create a dedicated index for that org.
We create multiple indexes per customer to serve different use cases. For example, Firesearch has a dedicated Autocomplete API, which we use to complete names in mentions and invite boxes.
Pace also distinguishes between card and conversation search, so we use an index for each.
Whenever a new card is added or updated, Pace kicks off a background task (using a pubsub service) which loads the updated card, and uses the IndexService.PutDoc
method to update its entry in the index.
The work is done in the background so it doesn’t impact the user experience. The card search is therefore eventually consistent.
If a card is deleted, the background task will go and remove it from the index using the IndexService.DeleteDoc
method.
Each card gets its own document in our customer’s index.
In Firesearch, each document has a unique ID. This allows us to update or delete them.
In Pace, we use the card ID as the document ID. Whenever a card changes, we know which document to update (since the IDs are the same).
A typical search document looks like this:
{
"id": "card-123",
"title": "Title of the card",
"searchFields": [
{
"key": "title",
"value": "Title of the card",
"store": true
},
{
"key": "body",
"value": "This is the body of the card."
}
],
"fields": [
{
"key": "teamID",
"value": "playground-team-id"
},
{
"key": "cardStatus",
"value": "done"
},
{
"key": "isOpen",
"value": true
}
]
}
We include the title and body of the card in the searchFields
. We set store: true
on the title
field because we want to get it back in the search results later.
We also include some additional fields to drive our user experience. These are explained below.
Whenever source data changes (in our case, cards), we want to update the search index document to reflect the changes.
In Firesearch, you do not update individual fields within a document (like when a card’s status changes), instead you have to put the entire document each time.
This is worth bearing in mind when you come to design your system.
In our case, a pubsub driven background task is perfect. Whenever the title, body or status of a card changes, we fire an event which indicates that we want the search document to be updated.
This means that our search is eventually consistent, although things happen pretty quickly so you don’t ever notice that as a user.
Search is best when the user actively or passively provides additional context about what they’re looking for.
In our case, the user is likely searching for open cards (i.e. future work, up next, or in progress) by default. We allow them to change this selection in the UI, which we oblige by tweaking the filters
in our search query.
Let’s take another look at those fields:
"fields": [
{
"key": "teamID",
"value": "playground-team-id"
},
{
"key": "cardStatus",
"value": "done"
},
{
"key": "isOpen",
"value": true
}
]
teamID
filter field allows us to scope search to an individual team - although this is optional at query timecardStatus
filter field lets users search within specific statues (or by omitting this field from the query to search all cards)isOpen
boolean filter lets us specify whether a card is open or not (i.e. status != "done"
)—omitting a value altogether in the query has the result of not filtering the results, so they would include both open and closed cardsFor performance reasons, Firesearch provides equality checking in filters rather than greater than, less than, not equal to, etc.
Instead of writing queries against a large selection of fields like you might in a document store, it is recommended to do the business logic work up-front, and store the results in the document.
Notice in Pace how we set the isOpen
value when we write the document (we aren’t doing an IN query on the status
field, we’re just doing a true
or false
on a simple boolean).
An ecommerce example might show that product price ranges might have a priceGroup
string field that has one of a possible range of values, depending on which group the item is in: 0-99
, 100-199
, 200-plus
, etc.
We covered how we use Firesearch at Pace to power all our customers’ card search, conversation search, and name autocomplete.
A lot of our blog posts come out of the technical work behind a project we're working on called Pace.
We were frustrated by communication and project management tools that interrupt your flow and overly complicated workflows turn simple tasks, hard. So we decided to build Pace.
Pace is a new minimalist project management tool for tech teams. We promote asynchronous communication by default, while allowing for those times when you really need to chat.
We shift the way work is assigned by allowing only self-assignment, creating a more empowered team and protecting the attention and focus of devs.
We're currently live and would love you to try it and share your opinions on what project management tools should and shouldn't do.
What next? Start your 14 day free trial to see if Pace is right for your team
or you can share the URL directly:
https://pace.dev/blog/2020/11/17/pace-search-with-firesearch.dev.html
Thank you, we don't do ads so we rely on you to spread the word.
If you have any questions about our tech stack, working practices, or how project management will work in Pace, tweet us any time. :)
— pace.dev (@pacedotdev) February 25, 2020
Batching operations in Go #Golang #Patterns
Evil UX patterns for attention seeking apps #UX #Design #Productivity
How code generation wrote our API and CLI #codegen #api #cli #golang #javascript #typescript