Searching for and locating data is one of the primary benefits of using computers (alongside computing, of course). Moreover, it is a task indelible to computers, fundamental to why we would want to use them in the first place.
Yet, I let this fact slip to the back of my mind, and it only resurfaced when I went to add an administrative search tool to an application. Then, I reminded myself that searching documents (PDFs) was one of the first curiosities that piqued my interest in programming.
At a previous job, before I became a developer, our company had a box of PDFs. Sometimes, I manually searched through the box of PDFs, looking for a document that may or may not exist. That made me crazy. I knew betters ways existed.
Flash forward, and I built the tool I dreamed of a few years prior.
A key player in the application is Amazon Textract.
"Amazon Textract is a machine learning (ML) service that automatically extracts text, handwriting, and data from scanned documents."
Once processing is complete in the main application, the file goes to Textract and returns a long string of characters containing all the words it found.
You can see an example of this on AWS.
At this point, the document in the application has:
- an ID
- S3 link
- AWS Textract output text
Next, the application sends it off to an OpenSearch instance, waiting to index new documents.
AWS is behind the next step in the search application by providing Amazon OpenSearch Service:
"OpenSearch is a distributed, community-driven, Apache 2.0-licensed, 100% open-source search and analytics suite used for a broad set of use cases like real-time application monitoring, log analytics, and website search."
When OpenSearch indexes the document, the text is easily searchable in combination with the date of the document.
The search application allows the user to search by text query and then narrow that search with a date range.
It is a simple query structure. OpenSearch allows for much more complex query parameters and special search characters. However, all data for the document saved comes from the PDF. Therefore, the text string contains all searchable data.
As I mentioned early, the search application is part of a more extensive web application utilizing React Admin on the front end.
It's a three-input form interface, first giving the user the chance to specify a date range and then letting them enter a search phrase.
React Query is an instrumental library that made the front-end implementation of this app seemingly effortless.
React Query simplifies many aspects of querying the API:
- Updating state
- Caching data
- Error handling
- Canceling requests
Search applications should start searching as the user is typing. Recently, this has become an expectation of users across applications.
In the past, I used a custom debounce hook to handle when to send the API call to the server. With React Query, I can wrap the API request in a set-timeout function and then put that into a promise. Then, I include a cancel token in the Axios request to stop the request if the user continues to type. Example code in the React Query documentation demonstrates how to do this.
Finally, React Query makes caching request data easy, meaning that the application does not have to refetch data that the application just fetched if the user enters the same query terms again.
Caching is invaluable for requests with large payloads and reduces the amount of work done on the server.
Searching for documents can take a few seconds. First, the front end calls the back end, which needs to call the OpenSearch service.
After receiving the results, the front end needs to call the backend again to stream the PDF files that offer the user a preview of the file.
The user needs different visuals to accommodate the multi-faceted process of searching and loading documents.
First, as the user is typing, a single animation represents fetching. Although this animation appears, the application is not fetching the data because the user is still typing. Regardless, the user sees that the application recognizes their action and is waiting for them to finish.
Next, the results are skeleton loaded into the application instead of waiting to appear when all the data is ready. For a brief second, the user will see the number of documents in skeleton form or a "No Results Found" message.
Finally, text loads faster than files. Dividing the loading into different parts of the component allows the text to load immediately and the file preview to appear when it is ready. Again, this tiered loading gets the data to the user as soon as possible, neatly, and improves the user experience.
The user now sees the results of her search and can decide, based on the text and preview, to further identify the document by viewing an enlarged version or immediately downloading the PDF.
Suppose they decide to view the document, and a full-screen modal displays the PDF. The user can view all the pages and read the file's contents better.
Again, they can decide to download the document by clicking the button in the top right or return to their search by closing the modal.
A modal to display the file keeps the search active while keeping the user's search organized. This is compared to opening a new tab to view the PDF, navigating to a different part of the application, or downloading the PDF for viewing.
One of the excellent aspects of building this application was that I was the user. I knew what it needed and how it could be helpful! Another benefit of building applications for internal use or a personal project is I get to learn new technologies and use new libraries!