Searching and paginating results is more or less a straightforward task for the frontend service. All our service needs to do is parse the search terms, offset from the request's query string, and invoke the Query method of the text indexer store that was passed as a configuration option when the service was instantiated.
Then, the service consumes the result iterator until it has either processed enough results to populate the results page or the iterator reaches the end of the result set. Consider the following code:
for resCount := 0; resultIt.Next() && resCount < svc.cfg.ResultsPerPage; resCount++ { doc := resultIt.Document() matchedDocs = append(matchedDocs, matchedDoc{ doc: doc, summary: highlighter.Highlight( template.HTMLEscapeString( summarizer.MatchSummary(doc.Content), ), ), }) }
The service creates a decorated model for each result that provides some convenience methods that will be called by the Go code blocks within the template. In addition, the matchedDoc type includes a summary field, which is populated with a short excerpt of the matched page's contents, with the search terms highlighted.
To highlight search terms in the text summary, the keyword highlighter will wrap each term in a <em> tag. However, this approach requires the result page template to render summaries as raw HTML. Consequently, we must be very careful not to allow any other HTML tags in our result summaries as this would make our application vulnerable to cross-site scripting (XSS) attacks. While the crawler component strips all the HTML tags from crawled pages, it doesn't hurt to be a little paranoid and escape any HTML characters from the generated summaries before passing them through to our keyword highlighter.
To be able to render the navigation header and footer, we need to provide the page template with information about the current pagination state. The following code shows how the paginationDetails type is populated with the required bits of information:
pagination := &paginationDetails{ From: int(offset + 1), To: int(offset) + len(matchedDocs), Total: int(resultIt.TotalCount()), } if offset > 0 { pagination.PrevLink = fmt.Sprintf("%s?q=%s", searchEndpoint, searchTerms) if prevOffset := int(offset) - svc.cfg.ResultsPerPage; prevOffset > 0 { pagination.PrevLink += fmt.Sprintf("&offset=%d", prevOffset) } } if nextPageOffset := int(offset) + len(matchedDocs); nextPageOffset < pagination.Total { pagination.NextLink = fmt.Sprintf("%s?q=%s&offset=%d", searchEndpoint, searchTerms, nextPageOffset) }
A previous result page link will always be rendered when the current result offset is greater than 0. Unless we are moving back to the first result page, the link will always include an offset parameter. Similarly, the next result page link will be rendered as long as we haven't reached the end of the result set.