About a week ago I write a tutorial for implementing a typeahead search with Node.js and jQuery. A typeahead is one of many great use-cases when using full text search (FTS), but it certainly isn’t the only use-case. As many of you know, I am as big of a fan of Golang as I am Node.js, so I figured it would be a good idea to see the same example, but this time using a different programming technology.
We’re going to see how to use typeahead.js, a jQuery extension, as well as Couchbase and the Go programming language to implement a type of autocomplete in our application.
If you didn’t see my previous article, don’t worry because it is a different language so a different problem. We’re going to start from scratch and see how things are done with Go. The assumption is that you have Go installed and configured as well as Couchbase installed and configured.
Preparing a Dataset and a Full Text Search Index
For this example we’re going to work with a very simple dataset, but it could be significantly more complex with minimal extra effort. Our dataset will look something like the following:
1 2 3 4 5 |
{ "title": "Forever & Always", "artist": "Taylor Swift", "type": "song" } |
Each document in our database will represent a song. For simplicity, we’re only going to focus on the title
and artist
information, but the documents could contain a wide variety of information.
The next step is to create an FTS index for our documents.
The above image demonstrates what an index should look like. When it comes to the type mapping, we’re only going to be mapping the song
documents. We’re also specifying that we only want the artist
and title
information in our index. The important part here is that we are storing the fields in the index. If we don’t specifically choose to store the field in the index, then we will have no information to populate in the results of our search.
Creating the Go Powered Full Text Search (FTS) Backend
With some data and our FTS index in place, we can start developing the backend that will execute the queries. Our backend will be a RESTful API with a single endpoint for searching. The boilerplate for this application can be seen below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package main import ( "encoding/json" "net/http" "github.com/gorilla/handlers" "github.com/gorilla/mux" gocb "gopkg.in/couchbase/gocb.v1" "gopkg.in/couchbase/gocb.v1/cbft" ) type Song struct { Id string `json:"id"` Score float64 `json:"score"` Artist string `json:"artist"` Title string `json:"title"` } var bucket *gocb.Bucket func SearchEndpoint(response http.ResponseWriter, request *http.Request) { } func main() { cluster, _ := gocb.Connect("couchbase://localhost") cluster.Authenticate(gocb.PasswordAuthenticator{Username: "example", Password: "123456"}) bucket, _ = cluster.OpenBucket("example", "") router := mux.NewRouter() router.HandleFunc("/search", SearchEndpoint).Methods("GET") http.ListenAndServe(":12345", handlers.CORS(handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), handlers.AllowedOrigins([]string{"*"}))(router)) } |
First we’re creating a native Go data structure that will eventually hold our anticipated search results. Skipping over the endpoint logic for now, we have our main
function where we connect to our database and define our only endpoint. Because searching will happen via JavaScript, we need to configure cross-origin resource sharing (CORS) to allow the requests.
If you’d like to learn more about cross-origin resource sharing with Go, I wrote a previous article on the subject. However, knowing the ins and outs isn’t too important for this example.
Let’s take a look at the function that matters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func SearchEndpoint(response http.ResponseWriter, request *http.Request) { response.Header().Set("content-type", "application/json") params := request.URL.Query() query := gocb.NewSearchQuery("idx-music", cbft.NewMatchQuery(params.Get("query"))) query.Fields("artist", "title") result, _ := bucket.ExecuteSearchQuery(query) var hits []Song for _, hit := range result.Hits() { hits = append(hits, Song{ Id: hit.Id, Score: hit.Score, Artist: hit.Fields["artist"], Title: hit.Fields["title"], }) } json.NewEncoder(response).Encode(hits) } |
When this function is called, we extract any parameters from the request query parameters. These parameters are then used to create a new query against our full text search index. In the response we want both the artist
and title
fields so we must define them in our query. Upon successful execution, we loop through our search hits and construct an object that might be useful to our frontend.
If you wanted to run the Go application, http://localhost:12345/search?query=swift could potentially return results.
Designing the jQuery Frontend for Typeahead Searches Against the Database
With the backend in place, now we can focus on the frontend. The frontend will be universal regardless as to which backend language you’re going to be using.
Our goal is to create something like this:
We’re going to be using typeahead.js, jQuery, and Handlebars.js to make this part a success. However, it is very important that we use version 0.10.5 of typeahead.js due to a bug in later versions.
With the libraries downloaded, create a project structure that looks like the following:
1 2 3 4 5 |
js handlebars-v4.0.11.js jquery-3.3.1.min.js typeahead.bundle.min.js index.html |
Make sure that your versions match your file versions and not mine.
In the index.html file, we can bootstrap our code with the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<html> <head> <style> body { background-color: #F0F0F0; } .tt-input, .tt-hint, .twitter-typeahead { width: 100%; } </style> </head> <body> <div id="remote"> <input class="typeahead" type="text" placeholder="Songs..."> </div> <script src="js/jquery-3.3.1.min.js"></script> <script src="js/handlebars-v4.0.11.js"></script> <script src="js/typeahead.0.10.5.bundle.min.js"></script> <script> // Logic here </script> </body> </html> |
Notice that the above code essentially just contains a form input and the importing of all our libraries. I’ve commended out where our logic for the typeahead will end up.
Let’s bring this to a close with our logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$('#remote .typeahead').typeahead( { hint: true, minLength: 3, highlight: false, limit: 5 }, { name: "song", displayKey: "id", source: function (query, callback) { $.getJSON("http://localhost:12345/search?query=" + query, function (data) { return callback(data); }); }, templates: { suggestion: Handlebars.compile('<div>{{title}} – {{artist}}</div>') } } ); |
The first part of our typeahead is the configuration. We’re saying we want hints to show and that our logic won’t happen unless there are at least three characters in the search to save on remote calls. We won’t highlight the results, but we’ll limit them to five. To drive the logic of our typeahead, we’ll issue a request to our backend and process the response. It will be rendered based on our custom Handlebars template.
The displayKey
is what we want to show after a result is clicked. In our scenario, we want to show the artists and song titles, but populate the id when clicked. This way the id can be sent in a future request if we wanted to.
Conclusion
You just saw how to create a typeahead search in jQuery with Go as the backend and Couchbase as the NoSQL database. This tutorial was a followup to a previous tutorial where I had written the FTS backend with Node.js and JavaScript.
For more information on using the Go SDK with Couchbase, check out the Couchbase Developer Portal.